useparallelgc java_理解 Java GC

# 理解 Java GC

> 原文:[Understanding Java Garbage Collection](https://www.cubrid.org/blog/understanding-java-garbage-collection) by Sangmin Lee ON 05/31/2017

> 翻译:[码代码的陈同学](https://chenyongjun.vip/)

> 参考:[JVM源码分析之线程局部缓存TLAB](https://www.jianshu.com/p/cd85098cca39) 、[[jvm的card table数据结构](https://segmentfault.com/a/1190000004682407)

了解Java GC如何工作有什么好处呢?满足程序员的好奇心应该是一个很好的解释。不过,理解GC如何工作也可以帮助你开发出更好的应用。

这是我个人的主观观点,但我相信一个精通GC的人往往会是一个更好的Java开发人员。如果你对GC处理过程感兴趣,这意味着你具备一定规模应用的开发经验;如果你慎重考虑过如何选择正确的GC算法,意味着你已经完全理解你所开发的应用的特点。当然,这不是判断一个优秀程序员的通用标准。然而,如果我说理解GC是成为一名优秀Java开发人员的要求时,估计没什么人会反对。

本文是《成为Java GC专家》的首篇,我将对GC进行介绍,在下一篇文章中,我将聊聊 GC状态分析和GC优化的例子。

在学习GC之前你需要先了解一个术语—**stop-the-world**。无论使用何种GC算法,stop-the-world都会发生。stop-the-world意味着JVM为了执行GC将会暂停运行中的应用,除GC线程外,应用中的其他所有线程将暂停工作,只有在GC结束之后,这些被中断的任务才会重新恢复。GC优化通常指的是减少stop-the-world的持续时间。

## 分代垃圾收集

在Java程序中不能显示的分配和释放内存。有人会将相关对象设置NULL或者调用`System.gc()`来显示释放内存,将对象设置为NULL倒是没什么,但是调用`System.gc()`将极大的影响系统性能,必须禁止使用这种方式。

在Java中,开发人员不需要在代码中手动释放内存,垃圾收集器会找到不必要的(垃圾)对象并且清理掉它们。如果满足下面两种假设(也许叫先决条件更为合适),垃圾收集器将会启动:

* 大部分对象很快将变得 *不可达(unreachable)*

* 只有少量从旧对象中指向新对象的引用

这种假设被称为 **弱代假设(Weak generational hypothesis)**。所以为了保留这些假设的优势,在HotSpot虚拟机中从物理上将内存分成了两块:**年轻代**和**老年代**。

**年轻代**:大部分新创建的对象都分配在这里,由于大多数对象很快将变得不可达,因此许多新对象都分配在年轻代中。当新对象从年轻代消失时,我们说发生了 **年轻代GC(Minor GC)**。

**老年代**:那些没有变得**不可达**而且从年轻代幸存下来将被移动到老年代。老年代空间比年轻代大很多,由于它的空间比较大,GC发生的频率也要比年轻代低。当对象从老年代消失时,我们会说发生了 **老年代GC(Major GC) 或 full GC**。

![](https://imgcdn.chenyongjun.vip/2018/06/18/4562c1cb6b044618bc1e8ab8c82fff53.png)

> 图 1: GC区域和数据流

上图中的**永久代(Permanent Generation)** 也叫**方法区(Method Area)**,用于存储class等信息。因此,这个区域不是用于继续存储老年代中幸存的对象。这个区域也可能发生GC,这里发生的GC依然叫 **Major GC**。

有人可能会想:**如果老年代中的对象引用年轻代中的对象时怎么办?**

为了处理这种情况,老年代中有一个叫做 **卡片表(Card Table)** ,它是一个大小为512字节的块,所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当发生Minor GC时,只会检索卡片表以确定对象是否属于GC范围,而不是检查老年代中所有对象的引用。卡片表通过 **写屏障(write barrier)** 来管理,**写屏障** 是一种可以提高 Minor GC性能的设备,尽管这种方式会产生一点开销,但是减少了GC的总体时间。

![](https://imgcdn.chenyongjun.vip/2018/06/19/0c5254601940488dbefec19c9d632612.png)

> 图2:卡片表结构

> 译者注:作者对于卡片表说的其实并不清楚,有兴趣的可以参考 [jvm的card table数据结构](https://blog.csdn.net/lihuifeng/article/details/51681089)

## 年轻代的组成

> 译者注:堆内存的更多知识可以阅读译者以前的译文 [JVM内存管理](https://chenyongjun.vip/articles/40)、[高性能应用之理解JVM堆内存](https://chenyongjun.vip/articles/41)

为了了解GC,让我们先学习下年轻代,年轻代是新对象呆的第一个地方。

年轻代分成三部分:

* 一个 Eden(伊甸园) 区域

* 二个 Survivor(幸存者) 区域

下面是每个区域的内存分配顺序:

* 大多数新创建的对象都分配在Eden区

* 在经历一次Eden区的GC之后,幸存的对象将被移动到 Survivor区中的一个区(这个区已经有其他幸存对象存在)

* 一旦一个Survivor区满了,幸存的对象将被移动到另一个Survivor区,然后,这个满的Survivor区将被设置重新可用的状态

* 那些经历上述步骤一定次数的对象将被移动到老年代

通过这些步骤你可以发现,Survivor区中的一个区必须保持空白。如果Survivor的两个区都存在数据或者没有任何一个区存在数据,这将是 **系统出现问题的标志**。

数据通过GC进入老年代的过程如下图所示:

![](https://imgcdn.chenyongjun.vip/2018/06/19/e7bf7027132e4744b67e7822619e0a27.png)

> 图3:GC执行前后对比

值得注意的是,在HotSpot VM中使用了两种技术来快速分配内存。一是 **指针碰撞(Bump-the-pointer)**,二是 **TLABs(Thread-Local Allocation Buffers)即线程局部缓存**。

* **Bump-the-pointer**:指针碰撞技术会跟踪在**Eden**中分配的最后一个对象,这个对象会分配在**Eden**区的top位置。当继续创建新对象时,首先会检查对象的大小是否可以在**Eden**中进行分配,如果新对象大小合适,将被放到Eden的top位置。因此,当新对象创建时,只需要检查最后一个添加到Eden区的对象,这种方式极大提高了内存分配速度。然而,如果考虑多线程环境,将又是另一番故事。为了保证多线程环境下对象内存分配的线程安全,无可避免将会产生锁问题,并且由于锁竞争将导致性能降低。

* **TLABs**:TLABs是HotSpot VM 指针碰撞中多线程环境问题的解决方案。这种方案允许线程在Eden中拥有一小部分自己的专属空间,每个线程只能访问属于他们自己的TLAB区域,这样指针碰撞技术在分配内存时就不用考虑锁的问题。

现在我们已经大致了解年轻代的GC。你不需要记住上面的两种技术,即使不知道也不会让你蹲号子,但是需要记住新对象是在Eden区中进行分配,在Survivor区存活时间足够长的对象将被从Survivor区移动到老年代。

## 老年代GC

一般在老年代满了之后才会执行GC活动,执行过程因GC类型而异,所以如果你了解不同的GC类型,理解起来将会更加容易。

对于JDK 7,有5种GC类型:

1. Serial GC

2. Parallel GC

3. Parallel Old GC(Parallel Compacting GC)

4. Concurrent Mark & Sweep GC (or "CMS")

5. Garbage First(G1) GC

一定不能在服务器上使用 Serial GC,这种GC类型是为了单核桌面计算机而创建,使用这种GC类型将会显著降低应用性能。

现在让我们了解一下各种GC类型。

### Serial GC (-XX:+UseSerialGC)

年轻代中使用上述我们提过的GC类型,老年代中的GC使用了一种 **标记-清除-压缩(mark-sweep-compact)** 算法。

1. 这种算法的第一步是标记老年代中存活的对象(标记)

2. 然后,从头开始检查堆中对象,并且只留下存活的对象(清除)

3. 最后,顺序的填满堆内存,并且将堆分成两部分:一部分有对象,另一部分没有对象(压缩)

Serial GC适合于小内存以及少核数场景。

### Parallel GC (-XX:+UseParallelGC)

![](https://imgcdn.chenyongjun.vip/2018/06/19/d5af69861f0849d69076aa4c8493367d.png)

> 图4: Serial GC与Parallel GC的区别

通过上图,你可以很容易看到 Serial GC 和 Parallel GC的区别。Serial GC使用单线程进行GC,Parallel GC使用多线程进行GC,因此Parallel GC速度更快。当内存很大,同时操作系统核数足够时,这种GC方式将十分有用,这种方式也叫做 **throughput GC**。

### Parallel Old GC(-XX:+UseParallelOldGC)

JDK5开始支持Parallel Old GC,与Parallel GC相比,唯一的区别是对于老年代使用的GC算法不同。算法分成三个步骤:**标记-整理-压缩(mark-summary-compaction)**,**summary**步骤会分别标记已经发生过GC的区域中的幸存对象,因此与 **mark-sweep-compact**中**sweep** 步骤不同的是,**summary** 会经历一些更为复杂的步骤。

### CMS GC(-XX:+UseConcMarkSweepGC)

![](https://imgcdn.chenyongjun.vip/2018/06/19/2906534d96c443b6b4703aa3af513312.png)

> 图5: Serial GC 与 CMS GC

从上图可以看出,CMS GC比上述提到的任何一种GC类型都复杂。**Initial Mark** 步骤非常简单,只需要检查离classloader比较近的对象,因此停顿时间非常短。在**Concurrent Mark** 步骤,那些被刚确认过的Survivor对象所引用的对象将被跟踪和检查,第一二步的区别是在 **Concurrent Mark** 的同时其他线程也可以正常工作。在**Remark** 步骤,那些在**Concurrent Mark**期间新创建的对象或者不再被引用的对象将被检查。最后,在**Concurrent Sweep** 步骤中,将执行垃圾收集,垃圾收集时其他线程可以同时正常工作。由于采取这种GC方式,GC的停顿时间将非常短,因此CMS GC也叫做低延迟GC,常常用于那些对响应时间非常苛刻的应用。

这种GC方式虽然能缩短 stop-the-world 的时间,但也有以下不足之处:

* 与其它GC类型相比,会耗费更多的内存和CPU

* 默认不支持Compaction步骤

在使用这种GC类型前,你需要慎重考虑。同时,如果由于内存碎片太多不得不执行**Compaction** 步骤,那么stop-the-world所消耗的时间将比起他任何GC类型都多,你需要检查 compaction 任务多久执行一次。

### G1 GC

最后,让我们了解下G1 GC。

![](https://imgcdn.chenyongjun.vip/2018/06/19/90361c989305459db3c8eec827619c82.png)

> 图6: G1 GC的布局

如果你想了解G1 GC,你首先要做的是把年轻代、老年代概念统统先抛诸脑后。从上图可知,每个对象被分配在一个格子中,随后执行GC。当一个格子区域满了,新对象将被分配到起他格子区域,然后再执行一次GC。这种GC类型中,你将看不到年轻代、老年代之间的对象移动过程。G1 GC是创建出来用于替换CMS GC的,因为从长远来看,CMS GC导致了很多问题和抱怨。

G1 GC最大的优势是**性能**,它比我们上面讨论过的任何GC算法都快。但是在JDK6中,只是尝尝鲜而且只在测试中使用,在JDK1.7中G1 GC被正式包含进来。

> 译者注:剩下的几段没有意义的话不再翻译。

## 译者后记

本计划翻译这个作者的好几篇文章,在查阅资料时,无意中查到国内已经有人翻译了这个系列。待自己翻译完成后,和这位译者的进行对比,发现人家翻译的是真好。

目前自己的翻译目标是尽量准确翻译原文,希望以后在确保翻译准确的前提下,文字能够更加精炼一些。

以下是另一位译者翻译的系列:

1. [成为JavaGC专家(1)—深入浅出Java垃圾回收机制](http://www.importnew.com/1993.html)

2. [成为JavaGC专家(2)—如何监控Java垃圾回收机制](http://www.importnew.com/2057.html)

3. [成为Java GC专家(3)—如何优化Java垃圾回收机制](http://www.importnew.com/3146.html)

4. [成为Java GC专家(4)—Apache的MaxClients参数详解及其在Tomcat执行FullGC时的影响](http://www.importnew.com/3151.html)

扫码或搜索 codercyj 关注微信公众号, 结伴学习, 一起努力

wechat.jpg;jsessionid=6D5D400E9573DB4E72A3608490D5D94E

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值