TLAB 分配和 栈分配解析

  前文已经讲过jvm 在初始化类的时候需要在堆内存中开辟一块内存空间用于存放实例化的类的信息。其实在仔细想想如果在多个线程同时new 同一个对象的时候 虚拟机会不会将两个不同引用的对象指向了同一块内存地址呢(当然不会),那他是怎么实现的。

  我们都知道当虚拟机在运到 new 关键字的时候(反射,clone 方法同理) 首先判断当前类是否被加载过 如果加载过了,就执行实例化操作。实例化需要在堆内存中开辟空间用于存放对象的一些常量,而堆又是一个共享内存所以需要考虑线程安全,当然最简单的方式就说将开僻空间这块使用cas 来保证线程的安全,但是这样效率太低(jvm 不会这么粗糙的使用这么暴力的方式)。其实jvm 默认采用的是  TLAB 方式来保证线程安全的,其实就是jvm 会为每个 线程多分配一快空间 TLAB占用的是eden区的空间,会占很小的一部分空间,参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间(GC的年轻代空间——GC快)的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。使用-XX:TLABSize手工指定一个TLAB的大小。-XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为,因为虚拟机内部的默认值会有一套默认的机制来保证虚拟机运行的情况最优且内存使用的相对合理。

  优化的方案——例如 ,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。  转载自  https://blog.csdn.net/xiaomingdetianxia/article/details/77688945

  其实除了 TLAB 分配还有一种分配方式 叫 栈分配,栈分配的其实就是将内存直接分配到栈空间中,因为栈空间的内存是不受GC控制的每次使用完 直接清除,这样一是性能要高一些 ,二是在分配内存的时候不会造成线程安全的问题。栈分配也有缺点 就是 当一个大的对象直接分配在栈中的时候栈内存会很快就被使用完成,所以是不能直接分配大对象在栈内存中,还有一个问题是 该对象是否在该方法中使用 如果超出了该方法的使用范围也不适合使用栈分配——逃逸对象。下面给一个例子解释一下我对逃逸对象和栈分配的理解。

 

public class Demo2 {
    
    private static Student student =null;
    public static void main(String[] args) {
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {
            initStu2();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
 
    // 此方法会走栈内存分配
    public static void initStu1() {
        Student student = new Student();
        student.setAge("10");
        student.setName("zhangsan");
    }
    
    // 此方法不会走栈内存分配
    public static void initStu2() {
        student = new Student();
        student.setAge("100");
        student.setName("lisi");
    }
    
}


class Student{
    
    private String name;
    
    private String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
    
}

 

启动参数
-server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
+DoEscapeAnalysis 开启逃逸分析
-DoEscapeAnalysis 关闭逃逸分析
-XX:+EliminateAllocations 使用标量替换
-XX:-EliminateAllocations 不使用标量替换

  XX:+UseTLAB 使用TLAB
  -XX:+TLABSize 设置TLAB大小
  -XX:+PrintTLAB 查看TLAB信息
  -XX:ResizeTLAB 自调整TLABRefillWasteFraction阈值

 

在开启 逃逸分析和 开启标量替换的时候 执行initStu1 不会执行GC   执行initStu2 的时候会产生GC

在开启逃逸分析 不开启标量替换的时候 执行initStu1 和 initStu2 都会产生GC

 

总结:

  其实JVM 是默认开启 TLAB 的 然后 栈内存分配是 TLAB的一种优化方式  开启栈分配的条件 有两个 ,一是 开启逃逸分析 ,二是 使用标量替换。

  * 全局变量赋值逃逸,方法返回值逃逸,实例引用发生逃逸,线程逃逸,不逃逸  其中引用分配给一个静态变量一定会产生逃逸

  * 前面没有提及标量替换,这里来解释一下  通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。

整个TLAB的过程 用以下图来形容  

  

 

 

 

  

 

 

 

 

 

posted @ 2019-11-13 20:13  小学生很小  阅读(1106)  评论(0)    收藏  举报