目录
JVM内存分配策略
1.优先分配到Eden
过程描述
当Eden分配内存大小不足时,会向有内存的区域借,Survivor如果不足,则向老年代借,即内存担保(将先创建的对象移动到老年代,新对象继续创建到Eden中)
如何打印GC信息?
配置:-verbosegc -XX:+PrintGCDetails
JVM设置
限制内存区域大小:
-Xms20M :设定程序启动时占用内存大小
-Xmx20M :设定程序运行期间最大可占用的内存大小
-Xmn10M :设定年轻代大小
-Xss128k:设置每个线程的堆栈大小
-XX:SurvivorRatio=8 :限制Eden区域占新生代比为0.8
2.大对象直接分配到老年代
指定多大对象范围
-XX:PretenureSizeThreshold=6M
原因
避免Eden gc时频繁的移动对象
3.长期存活的对象分配到老年代
设置多久为长期
-XX:MaxTenuringThreshold=15 (对象被复制的次数)
4.空间分配担保
(Eden内存不够,老年代借给内存,前提是老年代内存足够)
开启空间担保
-XX:+HandlePromotionFailure
关闭空间担保
-XX:-HandlePromotionFailure
栈上分配
优点:
变量分配根据方法的开始和完成进行处理,不需要GC去释放。
逃逸分析
(可以使对象分配到栈上)
目标
分析对象的作用域
当分析对象只在方法内使用(局部变量),则分配到栈上。
逃逸状态
1、全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
- 对象是一个静态变量
- 对象是一个已经发生逃逸的对象
- 对象作为当前方法的返回值
2、参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
3、没有逃逸
即方法中的对象没有发生逃逸。
逃逸分析优化
针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化。
1) 锁消除
我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
锁消除的 JVM 参数如下:
- 开启锁消除:-XX:+EliminateLocks
- 关闭锁消除:-XX:-EliminateLocks
锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
2) 标量替换
首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。
对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
标量替换的 JVM 参数如下:
- 开启标量替换:-XX:+EliminateAllocations
- 关闭标量替换:-XX:-EliminateAllocations
- 显示标量替换详情:-XX:+PrintEliminateAllocations
标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。
3) 栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。