JVM学习之垃圾回收机制

jvm的垃圾回收算法,除了我们熟悉的引用计数判断对象是否活着之外,其他还有那些有意思的东西呢?

总是听到的年轻代年老代又是啥?

传说中的YoungGC(MinorGC) 和 FullGC的时机是什么,又干了些啥?

I. 对象存活判断

垃圾回收,回收的都是那些不在使用的对象(也就是没有存活的对象),因此怎么判断对象是否存活,就显得比较重要了

对这个映像最深刻的就是引用计数方式,一个对象被使用了,计数就+1;不用了,技术就-1;当计数为0的时候,就表示对象没人用了,简单粗暴,然而实际的情况中,大都不用这个方式,因为无法解决对象相互循环引用的问题

目前更多的是采用gc root可达性分析,简单来讲就是从一个根节点往下走,走的轨迹上所有的对象,都表示是存活的;也就是说,所有游离在这个之外的对象,都是需要回收的

那么什么是GC ROOT呢 ?

  • 虚拟机栈内引用的对象
  • 方法区静态属性引用的对象
  • 方法区常量引用的对象
  • 本地方法栈中JNI引用的对象

II. 垃圾回收算法

回收,主要指的是将堆和运行时方法区内没有存活的对象干掉;而通常我们所说的垃圾回收,则主要针对的就是堆内的回收

1. 标记-清除算法

简单理解:根据可达性扫一遍,有用的对象打个标记;剩下来一次大清理,将没有标记的都ko掉

说明

看书和博文时,常感觉标记,是将需要回收的对象标记出来,但仔细想了下,从实现成本来讲,根据可达性分析对象是否存活,顺带的直接将存活的打个标记,比将所有没存活的上面打上标记要来的简单,而且这也能算是标记出需要回收的对象

缺点

缺点很明显,会出现大量的碎片空间

2. 复制算法

将存储空间一分为二,每次回收就是将这一边的存活对象搬移到另一边

缺点

  • 空间少了一半
  • 对于存活时间比较久的对象,需要频繁的来回搬迁

3. 标记-压缩算法(或标记-整理算法)

为了节省空间,这个的策略是将所有存活的对象,往某一边界进行复制,等复制完毕之后,将辩解之外的对象都ko掉

4. 分代收集算法

分代收集,实际来说就是综合其他算法的优良特性,结合实际应用场景来处理

  • 将存活时间久,占用空间大的对象,放在老年代
  • 其他的对象可以放在年轻代

也就是说:

老年代中,基本上是老而弥坚的对象,更加适合标记-整理算法,移到一边之后,由于经常活着,也就避免了频繁的复制了

新生代中,常是一些朝生夕死的对象,可能用了一次就可以ko,因此可以采用复制算法,标记-清除也是ok的

分代的主要思想就是根据不同的情况,给予不同的策略

III. 简单说下垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

1. Serial收集

串行收集器,也就是程序跑一会,停下,让我们的回收线程(只有一个)来实现垃圾回收

新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩

2. ParNew收集

上面的多线程版本

新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

3. Parallel收集

类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;

新生代复制算法、老年代标记-压缩

4. Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供

5. CMS收集器

这个是比较常用的,有必要好好了解下

Concurrent Mark Sweep 收集器,是一种以获取最短回收停顿时间为目标的收集器,核心就是标记-清除算法

a 步骤

  • 初始标记:标记GC Roots能直接关联到的对象,速度很快,会暂停
  • 并发标记:进行 GC Roots Tracing的过程
  • 重新标记:为了修正并发标记期间,因为程序继续运作导致标记变动的那一部分对象的标记记录,一般会长于初始标记时间,远小于并发标记的时间
  • 并发清除:并发干掉被回收的问题

初始标记和重新标记的时候,会暂停服务;后面两个则是并发修改

b. 优缺点

优点:并发收集、低停顿

缺点:产生大量空间碎片、并发阶段会降低吞吐量

img

6. G1收集器

传说中是最先进的收集器。。。。

用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合

a. 步骤

  • 标记阶段:初始标记,会停顿,触发minorgc
  • Root Region Scanning: 程序运行过程中会回收survivor区(存活到老年代),这一过程必须在minorGC之前完成
  • 并发标记:若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例);并发执行,可能被minorgc打断
  • 再标记:再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行),停顿
  • 复制-整理:并发干掉死亡对象,G1将回收区域的存活对象拷贝到新区域

IV. GC分析

这个日志主要针对的是CMS收集器的分析,因为我接触的应用,服务器上就是选择的这个…

看一张神奇的图

gc日志图

内存分配和回收策略

a. 对象优先在Eden分配

大多数场景下,对象在新生代Eden区分配,当Eden去没有足够的空间进行分配时,虚拟机发起一次 Minor GC

  • 新生代MinorGC : 发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性是,所以一般MinorGC非常频繁,一般回收速度也很快

  • 老年代MajorGC(FullGC) : 发生在老年代的GC,通常就伴随至少一次的MinorGC(非绝对),一般较慢,是MinorGC的十倍以上

b. 大对象直接进入老年代

需要大量连续内存空间的Java对象,通常是数组,同构 -XX:PretenuresizeThreshold 参数,来设置大对象的阀值,超过这个阀值的直接分配在年老代,避免在Eden区及两个Survivor区之间发生大量的内存复制

c. 长期存活的对象将进入老年代

既然虚拟机采用分代收集的思想来管理内存,在回收时,就必须能识别哪些对象应放在新生代,那些对象应放在老年代中

每个对象都有个Age的计数器,对象在Eden出生并经过第一次MinorGC后仍存在,且可以被Survivor容纳的话,会被移动到Survivor空间中,并设置Age为1

对象在Survivor区没多经过一次MinorGC,则age+1

当age超过阀值(默认15),就会晋升到老年代

阀值可以通过 -XX:MaxTenuringThreshold来设置

d. 动态对象年龄判定

如果在Survivor空间中相同年龄所有对象的大小的总和,大于Survivor空间的一半,则年龄大于或等于该年龄的对象就可以进入老年代,无需等Age达到阈值

e. 空间分配担保

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,则Minor GC可以确保总是安全的;

否则,查看 HandlePromotionFailure参数,是否允许担保失败

若允许,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则尝试MinorGC

否则进行FullGC

V. 小结

1. 怎么判断对象是否存活

两种方式,引用计数和可达性分析

引用计数: 循环依赖问题,没啥用

可达性:从gc roots出发,可达的都是存活的

2. 几种回收算法对比

算法 简述 缺点
标记-清除 标记对象,统一清楚可回收对象 大量碎片
复制算法 内存一分为二,将存活的移动到另一边 存活久的对象,频繁复制;空间变小
标记-整理 存活对象往一边界拷贝,边界外的都干掉 对于生命周期特别短的不太合适
分代 年轻代 + 年老代,不同代选用不同算法 -

3. CMS和G1阶段对比

cms主要区分四个步骤:

  • 标记:停顿
  • 并发标记
  • 重新标记:停顿,重新处理并发过程中新标记的对象
  • 并发清除:并发回收

g1,从结构上而言,划分为一个个独立区域(region),采用标记-整理算法,避免碎皮空间

4. 简述内存分配和回收

基于CMS进行说明

  • 优先分配edge区(不够则触发gc)
  • 大对象,分配在old区
  • 存活时间久的塞入old区
  • 动态时间判断(某个age对象总和大于Survivor一半,则塞入old区)
  • 分配担保(进入old区,但是old区空间不够的策略,决定是否触发gc)

VI. 其他

参考

个人博客: Z+|blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode