四、逃逸分析(Escape Analysis)
逃逸分析是 Java 虚拟机的优化技术,逃逸分析的原理是:分析对动态作用域, 当一个对象在方法区里面被定义后,它可能被外部方法引用,例如作为调用参数传递给其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如直接赋值给可以在其他线程访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸、到线程逃逸,称为对象由低到高的不同逃逸程度。
栈上分配(Stack Allocations)
很显然test方法中的user对象被返回了,这个对象的作用域范围不确定,test2方法中的user对象我们可以确定当方法结束这个对象就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内存一起被回收掉JM对于这种情况可以通过开启逃逸分析参数(-XX:+Doescapeanalysis 屎来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配), Jdk7之后默认开启逃逸分析, 如果要关闭使用参数(-XX:+Doescapeanalysis)
标量替换(Scalar Replacement)
1. 标量替换
通过逃逸分析确定该对象不会被外部访问, 并且对象可以被进一步分解时,JVM 不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替, 这些代替的成员变量在栈帧或寄存器上分配空间, 这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+Eliminateallocations), JDK7 之后默认开启。
2. 标量与聚合量
标量即不可被进一步分解的量, 而 Java 的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等), 标量的对立就是可以被进一步分解的量, 而这种量称之为聚合量。而在 JAVA 中对象就是可以被进一步分解的聚合量。
3. 案例演示
// 未被优化前的代码 public int test(int x) { int xx = x + 2; Point p = new Point(xx, 43); return p.getX(); } class Point { private int x, private int y, public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return this.x; } }
首先进行内联优化
// 内联优化 public int test(int x) { int xx = x + 2; Point p = point_memory_alloc(); p.x = xx; p.y = 42 return p.x; }
然后进行逃逸分析,发现在 test 犯法中 Point 对象实例不会发生任何逃逸,这样就可以对它进行标量替换优化,把内部的 x 和 y 直接置换出来分解为 test() 方法内的局部变量,从而避免 Point 对象实例被实际创建。优化后的代码
// 标量替换 public int test(int x) { int xx = x + 2; int px = xx; int py = 42 return px; } // 最终优化 public int test(int x) { return x + 2; }
五、内存分配回收策略
对象优先分配在 Eden 区
大多数情况下,对象在新生代 Eden 区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。 HotSpot虚拟机提供了-XX:+PrintGCDetails
这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
测试代码:
/** * -XX:+PrintGCDetails */ public class GCTest { public static void main(String[] args) { byte[] allcation2 = new byte[8000 * 1024]; } }
输出结果
Heap PSYoungGen total 38400K, used 11353K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 34% used [0x0000000795580000,0x00000007960966f8,0x0000000797600000) from space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) to space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) ParOldGen total 87552K, used 0K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 0% used [0x0000000740000000,0x0000000740000000,0x0000000745580000) Metaspace used 3017K, capacity 4556K, committed 4864K, reserved 1056768K class space used 319K, capacity 392K, committed 512K, reserved 1048576K
我们可以通过内存空间的分布可以看出 allcation2
是被分配到 eden 区中的。
大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象(比如:字符串、数组),JVM 参数 -XX:PretenureSizeThreshold
参数可以设置大对象的大小,指定大于该设置值的对象直接在老年代分配,不会进入年轻代,这个参数只有在 Serial 和 ParNew 两个收集器下有效。
比如设置:JVM 参数:-XX:PretenureSizeThreshold=1000000(单位直接)-XX:+UseSerialGC, 在执行上面的第一个程序就会发现大对象直接进入了老年代。
这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
长期存活的对象将进入老年代
HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
对象通常在Eden区里诞生,如果经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
设置。
动态对象年龄判断
为了能更好地适应不同程序的内存状况,HotSpot 虚拟机并不是永远要求对象的年龄必须达到 -XX:MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 -XX:MaxTenuringThreshold 中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure
参数的设置值是否允许担保失败(Handle Promotion Failure);
如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,
如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;
如果小于,或者 -XX:HandlePromotionFailure
设置不允许冒险,那这时就要改为进行一次 Full GC。
空间担保过程
六、对象内存回收
引用计数法
在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
这个方法实现很简单,效率很高, 但是目前的主流虚拟机并没有悬着这个算法来管理内存,其中主要的运用就是它很难解决对象之间相互循环应用的问题,对象之间的相互引用问题,如下面的代码所示对象objA和objB都有字段instance,赋值令 objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
public class ReferenceCountingGC { public Object instance = null; public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; } }
可达性分析算法
将 “GC Roots” 作为对象的起点,从这里开始向下搜索引用对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
GC Root 根节点:线程栈的本地变量、静态变量,本地方法栈变量等等。