# 详细垃圾回收机制

### **概述**

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收集器


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tuonioooo-notebook.gitbook.io/performance-optimization/jvmyou-hua-pian/xiang-xi-la-ji-hui-shou-ji-zhi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
