学软件技术,读第一手资料,去官方网站:Java SE Specifications
若想看垃圾收集的简易版本的,参见:Java垃圾收集基础
《深入理解Java虚拟机》中原文:由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
简单的说,逃逸分析指的是分析变量能不能逃出它的作用域。标量替换和栈上分配都是基于逃逸分析去做的。
首先什么是标量,所谓的标量指的是不能进一步分解的量。像 Java 的基础数据类型(int、long等数值类型以及 reference 类型等)以及对象的地址引用都是标量,因为它们是没有办法继续分解的。与标量对应的是聚合量,聚合量指的是可以进一步分解的量,比如字符串就是一个聚合量,因为字符串是用字节数组实现的,可以分解。又比如我们自己定义的变量也都是聚合量。
那么什么是标量替换呢?根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变景来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。
1. Java对象分配流程
2. 栈上分配
2.1 本质:Java虚拟机提供的一项优化技术
2.2 基本思想: 将线程私有的对象打散分配在栈上
2.3 优点:
2.3.1 可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有效避免垃圾回收带来的负面影响
2.3.2 栈上分配速度快,提高系统性能
2.4 局限性: 栈空间小,对于大对象无法实现栈上分配
2.4 技术基础: 逃逸分析
2.4.1 逃逸分析的目的: 判断对象的作用域是否超出函数体[即:判断是否逃逸出函数体]
//user的作用域超出了函数setUser的范围,是逃逸对象 //当函数结束调用时,不会自行销毁user private User user; public void setUser(){ user = new User(); user.setId(1); user.setName("blueStarWei"); } //u只在函数内部生效,不是逃逸对象 //当函数调用结束,会自行销毁对象u public void createUser(){ User u = new User(); u.setId(2); u.setName("JVM"); }
2.5 栈上分配示例
package com.blueStarWei.templet; public class AllotOnStack { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { alloc(); } long end = System.currentTimeMillis(); System.out.println(end - start); } private static void alloc() { User user = new User(); user.setId(1); user.setName("blueStarWei"); } }
2.5.1 上述代码调用了1亿次alloc(),如果是分配到堆上,大概需要1.5GB的堆空间,如果堆空间小于该值,必然会触发GC。
2.5.2 使用如下参数运行,发现不会触发GC
-server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
2.5.3 使用如下参数(任意一行)运行,会发现触大量GC
//不使用逃逸分析 -server -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations //不使用标量替换 -server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:-EliminateAllocations
2.5.4 GC日志
[GC (Allocation Failure) 4095K->528K(15872K), 0.0025208 secs] [GC (Allocation Failure) 4624K->552K(15872K), 0.0012518 secs] [GC (Allocation Failure) 4648K->608K(15872K), 0.0009262 secs] ......(省略) 3718
2.5.4.1 GC日志解析
参数 | 作用 | 备注 |
GC | 用来区分是 Minor GC 还是 Full GC 的标志(Flag). | 这里的 |
Allocation Failure | 引起垃圾回收的原因. | 本次GC是因为年轻代中没有任何合适的区域能够存放需要分配的数据结构而触发的. |
4095K->528K | 在本次GC之前和之后的年轻代内存使用情况. | 本次GC前,年轻代使用空间4095K, GC后年轻代使用空间为528K |
(15872K) | 年轻代的总的大小 | |
0.0025208 secs | 本次GC使用时间(单位:秒) |
2.5.5 JVM参数解析
参数 | 作用 | 备注 |
-server | 使用server模式 | 只有在server模式下,才可以弃用逃逸分析 |
-Xmx15m | 设置最大堆空间为15m | 如果在堆上分配,必然触发大量GC |
-Xms15m | 设初始对空间为15m | |
-XX:+DoEscapeAnalysis | 启用逃逸分析 | 默认启用 |
-XX:-DoEscapeAnalysis | 关闭逃逸分析 | |
-XX:+PrintGC | 打印GC日志 | |
-XX:-UseTLAB | 关闭TLAB | TLAB(Thread Local Allocation Buffer) 线程本地分配缓存区 |
-XX:+EliminateAllocations | 启用标量替换,允许对象打散分配到栈上 | 默认启用 |
-XX:-EliminateAllocations | 关闭标量替换 |