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垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章

浅谈JVM垃圾回收器相关知识点

2021 最全JVM性能调优:垃圾回收+线程+类加载+子系统...(干货)

浅谈JVM中的垃圾回收

jvm基础--GC垃圾回收机制

「JVM基础」——垃圾回收基础(GC相关)

Jvm垃圾回收器(终结篇)