理解java垃圾回收机制有什么好处?
如果说了解垃圾回收机制有利于你写出更好的java应用。一般说来我认同这样的说法:

  1. 一个人了解了GC,往往是一个优秀的程序员转变的开始。
  2. 一个人精通了GC,往往会是一个更好的Java程序员。
  3. 如果你对GC感兴趣,那就意味着你有一定大规模应用开发的经验。
  4. 如果你已经仔细过考虑选择合适的GC算法,这意味着你完全理解你开发的应用程序的功能。
    总结一句:理解GC是成为一个伟大的Java开发人员的要求。

在了解GC之前需要明确一个词“stop-the-world”
无论你选择哪种GC算法,Stop-the-world都会发生。Stop-the-world 意味着JVM停止应用程序,而去进行垃圾回收。当stop-the-world发生时,除了进行垃圾回收的线程,其他所有线程都将停止运行。被中断的任务将在GC任务完成后恢复执行。GC调优往往意味着减少stop-the-world的时间。(这个我一直理解的有偏差,之前一直认为minor GC (YGC)是不会终止应用程序的,而major GC(full GC)会引起stop-zhe-world,这个后续需要求证一下)

如何判断对象实例可否被回收?
说道这个问题需要延伸出两个常用判断对象是否存活的算法:

  1. 引用计数算法:
    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的。
    引用计数算法实现简单,效率很高,微软的COM技术、ActionScript、Python等都使用了引用计数算法进行内存管理,但是引用计数算法对于对象之间相互循环引用问题难以解决,因此java并没有使用引用计数算法。

  2. 根搜索算法(可达性分析算法):
    通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的。
    如下图对象object5,object6,object7虽然互相有关系,但是没有GC roots可以达到他们。所以他们时可以被回收的对象。

主流的商用程序语言C#、java和Lisp都使用根搜素算法进行内存管理。

看来理解GC Roots是了解根搜索方法的重点,那么着重看下在java中哪些类型对象可以作为GC roots的对象:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b. 方法区中的类静态属性引用的对象。
c. 方法区中的常量引用的对象。
d. 本地方法栈中JNI本地方法的引用对象。

既然标注出了无用对象那么java里是用什么算法把这些无用对象回收的呢?
在Java中,由于开发人员没有在代码中显式删除内存,所以垃圾收集器会去发现不需要(垃圾)的对象,然后删除它们,释放内存。这款垃圾收集器是基于以下两个假设:
绝大多数对象在短时间内变得不可达。
只有少量年老对象引用年轻对象。
这个假说就是牛X哄哄的:弱代假说。为了发挥这一假设的优势,在java HotSpot虚拟机中,物理的将内存分为两个—年轻代(young generation)和老年代(old generation)。

(ps:年轻代我有时也叫新生代,另外说到此处需要引用另一个文章 java虚拟机的内部体系结构.note)
这里的的年轻代和年老代均属于堆内存,这里分别来说下这两个区域的内存管理。
年轻代:新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次“minor GC”。
老年代:没有变得不可达,存活下来的年轻代对象被复制到这里。这块内存区域一般大于年轻代。因为它更大的规模,GC发生的次数比在年轻代的少。对象从老年代消失时,我们说“major GC”(或“full GC”)发生了。

  永久代(permanent generation)也称为“方法区(method area)”,他存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为major GC。永久代(方法区)不属于堆内存。

对象回收算法:
分代收集算法:
根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把堆内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。(两种情况进入年老代,和这个很重要)

上图表述的就是java垃圾回收机制作用的几个重要区域,而这种分代回收的算法(我觉得更像是一种策略)的目的主要就是将不同的情况分而治之,针对每个区域进行不同策略的回收机制,下文将具体描述每个代的回收算法。

标记-清除算法:
最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法:
将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法的缺点显而易见,可使用的内存降为原来一半。
复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低。

标记-整理算法:
标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。在对象存活率高的情况下使用标记-整理算法效率会大大提高。

年轻代的组成部分和工作机理:
为了理解GC,我们学习一下年轻代,对象第一次创建发生在这块内存区域。年轻代分为3块。
Eden区
2个Survivor区(From、To)
年轻代总共有3块空间,其中2块为Survivor区。各个空间的执行顺序如下:
绝大多数新创建的对象分配在Eden区。
在Eden区发生一次GC后,存活的对象移到其中一个Survivor区。
一旦一个Survivor区已满,存活的对象移动到另外一个Survivor区。然后之前那个空间已满Survivor区将置为空,没有任何数据。
经过重复多次这样的步骤后依旧存活的对象将被移到老年代。
以上可以看到肯定有一个Survivor区域肯定是空的。如下图:

可以预见的,在新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为811,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。
新生代中98%情况下空白Survivor都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。

Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。
使用java虚拟机-Xmn参数可以指定新生代内存大小。

扩展阅读点—>指针碰撞(bump-the-pointer)、TLABs(线程本地分配缓冲)
在HotSpot虚拟机中,使用两种技术加快内存的分配。一个被称为“指针碰撞(bump-the-pointer)”,另外一个被称为“TLABs(线程本地分配缓冲)”。
指针碰撞技术跟踪分配给Eden区上最新的对象。该对象将位于Eden 区的顶部。如果之后有一个对象被创建,只需检查Eden区是否有足够大的空间存放该对象。如果空间够用,它将被放置在Eden区,存放在空间的顶部。因此,在创建新对象时,只需检查最后被添加对象,看是否还有更多的内存空间允许分配。然而,如果考虑多线程的环境,则是另外一种情况。为了实现多线程环境下,在Eden 区线程安全的去创建保存对象,那么必须加锁,因此性能会下降。在HotSpot虚拟机中TLABs能够解决这一问题。它允许每个线程在Eden区有自己的一小块私有空间。因为每一个线程只能访问自己的TLAB,所以在这个区域甚至可以使用无锁的指针碰撞技术进行内存分配。

年老代的组成部分和工作机理:
年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。
Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。
当新生代中无足够空间为对象创建分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代空间无法在扩展时,堆就会产生OutOfMemoryError异常。
java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。
扩展阅读点—>卡表(card table)
如果老年代的对象去引用了新生代的对象我们将怎么处理呢?难道每次新生代的GC时HotSpot都会去将老年代的对象都去扫描一遍吗?显然不是。
老年代中有一个被称为“卡表(card table)”的东西,它是一个512 byte大小的块。每当老年代的对象引用年轻代对象时,这种引用会被记录在这张表格中。当垃圾回收发生在年轻代时,只需对这张表进行搜索以确定是否需要进行垃圾回收,而不是检查老年代中的所有对象引用。这张表格用一个叫做“写闸(write barrier)”的东西进行管理。“写闸”是一种装置,对minor GC有更好性能。虽然因为这种机制,会产生一些时间性能开销,但降低了整体的GC时间。如下图:

当老年代数据满时,基本上会执行一次GC。执行程序根据不同GC类型而变化,所以如果你知道不同类型的垃圾收集器,会更容易理解垃圾回收过程。

在JDK7中,有5种垃圾收集器:
Serial收集器
Parallel收集器
Parallel Old收集器 (Parallel Compacting GC)收集器
Concurrent Mark & Sweep GC (or “CMS”)收集器
Garbage First (G1) 收集器
注:Serial[‘sɪərɪəl] adj. 连续的;连载的;分期偿还的
parallel [‘pærəlel] n. 平行线;对比
concurrent [kən’kʌr(ə)nt] adj. 并发的;一致的;同时发生的

Serial 收集器 (-XX:+UseSerialGC)

首先serial 收集器一定不能用于服务器端。这个收集器类型仅应用于单核CPU桌面电脑。使用serial收集器会显着降低应用程序的性能。serial收集器应用于小的存储器和少量的CPU。

Parallel收集器(-XX:+UseParallelGC)

你可以很容易看到Serial收集器 和 Parallel收集器的差异。serial收集器只使用一个线程来处理的GC,而parallel收集器使用多线程并行处理GC,因此更快。当有足够大的内存和大量芯数时,parallel收集器是有用的。它也被称为“吞吐量优先垃圾收集器。”

Parallel Old 垃圾收集器(-XX:+UseParallelOldGC)
Parallel Old收集器是自JDK 5开始支持的。相比于parallel收集器,他们的唯一区别就是在老年代所执行的GC算法的不同。它执行三个步骤:标记-汇总-压缩(mark – summary – compaction)。汇总步骤与清理的不同之处在于,其将依然幸存的对象分发到GC预先处理好的不同区域,算法相对清理来说略微复杂一点。

CMS垃圾收集器(-XX:+UseConcMarkSweepGC)

如你在上图看到的那样, CMS垃圾收集器比之前我解释的各种算法都要复杂很多。初始标记(initial mark) 比较简单。这一步骤只是查找距离类加载器最近的幸存对象。所以停顿时间非常短。之后的并发标记步骤,所有被幸存对象引用的对象会被确认是否已经被追踪检查。这一步的不同之处在于,在标记的过程中,其他的线程依然在执行。在重新标记步骤会修正那些在并发标记步骤中,因新增或者删除对象而导致变动的那部分标记记录。最后,在并发清除步骤,垃圾收集器执行。垃圾收集器进行垃圾收集时,其他线程的依旧在工作。一旦采取了这种GC类型,由于垃圾回收导致的停顿时间会极其短暂。CMS 收集器也被称为低延迟垃圾收集器。它经常被用在那些对于响应时间要求十分苛刻的应用上。

当然,这种GC类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:
它会比其他GC类型占用更多的内存和CPU
默认情况下不支持压缩步骤
在使用这个GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他任何GC类型都长,你需要考虑压缩任务的发生频率以及执行时间。

Garbage First (G1)

如果你想要理解G1收集器,首先你要忘记你所理解的新生代和老年代。正如你在上图所看到的,每个对象被分配到不同的网格中,随后执行垃圾回收。当一个区域填满之后,对象被转移到另一个区域,并再执行一次垃圾回收。在这种垃圾回收算法中,不再有从新生代移动到老年代的三部曲。这个类型的垃圾收集算法是为了替代CMS 收集器而被创建的,因为CMS 收集器在长时间持续运行时会产生很多问题。
G1最大的好处是他的性能,他比我们在上面讨论过的任何一种GC都要快。但是在JDK 6中,他还只是一个早期试用版本。在JDK7之后才由官方正式发布。就我个人看来,NHN在将JDK 7正式投入商用之前需要很长的一段测试期(至少一年)。因此你可能需要再等一段时间。并且,我也听过几次使用了JDK 6中的G1而导致Java虚拟机宕机的事件。请耐心的等待它更稳定吧。

永久代(方法区):

java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。
永久代也使用标记-整理算法进行垃圾回收,java虚拟机参数-XX:PermSize-XX:MaxPermSize可以设置永久代的初始大小和最大容量。

作者:张三  创建时间:2026-03-13 11:56
最后编辑:张三  更新时间:2026-03-13 12:01