前言
- 何为垃圾
- 如何发现垃圾?
- 如何回收垃圾?
- 堆内存逻辑分区
- 常见垃圾回收器
垃圾(Garbage)
何为垃圾!?
概念
没有任何引用指向的单个或多个对象。
C/C++ 的处理
C:
- malloc 申请内存
- free 释放内存
C++:
- new 创建对象
- delete 销毁对象
java 的处理
- new 创建对象
- 自动回收对象
C/C++ vs java
C/C++:
- 手动处理垃圾,容易出错:
- 忘记回收 -> 内存泄露
- 多次回收 -> 非法访问
- 开发效率低,执行效率高
java:
- GC处理垃圾
- 开发效率高,执行效率低
垃圾定位方法
引用计数(Reference Count)
- 每多一个引用指向某个对象,就将该对象的引用计数值+1,反之亦然。
- 无法解决循环引用(即几个对象相互引用,但实际并没有外部引用指向这其中的任何一个对象,因此这几个对象都是垃圾,但是他们的引用计数都不为0)的问题。
根可达算法(Root Searching)
- 从根对象(GC Roots)开始搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则此对象垃圾。
哪些对象是根对象:
- 线程栈变量 JVM stacks
- JNI变量 native method stacks
- 常量池 runtime constant pool
- 静态变量 static reference in method area,Clazz
垃圾回收算法
标记清除(Mark-Sweep)
- 如下图所示,将标记为垃圾的内存直接清理。
特点:
- 垃圾的内存位置不连续,容易产生碎片;
- 整个过程需要两遍扫描,效率偏低;
- 存活对象较多的情况下,效率相对较高。
拷贝(Copying)
- 如下图所示,将内存分为两块区域,每次只用其中一块区域,垃圾回收时,将存活的对象拷贝到另一半区域。
特点:
- 没有碎片;
- 浪费空间;
- 移动赋值对象,指针所指的对象地址需要调整;
- 只需要扫描一次,效率较高;
- 存活对象较少的情况下,效率相对较高。
标记压缩(Mark-Compact)
- 如下图所示,垃圾回收时,将存活的对象搬运到连续的内存中。
特点:
- 没有碎片;
- 整个过程需要两遍扫描,并且由于需要搬运对象,指针所指的对象地址需要调整,效率偏低。
内存逻辑分区
背景
可以看到不同的垃圾回收算法也有其各自的适用场景,因此,HotSpot在实现垃圾回收时,将堆内存划分成了两个区域:新生代(Young)和老年代(Old)。
顾名思义,新生代里存放的都是最近产生的对象,而老年代里,存放的是经过多次垃圾回收后,仍然存活的对象。因此,两者相较而言,垃圾回收时,通常新生代里存活的对象较少,而老年代里存活的对象较多。
根据不同区域中对象的特点不同,采用不同的垃圾回收器。
后面随着软硬件的发展,出现了不分代的垃圾回收器(如:G1 逻辑上分代,物理上不分代;ZGC,Shenandoah 逻辑和物理上都不分代)
因此,分代模型仅针对除了 Epsilon、ZGC 和 Shenandoah 以外的垃圾回收器。
分代模型
- 新生代(young)
- 老年代(old)
- 永久代(Perm Generation)/ 元数据区(Metaspace)
备注:
- 堆(Heap) 分为 新生代 和 老年代。
- 永久代 和 元数据区 都是 方法区(Method Area) 的具体实现,在jdk1.7及之前的版本中,叫 永久代,在jdk1.8及之后的版本中叫 元数据区。
- Class对象在永久代/元数据区中。
- 永久代必须指定大小,运行过程中大小不会变,存满即报OOM,FGC不会清理Perm Generation;元数据区若不指定大小,则大小受限于物理内存,会触发FGC
- 字符串常量在jdk1.7及之前版本中,存放于 永久代,在jdk1.8及之后版本中,存放于 堆。
- GC概念:
- MinorGC/YGC:对年轻代进行垃圾回收称为 MinorGC 或 YGC。年轻代空间耗尽时触发。
- MajorGC/FullGC:对新生代和老年代都进行垃圾回收称为 MajorGC 或 FullGC。在老年代无法继续分配空间时触发。
新生代
新生代(Young)又划分为 Eden 区 和 2个 survivor 区(s0 和 s1)。
Eden(伊甸)区 存放新生成的对象;两个Survivor(幸存)区 交替存放每次YGC后,存活下来的对象。
GC过程:
- YGC后,大多数对象被回收,存活的对象进入 s0 区
- 再次YGC,eden 区和 s0 区活着的对象进入 s1 区
- 再次YGC,eden 区和 s1 区活着的对象进入 s0 区
- …
- 对象年龄足够,进入老年代。(CMS - 幸存6次后进入老年代;其他 - 幸存15次后进入老年代)
老年代
老年代(Old)中存储的通常是顽固不易被回收的对象。老年代满了会触发FullGC。
对象分配过程
动态年龄
分配担保
常见垃圾回收器
如上图所示,除了 Epsilon 是用于调试以外,垃圾回收器大致可分为两类:使用分代模型的和不使用分代模型的。
几个基础概念
STW:
- stop the world的缩写。
- 是指垃圾回收线程工作时,其他所有线程都暂停,等垃圾回收完毕,再恢复继续工作。
- STW期间,业务线程也停了,因此业务会表现出没有响应的现象。
- 目前,所有的垃圾回收都有STW
safe point:
基于分代模型的垃圾回收器
使用分代模型的垃圾回收器,通常成对出现,分别用于Young区和Old区:
Serial 和 Serial Old:
- 单线程回收
- 单CPU情况下,效率最高
- 适用于堆内存在 几十M左右 的情况
- Serial使用拷贝算法,Serial Old使用标记清除压缩算法
- Serial: a stop-the-world, copying collector which uses a single GC thread
- Serial Old: a stop-the-world, mark-sweep-compact collector that uses a single GC thread
Parallel Scavenge 和 Parallel Old:
- 多线程回收
- 适用于堆内存在 上百M 到 几个G 之间的情况
- 是jdk1.8的默认垃圾回收器
- Parallel Scavenge使用拷贝算法,Parallel Old使用标记压缩算法
- Parallel Scavenge: a stop-the-world, copying collector which uses multiple GC threads
- Parallel Old:a stop-the world, compacting collector that uses multiple GC threads
ParNew 和 CMS
- 适用于内存在 20 G 以下 的情况
- ParaNew:
- ParNew 就是为了配合 CMS 使用,基于 Parallel Scavenge 的增强版本
- a stop-the-world, copying collector which uses multiple GC threads.
- It differs from “Parallel Scavenge” in that it has enhancements that make it usable with CMS
- For Example, “ParNew” does the synchronization needed so that it can run during the concurrent phases of CMS
CMS
- Concurrent Mark Sweep 的缩写
- 在JDK1.4版本后期引入,是里程碑式的GC,开启了 并发回收 的先河,但是由于 CMS 问题较多,目前没有任何一个版本的JDK使用 CMS 作为默认垃圾回收器。
- 并发回收:
- 随着物理内存越来越大,传统垃圾回收器GC产生的STW时间已经长到无法忍受。因此,为了缩短STW时间,产生了并发垃圾回收器。
- 并发垃圾回收器工作(即GC)的大部分时间内,其他线程也是正常运行的。只在某些必要阶段,会把其他线程暂停,因此,大大缩短了STW时间。
工作阶段:
- 初始标记(initial mark):STW,标记根对象。
- 并发标记(concurrent mark):标记非垃圾对象。使用 三色标记 算法。
- 重新标记(remark):STW,修正并发标记阶段中,由于其他线程同时运行,而产生的错标和漏标。使用 Incremental Update 算法。
- 并发清理(concurrent sweep):清理垃圾。使用 标记清除 算法。
CMS 的问题:
碎片化(Memory Fragmentation):CMS默认使用标记清除算法,会产生内存碎片。
解决方法:
-XX:UseCMSCompactAtFullCollection
-XX:CMSFullGCBeforeCompaction=5
即进行了若干次(可配置,上例中是5)Full GC后,进行一次压缩算法。
浮动垃圾(Floating Garbage)
CMS并发回收过程中,产生的垃圾,称为浮动垃圾,浮动垃圾需要等到下一次GC时才会回收。
但是浮动垃圾会产生下面2种情况:
Concurrent Mode Failure:CMS和业务线程并发运行,在执行CMS的过程中有业务对象需要在老年代直接分配,例如大对象,但是老年代没有足够的空间来分配。
Promotion Failed:YGC后, Survivor空间容纳不了剩余对象,将要放入老年代,老年代有碎片或者不能容纳这些对象。
这两种情况,会进入STW,并降级使用Serial Old垃圾器进行FullGC,从而造成长时间的卡顿。
解决方法:+XX:CMSInitiatingOccupancyFraction=60
即适当调低老年代触发FullGC的阈值,以保证老年代有足够大的空间。
G1
- Garbage First 的缩写
- 逻辑上分代,物理上不分代
- 适用于堆内存在 100 G 以下 的情况
- 并发标记阶段使用 三色标记 算法;重新标记阶段使用 SATB (snapshot at the beginning) 算法。
不使用分代模型的垃圾回收器
ZGC
- 适用于堆内存在 16 T 以下 的情况
- 使用 颜色指针(Colored Pointers) + 读屏障(Load Barrier)
Shenandoah
- 使用 颜色指针(Colored Pointers) + 写屏障(Load Barrier)