Java对象内存分配流程

内存分配流程

Java对象内存分配流程_第1张图片

针对不同年龄段的对象分配原则

  • 优先分配到Eden区
  • 大对象(过长的字符串、数组)直接分配到老年代,尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代

动态对象年龄判断

  • 如果survivor区中相同年龄的所有对象所占内存大小的总和大于survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保:-XX:HandlePromotionFailure

对象分配过程:TLAB

  • TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块每个线程私有的内存分配区域,它存在于Eden区,TLAB空间的内存非常小,仅占有整个Eden空间的1%

作用:

  • 为了加速对象的分配,由于对象一般分配在堆上,而堆是线程共享的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降
  • 考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率,称之为快速分配策略。

其他TLAB说明

  • 不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
  • -XX:UseTLAB: 设置是否开启TLAB。
  • -XX:TLABWasteTargetPercent: 设置TLAB空间所占用Eden空间的百分比大小。
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

局限性: TLAB空间一般不会太大(占Eden区1%),所以大对象无法进行TLAB分配。

逃逸分析

堆中分配对象是唯一的选择吗?

  • 《深入理解JVM虚拟机》:对象在Java堆中分配内存,这是一个普遍的常识了,但是有一种特殊情况,那就是如果经过逃逸分析后发现一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样能尽量减少将对象分配的堆中,减少OldGC或FullGC的次数,提高性能

什么是逃逸分析?

逃逸分析的基本行为就是分析对象动态作用域

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

快速判断逃逸:只要在方法内new的对象实体在外部被使用到了,则认为发生了逃逸,不管是不是静态,只要new的对象跑出了方法,注意关注的是对象实体,不是变量名,

/**
 * 没有发生逃逸,则可以分配到栈上,随着方法的执行而结束。
 */
public void test1(){
    V v = new V();
    // ...
    return;
}


// 发生了逃逸,对象返回到了方法外面
// 如果不想发生逃逸,代码可以改写成test3
public static StringBuffer test2(String s1, String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}
public static String test3(String s1, String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

// 引用成员变量的值,发生逃逸
public void test4(){
    V v = getInstance();
    // getInstance().xxx() 同样发生逃逸
}

// 为成员变量赋值,发生逃逸
public void test5(){
    this.obj = new xxx();
}

参数设置:

  • 在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。如果使用的是较早的版本,可以通过:
    • -XX: +DoEscapeAnalysis:显式开启逃逸分析
    • XX: +PrintEscapeAnalysis:查看逃逸分析的筛选结果。

逃逸分析:代码优化

使用逃逸分析,编译器可以对代码做如下优化:

一、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

二、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

代码优化之栈上分配

  • JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
  • 常见的栈上分配的场景:给成员变量赋值、方法返回值、实例引用传递。

代码优化之同步省略(锁消除)

  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
    9 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被到其他线程占有。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

代码优化之标量替换

  • 标量(Scalar):指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量
  • 相对的,那些还可以分解的数据叫做聚合量(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
  • 在JIT阶段,如果经过逃逸分析,如果没有发生逃逸的话,那么经过JIT优化,就会把这个对象拆解成若干个其中的成员变量来代替。这个过程就是标量替换。
public static void main (string [] args) {
    alloc() ;
}

private static void alloc() {
    Point point =new Point(1,2) ;
    system.out.println ( "point.x=" + point.x + "point.y=" + point.y) ;
}

class Point {
    private int x;
    private int y;
}
  • 以上代码,经过标量替换后,就会变成:
private static void alloc({
    int x = 1;
    int y = 2;
    system.out.println ( "point.x=" + point.x + "point.y=" + point.y) ;
}

可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。

那么标量替换有什么好处呢?

就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。

标量替换为栈上分配提供了很好的基础。

标量替换参数设置:

-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。

逃逸分析技术并不成熟

  • 关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。
  • 其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程
  • 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
  • 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段
  • 注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
  • 目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上

你可能感兴趣的:(JVM)