JVM——GC

Java 垃圾回收机制

在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被引用的对象,并将它们添加到要回收的集合中,进行回收。

GC 垃圾回收流程

JVM——GC

堆区是 gc 的主要区域,通常情况下分为两个区块新生代和老年代。新生代又分为 Eden 区和 Survivor 区,From survivor 和 To survivor 保存 gc 后幸存下的对象,默认情况下各自占比 8:1:1。

  1. 新创建对象时,优先分配在 Eden 区,如果 Eden 内存不足,那么会自动执行一个 Minor GC(Young GC)操作,将 Eden 区的无用内存空间进行清理,Minor GC 的清理范围只在 Eden 区,清理之后会继续判断 Eden 区的内存是否充足?如果充足,则将新对象直接在 Eden 区分配内存。

  2. 如果执行 Minor GC 之后 Eden 区的内存空间依然不足,就会执行 Survivor 区的判断,如果 Survivor 区有剩余空间,则将 Eden 区部分活跃对象保存在存活区,那么随后继续判断 Eden 园区的内存空间是否充足,如果充足则将新对象直接在 Eden 区进行空间分配。

  3. 此时如果 Survivor 区没有内存空间,则继续判断老年代。则将部分存活对象保存在老年代,而后 Survivor 区将有空余空间。

  4. 如果这个时候老年代也满了,那么这个时候将产生 Major GC(Full GC),那么这个时候将进行老年代的清理。

  5. 如果老年代执行 Full GC 之后,无法进行对象的保存,则会产生 OOM 异常,OutOfMemoryError 异常。

垃圾收集算法

其实所有垃圾回收算法所面临的问题是相同的——找出已经分配的,但是用户程序不可到达的内存块。

GC 最基础的算法有三种: 标记-清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

  1. 标记-清除算法,用在新生代,分为两个阶段,标记,清除,被标记的对象会在标记结束有被清除回收。但由于回收的对象是分散的,所以就会导致内存碎片化严重。

  2. 复制算法,用在 Survivor 区,将内存分为容量大小相等的两块,当发生 GC 之后,将 from 的对象,复制到 to 区,避免了内存碎片,但也造成空间浪费,使用率只有原来的一半。

  3. 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存。

  4. 分代收集算法:按照对象的特性区分年轻代,老年代,根据对象的特性选择对应的回收算法。年轻代,存活时间短选择复制算法,老年代选择标记清除或标记压缩

  5. 引用计数法:对于一个 对象 A,只要引用一次就加 1,引用失效减一,当计数为 0 时就失效

  6. 根搜索算法:判断是否可达:需要和根节点有依赖关系。如果没有和我的 GCroots 有关

标记-清除算法

JVM——GC

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点: 标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。

缺点: 效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。

复制算法

JVM——GC

为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。

优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

标记-压缩算法(标记-整理)

JVM——GC

标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

优点: 该算法不会像标记-清除算法那样产生大量的碎片空间。

缺点: 如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

JVM 有哪些垃圾回收器

下图展示了 7 种作用于不同分代的收集器

  • 新生代收集器:Serial、PraNew、Parallel、 Scavenge

  • 老年代收集器:Serial Old、Parallel Old、CMS

  • 整堆的收集器:G1

不同收集器之间的连线表示它们可以搭配使用。

JVM——GC

  • Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

  • ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;

  • Parallel Scavenge 收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC 线程时间),高吞吐量可以高效率的利用 CPU 时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

  • Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器的老年代版本;

  • Parallel Old 收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge 收集器的老年代版本;

  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。

  • G1(Garbage First)收集器 (标记-整理算法):Java 堆并行收集器,G1 收集器是 JDK1.7 提供的一个新收集器,G1 收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

young gc、old gc、full gc、mixed gc

其实 GC 分为两大类,分别是 Partial GC 和 Full GC。Partial GC 即部分收集,分为 young gc、old gc、mixed gc。

  • young gc(Minor GC):指的是单单收集年轻代的 GC。

  • old gc(Major GC):指的是单单收集老年代的 GC。

  • mixed gc:这个是 G1 收集器特有的,指的是收集整个年轻代和部分老年代的 GC。

  • Full GC 即整堆回收,指的是收取整个堆,包括年轻代、老年代,如果有永久代的话还包括永久代。

young gc(Minor GC) 触发条件

大致上可以认为在年轻代的 eden 快要被占满的时候会触发 young gc。

为什么要说大致上呢?因为有一些收集器的回收实现是在 full gc 前会让先执行以下 young gc。

eden 快满的触发因素有两个,一个是为对象分配内存不够,一个是为 TLAB(本地线程分配缓冲区) 分配内存不够。

Full GC 触发条件

1. 执行 System.gc()jmap -dump 等命令会触发 full gc。

System.gc() 只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存

2. 未指定老年代和新生代大小,堆伸缩 时会产生 Full GC,所以一定要配置-Xmx、-Xms

3. 老年代空间不足,常见场景比如大对象、大数组直接进入老年代、长期存活的对象进入老年代等。

为了避免因此引起的 Full GC,应当尽量不要创建过大的对象以及数组。
此外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。
还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
在执行 Full GC 后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space

4. JDK 1.7 及以前的(永久代)空间满。

永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。
如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError PermGen space
为了避免因此引起的 Full GC,可增大永久代大小或转为使用 CMS GC。

5. 空间分配担保失败

空间担保,下面两种情况是空间担保失败:
1、每次晋升的对象的平均大小 > 老年代剩余空间
2、Minor GC 后存活的对象超过了老年代剩余空间

注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状况,当出现这两种状况的时候就有可能会触发 Full GC。
promotion failed 是在进行 Minor GC 时候,survivor 放不下只能晋升老年代,而此时老年代也空间不足时发生的。
concurrent mode failure 是在进行 CMS GC 过程,此时有对象要放入老年代,而空间不足造成的,这种情况下会退化使用 Serial Old 收集器变成单线程的,此时是相当的慢的。

TLAB(Thread Local Allocation Buffer)本地线程分配缓存

一般而言生成对象需要向堆中的新生代申请内存空间,而堆又是全局共享的,像新生代内存又是规整的,是通过一个指针来划分的。新对象创建,指针就右移对象大小 size 即可,这叫指针加法

JVM——GC

如果多个线程都在分配对象,那么这个指针就会成为热点资源,需要互斥那分配的效率就低了。于是搞了个 TLAB,为一个线程分配的内存申请区域。这个区域只允许这一个线程申请分配对象,允许所有线程访问这块内存区域。

TLAB 的思想其实很简单,就是划一块区域给一个线程,这样每个线程只需要在自己的那亩地申请对象内存,当这块内存用完了之后再去申请。不过每次申请的大小不固定,会根据该线程启动到现在的历史信息来调整,比如这个线程一直在分配内存那么 TLAB 就大一些,如果这个线程基本上不会申请分配内存那 TLAB 就小一些。还有 TLAB 只能分配小对象,大的对象还是需要在共享的 eden 区分配。

所以总的来说 TLAB 是为了避免对象分配时的竞争而设计的。

这种思想其实很常见,比如分布式发号器,每次不会一个一个号的取,会取一批号,用完之后再去申请一批。

JVM——GC

上一篇:哪些元素可以当作为"GC Roots"?


下一篇:jvm虚拟机及创建对象流程