深入理解 JVM 垃圾回收原理(上)

2016/10/13 技术

JVM 内存管理介绍

描述垃圾回收器如何实现内存管理,如何选择回收算法,如何设置回收区块的大小。同时介绍内存自动管理的概念,其中着重会讲解基于分代的内存管理机制,再详细描述下最常见的 4 种回收算法。

由于文章太长,分两上、下两篇文章来分别说明,这一篇是上篇

垃圾回收概念

一个成熟的垃圾回收器的主要作用包括

  • 分配内存空间
  • 保证存活的对象不被误杀、不被清理出内存
  • 释放已失效的对象内存空间

还存在引用的对象我们认为是存活的对象。 没有引用的对象我们称之为垃圾。 去发现那些垃圾的进程就是垃圾回收器。

注意 垃圾回收器可以解决很多的内存问题,但是并不能解决所有的内存问题。比如说你自动可以写一段代码去无限创建对象然后再引用他们,直到内存被耗尽。同时垃圾回收器是一个非常复杂并且消耗资源的进程。垃圾回收器最大的挑战就是如何避免产生磁盘碎片和减少延时。但是作为开发人员可以不用关心这些底层的内存问题,专注编码即可,选择相信 GC 是最明智的选择。但是有一个典型的例外场景,就是在使用 堆外缓存 的情况下,自己需要负责回收内存空间。

设计权衡

任何的设计都不是完美的,所以我们需要做出一些选择以取得在特定的某些场景下的最优解。

串行 vs 并行

串行回收是指在一个时间上只做一件事。比如说有多个 CPU 可用的时候,但是只有一个进程在执行回收。与此相反并行回收是把回收任务分解成若干小的回收任务,然后在多个 CPU 上同时执行回收。可以看出来并行回收的速度会快很多,但是并行回收会增加额外的复杂性,同时也会引入更多的空间碎片。

并发 vs 独占(Stop-the-world)

在独占(Stop-the-world)垃圾回收执行的时候,应用程序是完全处于挂起状态的。而并发回收的时候应用程序是可以同时运行的。并发回收虽然快,但是处理起来复杂,同时也需要更多的 heap 空间和其他资源。对于一个典型的并发回收器来讲,他的大多数的任务都是并发执行的,但是也会有一些小的任务是需要独占执行的。

压缩 vs 不压缩 vs 拷贝

当一个垃圾回收器可以区分内存里哪些对象是存活的,哪些是垃圾对象后,他就可以压缩内存,把存活的对象放到一起。压缩后就可以在原来的地方快速分配新的对象,然后用一个指针指向下一块内存空间。 与压缩的回收算法相反,非压缩回收算法在回收对象后不会移动存活的对象。这样做的好处就是垃圾回收会非常快,那么缺点就是会产生潜在的空间碎片。一般情况下非压缩的回收算法在分配内存空间的时候会更加的麻烦,因为他需要去遍历所有的碎片找到一块满足要求的空间。 第三个回收算法是复制回收算法,会把存活的对象拷贝到另一个的内存区域中,类似于一个双缓冲的设计。这种算法的优点是原来的内存空间是空白的空间,有利于下次分配对象,缺点是复制对象需要额外的时间开销,同是还需要一块额外的内存空间,存在一定的空间浪费。

性能指标

吞吐量

在系统运行的一段时间内,没有花费在垃圾回收上的时间与总的运行时间的百分比。这个值越大越好,代码吞吐量越高。

垃圾回收耗时

与吞吐量相反,花在垃圾回收上面的时间

暂停时间

垃圾回收执行的时候系统停止处理的时间

回收频率

垃圾回收发生的频率,相对于应用运行时间而言

分代回收

基于分代回收的原理是因为对象本身是具有生命周期的,然后把内存分成年轻代和老年代两个不同的空间,不同年龄大小的对象放在不同的内存空间里,对于不同的内存空间采用不同的回收算法。这样做最大的好处就是不同的算法可以针对不同的空间对算法进行固定特征进行优化。 基于如下两点

  • 大多数对象不会被引用太久,也就是在未成年就夭折了。
  • 很少有老年对象去用年轻对象。

年轻代垃圾回收是很频繁的并且很快效率也高,因为年轻代内存空间通常很小并且大多数的对象存活的时间很短。

年轻代的对象经过几轮的垃圾回收还存活下来后,年龄逐次变大,当增大到一定程度,那这个对象就会上升到老年代。所以老年代的空间是增长的很慢的,因此老年代的回收就不需要那么的频繁,但是可能会消费更长的回收时间,如下图 gen_collect.png

年轻代的垃圾回收算法需要更快的执行速度,因为年轻代的回收非常频繁。另一方面,老年代的空间增长相对较慢,所以老年代的回收算法更看重空间利用率

在实现分代回收时,通常会把内存空间分成三代

  • 一个年轻代
  • 一个老年代
  • 一个持久代

大多数的对象首先是分配在年轻代,而老年代会持有经过几轮回收后仍然存活下来的对象,同时大对象会直接在老年代分配。持久代持有类字节码和方法等其他一些原数据。 年轻代由一个『伊甸园』 Eden 和两个『存活区』 Survivor space 组成,如下图。大多数对象会首先在 Eden 区分配,而存活区会持有那些经过一轮垃圾回收仍然活下来的对象,同时又『不够老』而被提升进老年代。在任何时候,存活区都会持有这样的对象,而另一个存活区在下次回收前都会是空置状态。 gen_collect.png

回收算法

垃圾回收类型

当年轻代空间满后,年轻代的回收就会开始执行,有时也会称年轻代的回收为 Minor collection。当老年代或者永久代空间满后 Full GC 就开始执行,有时我们也把 Full GC 叫做 Major collection。有时老年代太满了,当年轻代的对象需要升级到老年代时,老年代空间不足,也会触发一次 Full GC。在这种情况下,年轻代的回收就不需要执行了,因为老年代的回收会去回收整个堆空间,包括年轻代、老年代和持久代,但是 CMS 回收算法除外,因为 CMS 是一个特殊的情况,他不会去回收年轻代的空间。

快速分配对象空间

在多线程的应用里,对象空间的分配必须要保证『多线程安全』,如果说利用『全局锁』去确保正确性的话,这会成功系统瓶颈大大降低性能。相反我们应该支使用『Thread-Local Allocation Buffers』(TLABs) 线程本地分配缓存的技术实现,这会给每个线程分配一个缓存空间,从而大大提升性能。

串行回收算法

serial_ygc.png

PS

由于篇幅太长,详细的算法说明放在下篇

打赏作者一杯

渣渣程序狗

文章内容导航