JVM基础干货浅谈JVM垃圾回收
Posted 在路上的德尔菲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础干货浅谈JVM垃圾回收相关的知识,希望对你有一定的参考价值。
哪些对象会进入老年代?
减少GC次数和减少GC频率
JVM调优主要目的是减少STW时间 —> 转换为减少Full GC次数 —> 减少老年代中对象,使老年代空间不要满 —> 哪些对象会进入老年代,思考能否不让他们进入老年代,在年轻代youngGC阶段回收掉
大对象(超过设定阈值):所谓的大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。
长期存活的对象:对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认是15岁), 就将会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
动态年龄判定:为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
Eden存活数量过多:如果一次Young GC后存活对象太多无法进入Survivor区,此时直接进入老年代.
总的来说,对象很大或者对象一直在被使用会进入老年代,一直存活的常用数据的cache
一般Web系统中存在以下几种类型对象
a、有一部分对象几乎一直活着,这些可能是常用数据的cache之类的
b、有一部分对象创建出来没多久之后就没用了,这些很可能会响应一个请求时创建出来的临时对象
c、最后可能还有一些中间的对象,创建出来之后不会马上就死,但也不会一直活着
针对young gen和old gen的比例问题,需要根据具体的系统来确定,没有固定的模式参考。
假如我们把young gen设置的大一些,大到每次young GC的时候里面的多数b对象最好已经死了。因为如果young gen太小,每次满了就触发一次young GC,那young GC就会很频繁,或许很多临时b对象正好还在被是使用(还没死),那这样的话young GC的收集效率就会比较低,为了避免这样的情况,就需要把young gen设大一些。
关于oldgen,它至少要足以装下所有长期存活的a对象,同时还有留出一定的空间来容纳young GC没能清理掉的临时b对象。
关于c对象,它们或许会经历多次young GC之后仍然存活,于是晋升到old gen,但晋升上去之后或许很快就又死掉了,这样的情况最好能不让晋升到old gen,此时可以想办法增加tenuring threshold,而这样又会存在另一个问题,young GC的暂停时间会因此增长。所以这里需要做一定的tradeoff,没有固定的模式。
GC时间的正常范围是需要根据业务情况而定的,需要根据请求访问量,响应时间要求等各个方面来综合考虑,比如说每5秒1次50ms的YoungGC,和每50秒一次的500ms的FullGC(其他YoungGC忽略不计),这两种GC情况哪种好,很难得出一个绝对的结论,比如说应用是一个在线聊天室,每5秒有一次50ms的GC导致响应变慢了,用户对50ms的感知不一定有那么敏感,他会觉得这个聊天室响应一直都是那么快,但是如果我们是50秒一次500ms的FullGC,用户会很明显会觉得,每隔一段时间就会出现一次卡顿,用户体验会降低。而在别的场景下,说不定又是每50秒一次500ms的FullGC是更优的选择。所以gc时间的正常必须结合自身的特性来的。
对象合理晋升(不要出现提前晋升现象,动态年龄判定情况);
消除内存泄漏;
不要出现因元空间满(java8)或者Perm满(<java8)而造成的fullgc。
触发FullGC的时机
只有那些有活跃引用的对象,或者已经经过压缩整理的对象会在老年代中继续保持,其余对象都会被回收
1、调用System.gc()
XX:+ DisableExplicitGC 禁止RMI调用System.gc()
2、老年代空间不足
老年代空间出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space
1 大对象直接进入老年代引起,由-XX:PretenureSizeThreshold参数定义
2 Minor GC时,经历过多次Minor GC仍存在的对象进入老年代。上面提过,由-XX:MaxTenuringThreashold参数定义
3 Minor GC时,动态对象年龄判定机制会将对象提前转移老年代。年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域 * -XX:TargetSurvivorRatio的时候,从这个年龄段往上的年龄的对象进入老年代
4 Minor GC时,Eden和From Space区向To Space区复制时,大于To Space区可用内存,会直接把对象转移到老年代
调优时应尽量做到让对象在Minor GC阶段被回收,让对象在新生代多存活一段时间及不要创建过大的对象及数组。
-XX:CMSInitiatingOccupancyFraction=75 老年代占比超过75的时候触发CMS收集
3、永久代(元空间)空间不足
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永久代,Permanet Generation(PermGen)中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
4、CMS GC时出现 fail
触发YoungGC时机
总的来说是Eden区空间不足时,就会触发YoungGC
另外新生代的四种垃圾回收器均采用复制算法,执行期间都会STW
1、正常流程
新对象会先尝试在栈上分配,如果不行则尝试在TLAB分配,否则再看是否满足大对象条件在老年代,最后考虑在Eden区申请空间,如果Eden区没有合适的空间,即Eden区域满的时候触发一次YGC
YGC时对Eden和From Survivor区的存活对象转移到To Survivor空间中,过程中如果相同年龄对象数量大于总数量的一半或To Survivor空间不足则直接进入老年代
执行后在Eden和From Survivor区剩余对象均为垃圾
具体细节:
1、查找GC Roots,将其引用的对象拷贝到To Survivor区,其中可以作为GC Root的对象有以下几种,必须是一组必须活跃的引用,否则就可能会漏扫描应该存活的对象,导致GC错误回收这些被漏扫的活对象
Tracing GC的根本思路就是,给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的对象被判定为存活
Java方法的local变量或参数(本地方法栈),虚拟机栈中引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
-
JNI方法的local变量或参数,本地方法栈中引用的对象 JNI Local
-
方法区类中中静态属性(static)、常量引用(static final)的对象
-
被Synchronized锁持有的对象 Monitor Used
-
存在跨代引用的对象
-
和GC Root处于同一CardTable 的对象 (CardTable为一种用空间换时间的思想,由于跨代引用的对象占比不到1%,如果中有一个对象存在跨代引用,可以用1字节标示该卡页为dirty)
由于方法区、虚拟机栈和本地方法栈中保存了类中和方法中定义的变量的引用,既然是自己定义的变量,所以肯定是有用的。
2、递归遍历第1步的对象,拷贝其引用的对象到To Survivor区
To Survivor区为了维护内存区域,使用了双指针, saved_mark_word 记录当前遍历对象的位置,top为当前可分配内存的位置,两指针之间的对象为已拷贝但未扫描的对象,直到saved_mark_word指针追上top指针,说明To Survivor区所有对象都已经遍历完成
3、CMS回收器
为了减少重新标记阶段耗时,也有可能触发触发一次YGC
如何减少长时间的GC停顿(时长维度)
1、高速率创建对象
如果程序创建对象速率很高,GC率也将会很高,高GC率即FullGC发生频繁,会增加GC的停顿时间
scalpel等Java分析器得到 1)创建了哪些对象 2)创建这些对象的速率的多少 3)内存中占用的空间是多少 4)谁在创建他们
2、年轻代空间不足
当年轻代空间不足时,对象会过早提升到老年代,增加年轻代的大小可能减少长时间的GC停顿,以下参数
-Xmn:指定年轻代的大小
-XX:NewRatio 指定年轻代相对于老年代的大小比例, -XX:NewRatio = 2,年轻代的大小将是整个堆的1/3
-XX:SurvivorRatio=10 扩大survivor区的大小,避免幸存对象由于空间问题很快被移动到老年代
3、设置一个特别大的堆
GC停顿时间取决于堆的大小,如果增大堆的空间,停顿的频率会变得更少,但是停顿的持续时间会变长
4、选择GC算法
可使用G1算法,具有自动调节的能力
-XX:MaxGCPauseMillis = 200 设置GC最大停顿时间为200ms目标,JVM尽力实现
5、调整GC线程数
6、进程使用了Swap
JVM怎么判断对象可以回收
- 对象没有引用,对象被赋值为空不一定被标记为可回收对象,因为可能会发生逃逸。标记为不可达的对象只是出于缓刑状态,至少需要进行两次标记才能确定该对象被回收,可达性分析为第一次标记,接着会对第一次标记后的对象经过一轮筛选,筛选的条件为此对象是否有必要执行finalize(),在finalize()中没有重新与引用链建立关系的将被第二次标记,第二次标记的真的会被回收
- 作用域发生未捕获异常
- 程序在作用域正常执行完毕
- 程序执行了System.exit()
- 程序发生意外终止(被杀进程)
Eden区存入一个大对象会发生什么
如果大于-XX:PretenureSizeThreshold 令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。
空间担保策略
发生时机:发生在YGC之前,检查老年代最大可用连续空间是否大于新生代所有对象的总空间
路径
1、如果大于新生代总空间,此次进行YGC
2、如果不大于
2.1关闭HandlePromotionFailure值,则进行Full GC
2.2开启HandlePromotionFailure值,检查老年代最大用用空间是否大于历次晋升到老年代的对象之和
2.2.1大于之和,尝试进行一次YGC,此次有风险,失败后会重新发起一次Full GC
2.2.2小于之和,则直接进行一次Full GC
以上是关于JVM基础干货浅谈JVM垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章