JVM之内存分配的详细解析

发布于:2024-05-07 ⋅ 阅读:(23) ⋅ 点赞:(0)

内存分配

两种方式

不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象

  • 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离

  • 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容

TLAB

TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做快速分配策略

  • 栈上分配使用的是栈来进行对象内存的分配

  • TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占

JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过使用加锁机制确保数据操作的原子性,从而直接在堆中分配内存

栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存

参数设置:

  • -XX:UseTLAB:设置是否开启 TLAB 空间

  • -XX:TLABWasteTargetPercent:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1%

  • -XX:TLABRefillWasteFraction:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配

逃逸分析

即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 Client、Server 和分层编译

  • C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进

  • C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译

逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸

  • 方法逃逸:当一个对象在方法中定义之后,被外部方法引用

    • 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值

    • 参数逃逸:一个对象被作为方法参数传递或者被参数引用

  • 线程逃逸:如类变量或实例变量,可能被其它线程访问到

如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配

  • 同步消除

    线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的同步锁,通过 -XX:+EliminateLocks 可以开启同步消除 ( - 号关闭)

  • 标量替换

    • 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问

    • 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型

      聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量

    • 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替

    • 参数设置:

      • -XX:+EliminateAllocations:开启标量替换

      • -XX:+PrintEliminateAllocations:查看标量替换情况

  • 栈上分配

    JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC

    User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力

    public class JVM {
        public static void main(String[] args) throws Exception {
            int sum = 0;
            int count = 1000000;
            //warm up
            for (int i = 0; i < count ; i++) {
                sum += fn(i);
            }
            System.out.println(sum);
            System.in.read();
        }
        private static int fn(int age) {
            User user = new User(age);
            int i = user.getAge();
            return i;
        }
    }
    ​
    class User {
        private final int age;
    ​
        public User(int age) {
            this.age = age;
        }
    ​
        public int getAge() {
            return age;
        }
    }