> For the complete documentation index, see [llms.txt](https://tuonioooo-notebook.gitbook.io/performance-optimization/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tuonioooo-notebook.gitbook.io/performance-optimization/jvmyou-hua-pian/xiang-xi-la-ji-hui-shou-ji-zhi.md).

# 详细垃圾回收机制

### **概述**

gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc，也没有规定gc如何工作。不过常用的jvm都有gc，而且大多数gc都使用类似的算法管理内存和执行收集操作。

在充分理解了垃圾收集算法和执行过程后，才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。比如，实时应用程序主要是为了避免垃圾收集中断，而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和jvm支持的垃圾收集算法，便可以进行优化配置垃圾收集器。

垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。gc首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。

### **JVM将内存划分为**

1）、New（新生代）：

> 年轻代用来存放JVM刚分配的Java对象

2）、Tenured（年老代）：

> 年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代

3）、永久代（Perm）

> 永久代存放Class、Method元信息，其大小跟项目的规模、类、方法的量有关，一般设置为128M就足够，设置原则是预留30%的空间

### **JVM分别对新生代和旧生代采用不同的垃圾回收机制**

### 1）**新生代的GC：**

新生代通常存活时间较短，因此基于Copying算法来进行回收，所谓Copying算法就是扫描出存活的对象，并复制到一块新的完全未使用的空间中，对应于新生代，就是在Eden和From Space或To Space之间copy。新生代采用空闲指针的方式来控制GC触发，指针保持最后一个分配的对象在新生代区间的位置，当有新的对象要分配内存时，用于检查空间是否足够，不够就触发GC。当连续分配对象时，对象会逐渐从eden到survivor，最后到旧生代，用java visualVM来查看，能明显观察到新生代满了后，会把对象转移到旧生代，然后清空继续装载，当旧生代也满了后，就会报OutOfMemoryError的异常，如下图所示：

![](/files/-Ltdv9bpYaCgQn-uXs4a)

> * Eden用来存放JVM刚分配的对象
> * Survivor1、Survivro2：两个Survivor空间一样大，当Eden中的对象经过垃圾回收没有被回收掉时，会在两个Survivor之间来回Copy，当满足某个条件，比如Copy次数，就会被Copy到Tenured。显然，Survivor只是增加了对象在年轻代中的逗留时间，增加了被垃圾回收的可能性。

在执行机制上JVM提供了串行GC（Serial GC）、并行回收GC（Parallel Scavenge）和并行GC（ParNew）

* **串行GC**

在整个扫描和复制过程采用单线程的方式来进行，适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上，是client级别默认的GC方式，可以通过-XX:+UseSerialGC来强制指定

* **并行收集器（吞吐量优先）**

在整个扫描和复制过程采用多线程的方式来进行，适用于多CPU、对暂停时间要求较短的应用上，是server级别默认采用的GC方式，可用-XX:+UseParallelGC来强制指定，用-XX:ParallelGCThreads=4来指定线程数

* **并发收集器（响应时间优先）**

与旧生代的并发GC配合使用

### 2）年老代**的GC：**

旧生代与新生代不同，对象存活的时间比较长，比较稳定，因此采用标记（Mark）算法来进行回收，所谓标记就是扫描出存活的对象，然后再进行回收未被标记的对象，回收后对用空出的空间要么进行合并，要么标记出来便于下次进行分配，总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行GC（Serial MSC）、并行GC（parallel MSC）和并发GC（CMS），具体算法细节还有待进一步深入研究。

以上各种GC机制是需要组合使用的，指定方式由下表所示：

| 指定方式                                     | 新生代GC方式 | 旧生代GC方式                                                                     |
| ---------------------------------------- | ------- | --------------------------------------------------------------------------- |
| -XX:+UseSerialGC                         | 串行GC    | 串行GC                                                                        |
| -XX:+UseParallelGC                       | 并行回收GC  | 并行GC                                                                        |
| -XX:+UseConeMarkSweepGC                  | 并行GC    | 并发GC                                                                        |
| -XX:+UseParNewGC                         | 并行GC    | 串行GC                                                                        |
| -XX:+UseParallelOldGC                    | 并行回收GC  | 并行GC                                                                        |
| -XX:+ UseConeMarkSweepGC-XX:+UseParNewGC | 串行GC    | 并发GC                                                                        |
| 不支持的组合                                   |         | 1、-XX:+UseParNewGC -XX:+UseParallelOldGC2、-XX:+UseParNewGC -XX:+UseSerialGC |

* **JVM命令行参数**

无论是客户端应用还是服务器端应用，一旦系统运行缓慢并且垃圾回收所占时间过长，你就会希望通过调整堆大小来改善这一点。不过，为了不影响其他也跑在同一个系统中的应用，不应该将堆大小设置的过大。

GC调优是很重要的。找到最佳的分代堆空间是一个迭代的过程\[3,10,12]。这里我们假定你已经为你的应用找到了最佳堆大小。那么你可以采用下面的JVM命令来进行设置：

| **GC命令行选项**                    | **描述**                                                                                                                                                                                                                                                     |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -Xms                           | 设置Java堆大小的初始值/最小值。例如：-Xms512m (请注意这里没有”=”).                                                                                                                                                                                                                |
| -Xmx                           | 设置Java堆大小的最大值                                                                                                                                                                                                                                              |
| -Xmn                           | 设置年轻代对空间的初始值，请注意，年老代堆空间大小是依赖于年轻代堆空间大小的。在整个堆内存大小确定的情况下，增大年轻代将会减小年老代，反之亦然。此值关系到JVM垃圾回收，对系统性能影响较大，官方推荐配置为整个堆大小的3/8                                                                                                                                            |
| -XX:NewSize=1024m              | 设置年轻代初始值为1024M。                                                                                                                                                                                                                                            |
| -XX:MaxNewSize=1024m           | 设置年轻代最大值为1024M                                                                                                                                                                                                                                             |
| -XX:NewRatio=4                 | 设置年轻代（包括1个Eden和2个Survivor区）与年老代的比值。表示年轻代比年老代为1:4。                                                                                                                                                                                                          |
| -XX:PermSize=\<n>\[g\|m\|k]    | 设置持久代堆空间的初始值和最小值                                                                                                                                                                                                                                           |
| -XX:MaxPermSize=\<n>\[g\|m\|k] | 设置持久代堆空间的最大值                                                                                                                                                                                                                                               |
| -Xss128k                       | 设置较小的线程栈以支持创建更多的线程，支持海量访问，并提升系统性能。                                                                                                                                                                                                                         |
| -XX:SurvivorRatio=6            | 设置年轻代中Eden区与Survivor区的比值。系统默认是8，根据经验设置为6，则2个Survivor区与1个Eden区的比值为2:6，一个Survivor区占整个年轻代的1/8。                                                                                                                                                                |
| -XX:ParallelGCThreads=8        | 设置并行收集器的线程数，即同时8个线程一起进行垃圾回收。此值一般配置为与CPU数目相等。                                                                                                                                                                                                               |
| -XX:MaxTenuringThreshold=0     | 设置垃圾最大年龄（在年轻代的存活次数）。如果设置为0的话，则年轻代对象不经过Survivor区直接进入年老代。对于年老代比较多的应用，可以提高效率；如果将此值设置为一个较大值，则年轻代对象会在Survivor区进行多次复制，这样可以增加对象再年轻代的存活时间，增加在年轻代即被回收的概率。根据被海量访问的动态Web应用之特点，其内存要么被缓存起来以减少直接访问DB，要么被快速回收以支持高并发海量请求，因此其内存对象在年轻代存活多次意义不大，可以直接进入年老代，根据实际应用效果，在这里设置此值为0。 |
| -XX:+UseConcMarkSweepGC        | 设置年老代为并发收集。CMS（ConcMarkSweepGC）收集的目标是尽量减少应用的暂停时间，减少Full GC发生的几率，利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存，适用于应用中存在比较多的长生命周期对象的情况。                                                                                                                                       |

* **JVM命令行辅助参数**

| -XX:-CITime                        | 打印消耗在JIT编译的时间。                                      |
| ---------------------------------- | --------------------------------------------------- |
| -XX:ErrorFile=./hs\_err\_pid.log   | 保存错误日志或数据到指定文件中。                                    |
| -XX:HeapDumpPath=./java\_pid.hprof | 指定Dump堆内存时的路径。                                      |
| -XX:-HeapDumpOnOutOfMemoryError    | 当首次遭遇内存溢出时Dump出此时的堆内存。                              |
| -XX:OnError=";"                    | 出现致命ERROR后运行自定义命令。                                  |
| -XX:OnOutOfMemoryError=";"         | 当首次遭遇内存溢出时执行自定义命令。                                  |
| -XX:-PrintClassHistogram           | 按下 Ctrl+Break 后打印堆内存中类实例的柱状信息，同JDK的 jmap -histo 命令。 |
| -XX:-PrintConcurrentLocks          | 按下 Ctrl+Break 后打印线程栈中并发锁的相关信息，同JDK的 jstack -l 命令。   |
| -XX:-PrintCompilation              | 当一个方法被编译时打印相关信息。                                    |
| -XX:-PrintGC                       | 每次GC时打印相关信息。                                        |
| -XX:-PrintGCDetails                | 每次GC时打印详细信息。                                        |
| -XX:-PrintGCTimeStamps             | 打印每次GC的时间戳。                                         |
| -XX:-TraceClassLoading             | 跟踪类的加载信息。                                           |
| -XX:-TraceClassLoadingPreorder     | 跟踪被引用到的所有类的加载信息。                                    |
| -XX:-TraceClassResolution          | 跟踪常量池。                                              |
| -XX:-TraceClassUnloading           | 跟踪类的卸载信息。                                           |

* **关于参数名称等**

标准参数（-），所有JVM都必须支持这些参数的功能，而且向后兼容；例如：

* * **-client**

    ——设置JVM使用Client模式，特点是启动速度比较快，但运行时性能和内存管理效率不高，通常用于客户端应用程序或开发调试；在32位环境下直接运行Java程序默认启用该模式。
  * **-server**

    ——设置JVM使Server模式，特点是启动速度比较慢，但运行时性能和内存管理效率很高，适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。
* 非标准参数（-X），默认JVM实现这些参数的功能，但是并不保证所有JVM实现都满足，且不保证向后兼容；
* 非稳定参数（-XX），此类参数各个JVM实现会有所不同，将来可能会不被支持，需要慎重使用；
* **算法分析**

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法，但是任何一种垃圾收集算法一般要做2件基本的事情：（1）发现无用信息对象；（2）回收被无用对象占用的内存空间，使该空间可被程序再次使用。

大多数垃圾回收算法使用了根集(root set)这个概念；所谓根集就是正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量)，程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的，从根集可达的对象都是活动对象，它们不能作为垃圾被回收，这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件，应该被回收。下面介绍几个常用的算法。

1、 引用计数法(Reference Counting Collector)

引用计数法是唯一没有使用根集的垃圾回收的法，该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说，堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时，引用计数器置为1。当对象被赋给任意变量时，引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用)，引用计数器减1，一旦引用计数器为0，对象就满足了垃圾收集的条件。

基于引用计数器的垃圾收集器运行较快，不会长时间中断程序执行，适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销，因为每次对象赋给新的变量，计数器加1，而每次现有对象出了作用域生，计数器减1。

> 注意：这种算法的思路是如果某一个对象被别的对象引用，那么就把他们引用计数器加上1，这样当进行垃圾回收时如果判断该引用的数量为0，此时就代表没有进行任何对象对其进行引用，此时就进行回收

2、tracing算法(标记-清除算法)

tracing算法是为了解决引用计数法的问题而提出，它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描，识别出哪些对象可达，哪些对象不可达，并用某种方式标记可达对象，例如对每个可达对象设置一个或多个位。在扫描识别过程中，基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.

图例：

![](/files/-Ltdv9bsMkCd8TuFRTM6)

> 缺点：1.效率问题 2 .空间问题（标记清除后会产生大量不连续的碎片）

3、compacting（标记-整理）算法(Compacting Collector)

为了解决堆碎片问题，基于tracing的垃圾回收吸收了Compacting算法的思想，在清除的过程中，算法将所有的对象移到堆的一端，堆的另一端就变成了一个相邻的空闲内存区，收集器会对它移动的所有对象的所有引用进行更新，使得这些引用在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中，一般增加句柄和句柄表。

图例：

![](/files/-Ltdv9bukp-FQ7v3-hN7)

4、copying（复制）算法(Coping Collector)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面，程序从对象面为对象分配空间，当对象满了，基于coping算法的垃圾 收集就从根集中扫描活动对象，并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞)，这样空闲面变成了对象面，原来的对象面变成了空闲面，程序会在新的对象面中分配内存。

一种典型的基于coping算法的垃圾回收是stop-and-copy算法，它将堆分成对象面和空闲区域面，在对象面与空闲区域面的切换过程中，程序暂停执行。

图例：

![](/files/-Ltdv9bwcOwUzEEDtzS9)

5、generation（分代收集）算法(Generational Collector)

stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象，这增加了程序等待时间，这是coping算法低效的原因。在程序设计中有这样的规律：多数对象存在的时间比较短，少数的存在时间比较长。因此，generation算法将堆分成两个或多个，每个子堆作为对象的一代 (generation)。由于多数对象存在的时间比较短，随着程序丢弃不使用的对象，垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后，上次运行存活下来的对象移到下一最高代的子堆中，由于老一代的子堆不会经常被回收，因而节省了时间。

图例：

![](/files/-Ltdv9byWwN8PQ-FXu_x)

6、adaptive算法(Adaptive Collector)

在特定的情况下，一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况，并将选择适当算法的垃圾收集器。

**触发GC（Garbage Collector）的条件/垃圾回收动作何时执行?**

1、GC在优先级最低的线程中运行，一般在应用程序空闲即没有应用线程在运行时被调用。但下面的条件例外。

2、Java堆内存不足时，GC会被调用。当应用线程在运行，并在运行过程中创建新对象，若这时内存空间不足，JVM就会强制调用GC线程。若GC一次之后仍不能满足内存分配，JVM会再进行两次GC，若仍无法满足要求，则JVM将报“out of memory”的错误，Java应用将停止。

3、当年轻代内存满时，会引发一次普通GC，该GC仅回收年轻代。需要强调的时，年轻代满是指Eden代满，Survivor满不会引发GC。

4、当年老代满时会引发Full GC，Full GC将会同时回收年轻代、年老代。

5、当永久代满时也会引发Full GC，会导致Class、Method元信息的卸载。

**垃圾回收的两个重要方法**

1）、System.gc()方法

使用System.gc()可以不管JVM使用的是哪一种垃圾回收的算法，都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc可以查看Java使用的堆内存的情况，它的格式如下：java -verbosegc classfile 由于这种方法会影响系统性能，不推荐使用，所以不详诉。

2）、 finalize()方法

在JVM垃圾回收器收集一个对象之前，一般要求程序调用适当的方法释放资源，但在没有明确释放资源的情况下，Java提供了缺省机制来终止该对象心释放资源，这个方法就是finalize（）。它的原型为：protected void finalize() throws Throwable 在finalize()方法返回之后，对象消失，垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。

之所以要使用finalize()，是存在着垃圾回收器不能处理的特殊情况。例如：

1）由于在分配内存的时候可能采用了类似 C语言的做法，而非JAVA的通常new做法。这种情况主要发生在native method中，比如native method调用了C/C++方法malloc()函数系列来分配存储空间，但是除非调用free()函数，否则这些内存空间将不会得到释放，那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数，所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。

2）又或者打开的文件资源，这些资源不属于垃圾回收器的回收范围。

**减少GC开销的措施**

1）、不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。

2）、尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

3）、对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

4）、尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

5）、能用基本类型如Int,Long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

6）、尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

7）、分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

8）、调整新生代的大小到最合适

9）、减少使用全局变量和大对象；

10）、设置老年代的大小为最合适

11）、选择合适的GC收集器
