对象被判定为垃圾的标准
对于java对象来讲,当没有被任何对象所引用时,该对象就是没用的。对于系统而言它就是垃圾,其占据的内存就要被释放,同时该对象也要被销毁。
判断对象是否为垃圾的算法
- 引用计数算法
- 可达性分析算法
引用计数算法
- 通过判断对象的引用数量来判断对象是否可以被回收;
- 每个对象实例都是一个引用计数器,被引用则 + 1,完成引用则 - 1;
- 任何引用计数为0的对象实例可以被当作垃圾收集;
引用计数算法通过判断对象的引用数量来决定对象是否可以被回收。在这种机制下,堆中的每一个对象实例都有一个引用计数器,当一个对象被创建时,若该对象实例分配给一个引用变量,该对象实例的引用变量则会被置为1,若该对象又被另一个对象所引用,则该对象的引用计数器继续+1。而当该对象的实例的某个引用超过了生命周期或者被置为一个新值时,该对象实例的引用计数器将会 -1,比如在某个方法中定义了一个引用变量,指向该对象实例,当方法结束的时,由于该引用变量是局部变量,存储在虚拟机栈上,方法结束后会被自动干掉,此时该实例变量的引用计数器便会 - 1。任何引用计数为 0 的对象实例都可以被当做垃圾收集。
采用此类算法的优势是可以快速的执行,因为我们只要过滤出引用计数为 0 的对象实例,将其内存回收即可,可以交织在程序运行中。由于垃圾回收的时候几乎不打断程序的执行,因此对程序需要不被长时间打断的实时环境有利 。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。如父对象有一个对子对象的引用,子对象又返回来引用父对象,这样引用计数永远不可能为 0。
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
主流的java垃圾回收机制用的是可达性分析算法来对垃圾进行标记。可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收的。可达性算法是从离散数学的图论引入的,程序把所有的引用关系看成一张图,通过一系列的名为 GC Root 的对象作为起始点,从这些起始点向下搜索,搜索所走过的路径被称做引用链。当一个对象从GC Root 开始没有与任何引用链相连,那么从图论上来说,即从GC Root 到该对象是不可达的,此时证明该对象是不可用的,它也就会被标记为垃圾。
如上图所示,垃圾回收器会对内存中的整个对象进行遍历,从GC 根 ,即GC Root 开始,到根对象引用的其他对象,回收器将访问到的所有对象标记为存活,存活的对象在图中被标记为蓝色,即 Object 1、Object 2、Object 3,均为可达对象。在所有标记阶段被完成之后,所有存活对象已被标记完毕,其他那些比如图中灰色部分 Object 4、Object 5 就是GC 根对象不可达的对象,也就意味着应用不会在用到他们,回收器将会在回收阶段清除它们。
可以作为 GC Root 的对象
- 虚拟机栈中引用的对象 : 比如java方法中new 了一个 Object 并赋值给一个局部变量,那么在该局部变量没有被销毁之前,new出来的Object 就会成为 GC Root。
- 方法区中的常量引用的对象 : 在类中定义了一个常量,而该常量保存的是某个对象的地址,那么被保存的对象也称为GC 的根对象,当别的对象引用到它的时候就会形成关系链。
- 方法区中的类静态属性引用的对象
- 本地方法栈中 JNI(Native方法) 的引用对象
- 活跃线程的引用对象 :java一切皆对象,因此活跃的线程也会成为 GC Root,只要线程还处于活跃状态,那么它引用的对象也被称为可达的。
引用
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1.强引用(StrongReference)
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用(SoftReference)
如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
垃圾收集算法
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
标记-清除算法(Mark and Sweep)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
该算法将回收分为标记和清除两个阶段,先从根节点进行扫描,对已经存活的对象进行标记,用的是可达性分析算法,标记完成后会对堆内存从头到尾进行线性遍历,如果发现某对象没有被标记为可达对象,就会将次对象占用的内存回收。并且将原来已标记为可达对象的标识给清除掉,以便进行下一次垃圾回收。
空间问题 - 碎片化
由于该算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后,会产生大量不连续的内存碎片。内存碎片太多会导致以后程序运行过程中需要分配较大的对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。
复制算法(Copying)
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制到空闲面
- 将对象面所有对象内存清除
复制算法将可用内存按容量、比例划分为两个或者多个块,并选择其中的一块或者两块作为对象面,其他的则作为空闲面。对象主要是在对象面上创建的,当被定义的对象面块的内存快用完时,就将还活着的对象复制到其中一块空闲面上,再将已使用的内存空间一次清理掉。
这种算法解决了碎片化的问题、顺序分配内存,简单高效、适用于对象存活率低的场景,比如年轻代,这样使得每次都对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,推倒重建只需要移动堆顶指针按顺序分配内存即可。
事实上现在商用的虚拟机都采用这种算法来回收年轻代,因为年轻代中的对象每次回收都基本上有10%左右的对象存活,所以需要复制的对象很少。
标记-整理算法
- 标记:从根集合中进行扫描,对存活的对象进行标记
- 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。

复制回收算法在应对对象存活率较高的情况就不好用了,要进行较多的复制操作,效率就会变低,更关键的是,如果不想浪费50%的空间就需要有额外的空间进行分配担保以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代中一般并不会适用复制算法,而是采用标记整理算法。
标记-整理算法它采用同标记-清除算法相同的方式对对象进行标记,但在清除阶段不同,标记-整理算法是在标记清除算法的基础上又进行了对象的移动,因此成本更高,但也解决了内存碎片的问题。
分代收集算法(Generation Collector)
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采取不同的垃圾回收算法
- 提高JVM 的回收效率
分代收集算法可以理解为组合拳,将堆内存进一步划分,不同的对象的生命周期和存活情况也不一样,将不同生命周期的对象分配到堆中不同的区域并对堆内存的不同区域采用不同的回收策略进行回收是可以提高JVM回收效率。
java堆结构
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:年轻代和老年代,再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
我们知道jdk7及jdk7以前java的堆内存一般可以分为 Young Generation 年轻代、Old Generation 老年代、以及Permanent Generation 永久代,而 jdk8及以后 永久代就被去掉了,仅保留年轻代和老年代。下图所示的 Eden 区、From Survivor0(“From”) 区、To Survivor1(“To”) 区都属于新生代,Old Memory 区属于老年代。
从垃圾回收的角度,年轻代的对象存活率低就会采用复制算法,而老年代的存活率高就采用标记-清除算法或者标记整理算法。
常用的调优参数
-XX:SurvivorRatio: Eden 和 Survivor 的比值,默认为 8 :1-XX:NewRatio: 老年代和年轻代内存大小的比值-XX:MaxTenuringThreshold: 对象从年轻代晋升到老年代经过 GC 次数的最大阈值
GC 的分类
- Minor GC
- Full GC
分代收集的算法主要分为两种,分别了 Minor GC 和 Full GC , Minor GC 是发生在年轻代中的复制算法,年轻代几乎是所有java 对象出生的地方,即 java对象申请的内存以及存放都是在这里发生的。java 中的大部分对象通常不需要长久的存活,具有朝生夕灭的性质。当一个对象被判定为死亡时,GC 就有责任来回收掉这部分对象的内存空间。新生代是GC 收集垃圾的频繁区域。第二种GC 方式与老年代相关,由于对老年代的回收一般会伴随年轻代的垃圾收集,因此第二种方式被命名为Full GC。
Minor GC
大部分情况,对象都会首先在 Eden 区域分配,若Eden区放不下新创建的对象也可能被放到Survivor区或者老年代中去。两个Survivor 区分别被定义为 “From”区和 “To”区,并且并没有明确规定,会随着垃圾回收的进行而相互转换。
在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为大于 15 岁),就会被晋升到老年代中。
Full GC
在年轻代中的对象经过一定次数的回收后依然存活并且年龄超过了晋升阈值时,将会进入老年代,因此可以认为老年代中存放的都是生命周期较长的对象。老年代的内存要比新生代的内存要大,大概的比例为2:1。
大批对象死去少量对象存活的新生代,使用的是复制算法,复制成本较低;而对象存活率较高,没有额外空间进行分配担保的老年代则采用标记-清理算法或者标记-整理算法进行回收。

当触发老年代的垃圾回收时通常也会伴随着对新生代堆内存的回收,即对整个堆进行垃圾回收,这便是所谓的Full GC
Major GC 通常与 Full GC 是等价的(Major GC 要分清楚是年轻代的GC 还是老年代中的GC),即收集整个GC 堆。Full GC 的速度通常比 Minor GC 慢得多,一般会慢10倍以上,不过一般情况下,老年代中的对象大多数是在Survivor中熬过来的,他们是不会那么容易死掉的,因此Full GC发生的次数不会比 Major GC那么频繁。
触发Full GC 的条件
1、老年代空间不足:如果创建一个大对象,Eden 区域放不下这个对象,就会直接保存在老年代中,如果老年代空间也不足就会触发Full GC,为了避免这种情况最好就是不要创建太大的对象。
2、永久代空间不足:仅仅针对jdk7及以前版本,当系统中需要加载的类调用的方法很多,同时永久代中没有足够的空间去存放我们需要的类的信息和方法的信息就会触发 Full GC, 而jdk8以后取消了永久代所以该条件不成立。这也是为什么用元空间替代永久代的原因,为的是降低 Full GC 的频率,减少GC的负担,提升其效率。
3、CMS GC 时出现 promotion failed, concurrent mode failure : 对于采用 CMS 进行老年代的程序而言尤其要注意 GC 日志中是否有promotion failed, concurrent mode failure 这两种情况,当这两种日志出现的时候可能会触发 Full GC
4、Minor GC 晋升到老年代的平均大小大于老年代的剩余空间:HotSpot 为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象,在进行Minor GC 时做了一个判断,如果之前统计所得到了Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发Full GC.
5、调用 System.gc() : 在程序中直接调用 System.gc()会显示触发 Full GC ,同时对老年代和年轻代进行回收,这个方法只是提醒虚拟机,码农需要你回收一下,但是虚拟机执不执行由虚拟机自己决定,码农没有绝对的控制权。
6、使用RMI来进行 RPC 或管理的JDK 应用,每小时执行1次Full GC
垃圾收集器
相关概念
Stop - the - World
Stop - the - World 意味着 JVM 由于要执行 GC线程,而停止了应用程序的执行,并且这种情况会在任何一种GC 算法中发生。当 Stop - the - world 发生时,除了GC 所需要的线程外,所有线程都处于等待状态,直到GC 任务完成。
实际上GC优化很多时候就是要减少 Stop - the - World 发生的时间,从而使系统具有高吞吐低停顿的特点。
Safepoint
JVM垃圾回收中在可达性分析中,要分析哪个对象有没有被引用的时候,必须在一个快照点进行。此时所有线程被冻结,不可以出现在分析的过程中对象的引用关系还在不停变化的情况,因此分析的结果需要在某个节点具有确定性,该节点便叫做安全点。程序并不是随便到达哪个点便停下来,而是到达安全点才停止。一般在方法调用、循环跳转、异常跳转才会产生安全点。一旦GC 发生就让所有的线程都跑到最新的安全点,再停顿下来。如果发现线程不在安全点就恢复线程等其跑到安全点。安全点的选择不能太少,因为太少会让GC等待时间太长,也不能太多,因为太多会增加程序的负荷。
JVM的运行模式
JVM有两种运行模式,即 Client 和 Server,Client 启动较快、Server 启动较慢,但是启动进入稳定期后,Server 运行更快。因为Server 采用的是重量级的虚拟机而 Client 采用的则是轻量级的虚拟机,所有Server 启动慢但是运行起来后快。
查看当前虚拟机处于那种模式:
1 | C:\Users\hermi>java -version |
常见的垃圾收集器
Serial收集器ParNew收集器Parallel Scavenge收集器CMS收集器G1收集器
Serial 收集器
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client 模式下默认的年轻代收集器

Serial 收集器是java 虚拟机中最基本,历史也最悠久的收集器,在jdk1.3之前是java虚拟机年轻代收集的唯一选择,Serial 收集器是采用复制算法的单线程垃圾收集器,它的单线程意义并不只是说明它只适用于一个CPU或者一条收集线程去实现垃圾回收工作,更重要的是在它进行垃圾回收时必须暂停其他所有的工作线程直到它收集结束 (“Stop The World” )。虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
到目前为止,Serial 收集器依然是虚拟机运行在Client 模式下的默认年轻代收集器,因为其简单高效,在用户桌面应用中分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆年轻代的停顿时间会在几十毫秒到一百毫秒之间完成。只要不是频繁的发生这里的停顿是完全可以接受的。
ParNew收集器
- 多线程收集,其余的行为、特点和Serial 收集器一样
- 单核执行效率不如Serial,在多核下执行才有优势

ParNew 收集器采用的是复制算法的多线程收集,其余行为、特点和Serial 收集器一样。它是Server模式下虚拟机首选的年轻代收集器。在单个 CPU 环境中,并不会比 Serial 收集器好,因为存在线程的交互开销。当随着可用 CPU 数量的增加,对于GC 池以及系统资源的利用更好,默认开启的线程数与CPU 数量相同。同时除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。
Parallel Scavenge 收集器
- 比起关注用户线程停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server 模式下默认的年轻代收集器

Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和 ParNew 都一样,但是Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
Serial Old 收集器
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client 模式下默认的老年代收集器

Serial Old 收集器是Serial 收集器的老年代版本,采用的是标记整理算法。除了采用的垃圾收集算法和Serial 的不同以外,其他的都和Serial 类似。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Old 收集器
- 多线程,吞吐量优先

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。 该收集器是在jdk6以后提供的,在此之前新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态–> 如果新生代选择Parallel Scavenge 收集器,老年代除了选择 Serial Old 收集器以外别无选择。由于到年代 Serial Old 收集器在服务端的拖累,使用Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果。由于单线程的老年代收集中无法充分利用服务器多CPU 的处理能力。在老年代很大而且硬件比较高级的环境中这种组合的吞吐量还不一定有 ParNew + CMS 的组合强。
直到 Parallel Old 收集器出现以后,吞吐量优先收集器终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: stop -the - world 暂停所有的其他线程,并记录下直接与 root 相连的对象并做标记 ,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有 的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: stop -the - world 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 清除垃圾对象,程序不会停顿。

CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
G1收集器是既适用于年轻代也适用于老年代的垃圾收集器,采用 复制 + 标记整理算法
与其他的收集器相比,G1收集器具有如下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势 ,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
在Garbage First 之前的垃圾收集器收集的范围都是整个年轻代或者老年代的,Garbage First 不再是这样。使用 G1 收集器时 java 堆的内存能布局与其他的收集器有很大的差别。它会将整个java 堆内存划分成多个大小相同的独立区域 Region ,虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的啦,他们是可以不连续的 Region 的集合。
垃圾收集器之间的联系
如下图所示:如果两个收集器之间有连线说明它们可以搭配使用

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。
老年代的 CMS 收集器不能与年轻代的 Parallel Scavenge 收集器兼容使用
CMS 是 HotSpot 在jdk5 推出的真正意义上的收集器,第一次实现了让垃圾收集线程与用户线程同时工作。CMS 作为老年代的收集器但却无法与jdk4已经存在的 Parallel Scavenge 收集器配合使用,因为 Parallel Scavenge 以及 G1收集器都没有使用传统的GC 收集器代码框架,而是独立实现的。其余的收集器都共用了部分代码框架,因此可以搭配使用。
参考资料