JVM GC(Garbage Collection)
垃圾收集算法
程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭。
栈帧随着方法的开始而入栈,随着方法的结束而出栈。
这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
对于 Java 堆和方法区,只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
一、判定对象是否存活
若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。
引用计数法
在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1。若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。
引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。
但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为多线程环境下,引用计数变更要进行同步操作,性能较低(循环引用问题可通过 Recycler 算法解决)。
可达性分析法(图论)
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。GC Roots 指:
- Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
GC Roots 不包括堆中对象所引用的对象(避免循环引用问题)。

二、引用的种类
https://blog.csdn.net/f641385712/article/details/94298768
不同的引用类型,主要体现的是对象不同的可达性状态和垃圾收集的影响。
// 强引用,永远不会回收被引用的对象,会产生内存泄漏 String str = "abc"; // 软引用,内存不足时被回收 SoftReference<String> sr = new SoftReference<>("abc"); // 弱引用,无论内存是否充足,都会被回收 WeakReference<String> wr = new WeakReference<>("abc"); // 虚引用,和没有引用一样,用来感知对象被 GC 的时机(存入队列中),或用来堆外内存释放 ReferenceQueue rq = new ReferenceQueue<>(); PhantomReference<String> pr = new PhantomReference<>("abc", rq); // 建议 JVM 进行 Full GC,而非一定 // 只让虚拟机自己去管理内存,可以通过 -XX:+ DisableExplicitGC 来禁止调用 System.gc() System.gc();
三、回收内存
堆区
对于可达性分析中不可达的对象,也并不是没有存活的可能。如果在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。但 finalize() 方法只会被系统自动调用一次,下一次回收,它的 finalize() 方法不会被再次执行。
JVM 会判断此对象是否有必要执行 finalize() 方法,如果对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么对象基本上就真的被回收了。
如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。
方法区
方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
- 废弃常量(常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉)
- 无用的类
判定一个类是否是“无用的类”,条件较为苛刻:
- 该类的所有对象都已经被清除
- 加载该类的 ClassLoader 已经被回收
- 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。
四、常见垃圾收集算法
标记-清除算法(Mark-Sweep,操作清理对象)
判断哪些数据需要清除,并对它们进行标记,然后清除被标记的数据。
Mark-Sweep 之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-整理算法(操作清理对象)
将废弃对象做上标记,然后将未标记的对象移到一边,最后清空另一边区域即可。
这样就不用浪费内存空间,也避免的碎片化问题。适用于存活率高的情况。

复制算法(操作存活对象)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。
不会有内存碎片的问题,但内存缩小为原来的一半,浪费空间。适用于存活率低的情况。

分代收集算法(HotSpot 虚拟机 GC 采用分代收集算法)
根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,根据年代的特点来选择最佳的收集算法。
- 新生代:复制算法
- 老年代:标记-整理算法
堆大小=新生代+老年代(默认分别占堆空间为1/3、2/3),新生代又被分为Eden、from survivor、to survivor(默认8:1:1)
这样划分是为了更好的管理堆内存中的对象,方便 GC 算法来进行垃圾回收。

对象的分配通常在 Eden 中(大对象(需要大量连续内存空间的 Java 对象,如很长的字符串或数据)直接进入老年代,-XX:PretenureSizeThreshold)。
当 Eden 区满后,会触发 Minor GC,把 Eden 区和 from survivor 区中存活的对象进行转移,其中到达年龄(经过多次Minor GC)的会被放入老年代,未到达年龄的放入 to survivor 区。
然后清空 Eden 区和 from survivor 区,交换 from survivor 与 to survivor 的名字。
若存活对象大于 to survivor 区容量,则会被直接放入老年代。若打开了自适应(-XX:+AdaptiveSizePolicy),GC会自动重新调整新生代大小。
若老年代满了,则触发 Full GC。

Minor GC vs Major GC/Full GC:
- Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
- Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。
在 JVM 规范中,Major GC 和 Full GC 都没有一个正式的定义,所以有人也简单地认为 Major GC 清理老年代,而 Full GC 清理整个内存堆。
垃圾收集器(HotSpot)
垃圾收集器为垃圾收集算法的具体实现,是执行垃圾收集算法的,是守护线程。
HotSpot 虚拟机采用分代收集(JVM 规范并未对堆区进行划分),将堆分为年轻代和老年代,垃圾收集器也按照这样区分。不过已有通用垃圾收集器出现。
一、新生代垃圾收集器
Serial 垃圾收集器(复制算法,单线程)
在垃圾收集过程中停止一切用户线程(Stop The World),适合客户端使用。

ParNew 垃圾收集器(复制算法,多线程)
Serial 的多线程版本,但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial,清理过程依然需要 Stop The World。

Parallel Scavenge 垃圾收集器(复制算法,多线程)
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:
- Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
- ParNew:追求降低用户停顿时间,适合交互式应用。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
二、老年代垃圾收集器
Serial Old 垃圾收集器(标记-整理,单线程)
Serial 的老年代版本,区别:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。
Parallel Old 垃圾收集器(标记-整理,多线程)
Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
CMS 垃圾收集器(标记-清除,在 JDK9 被标记废弃)
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

三、G1 通用垃圾收集器
https://www.oracle.com/technical-resources/articles/java/g1gc.html
面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
四、相关命令
# 服务端与客户端查看
java -version

# 默认 GC 查看,显示出JVM初始化完毕后所有跟最初的默认值不同的参数及它们的值 java -XX:+PrintCommandLineFlags -version # java -XX:+PrintFlagsFinal -version | find ":"

| -XX:UseSerialGC | Client 模式的默认值,使用 Serial+Serial Old 收集器组合进行垃圾收集 |
| -XX:UseParNewGC | 使用 ParNew+Serial Old 收集器组合进行垃圾收集 |
| -XX:UseConcMarkSweepGC | 使用 ParNew+CMS+Serial Old 收集器组合进行垃圾收集。Serial Old 作为 CMS 收集器出现 Concurrent Mode Failure 的备用垃圾收集器 |
| -XX:UseParallelGC | Server 模式的默认值,使用 Parallel Scavenge+Serial Old 收集器组合进行垃圾收集 |
| -XX:UseParallelOldGC | 使用 Parallel Scavenge+Parallel Old 收集器组合进行垃圾收集 |
五、收集器组合情况

JDK8 默认方案:Parallel Scavenge + Parallel Old
低延迟要求:G1(堆内存 >6GB 时效果显著)
超大堆内存:考虑 ZGC(需升级到 JDK11+)
https://www.javacodegeeks.com/2015/03/minor-gc-vs-major-gc-vs-full-gc.html
https://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
Java Garbage Collection Basics:https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
Garbage Collection Tuning Guide:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning
Memory Management in the Java HotSpot Virtual Machine:https://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf
https://stackoverflow.com/questions/10400274/jvm-garbage-collection-algorithm
https://cn.bing.com/search?q=%E5%9E%83%E5%9C%BE%20site%3Atech.meituan.com


浙公网安备 33010602011771号