Java垃圾回收机制

问题1:
---对什么区域进行回收?
堆和方法区
因为栈是线程私有数据,当方法执行完成后,就会进行弹栈,所以不进行回收

问题2:
---什么情况下进行回收?
对这个对象不再引用的时候,进行回收

Java的引用类型

  • 强引用:在代码中有明显new Object()操作的这类引用,只要这种引用还存在,垃圾回收机制就不会回收它。就算内存不够,只会抛出OOM,也不会回收这种对像。
  • 软引用(java.lang.ref.SoftReference):如果内存够用的情况下不会回收,如果内存不够用的情况下进行回收。在JDK 1.2之后,提供了SoftReference类来实现软引用。
    应用场景:软引用通常用来实现内存敏感的缓存。如果内存空间还有剩余,就可以暂时保留缓存,当内存不足时,就可以将缓存清除,这样就保证了使用缓存的同时,不会耗尽内存。如用户的登录信息。
public class SoftRef {
    public static void main(String[] args) {
        Object object = new Object();
        SoftReference<Object> softReference = new SoftReference<>(object);
        //当结束object的引用时,Object对象就成了软引用
        object = null;
        System.gc();
        //重新获得该实例的强引用,如果该实例回收后get为null
        Object o = softReference.get();
        System.out.println(o == null); //内存足够时为false,内存不足时为true
    }
}
  • 弱引用(java.lang.ref.WeakReference):当下一次内存回收时,无论内存是否足够,都进行回收。
    弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
    应用场景:弱应用同样可用于内存敏感的缓存。与软引用不同的是,在内存空间还足够时,软引用的对象不会被回收,但是,弱引用的对象有可能会被回收,其存活时间相比于弱引用更短一点。
public class WeekRef {
    public static void main(String[] args) {
        //弱引用示例
        WeakReference<String> weakReference = new WeakReference<>(new String("hello"));
        //通知JVM进行垃圾回收
        System.gc();
        String str = weakReference.get();
        System.out.println(str == null); //true
    }
}
  • 虚引用(java.lang.ref.PhantomReference): 

    它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被垃圾回收器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
    应用场景:可以用虚引用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

public class PhantomRef {
    public static void main(String[] args) {
        //虚引用示例
        PhantomReference<String> phantomReference =
                new PhantomReference<>(new String("hello"), new ReferenceQueue<>());
        System.out.println(phantomReference.get()); //null
    }
}

软引用、弱引用和虚引用可以和一个引用队列(ReferenceQueue)联合使用,如果它们所引用的对象被垃圾回收,Java虚拟机就会把这个引用加入到与之关联的引用队列中。采用软引用、弱引用和虚引用可以预防Java虚拟机抛出OOM异常以及内存泄漏的问题。 

如何判断对象是否被引用

判断对象是否被引用有“引用计数”和“根可达性分析”两种方法,Java中采用的是“根可达性分析”方式,因为“引用计数”方式无法解决循环引用的问题。

引用计数

给对象增加一个引用计数器,每当这个对象进行一次引用,计数器就+1,每当引用失效时,计数器就-1。
当这个引用计数器等于0时,表示这个对象不再被引用。

如下代码,o1和o2的引用计数各为2,按理来说在GC时,对象不会进行回收

public class ReferenceCountingGC {
    private static final int MB = 1024 * 1024;
    public Object instance;

    private byte[] size = new byte[MB];

    public static void main(String[] args) {
        //强引用
        ReferenceCountingGC o1 = new ReferenceCountingGC();
        ReferenceCountingGC o2 = new ReferenceCountingGC();
        //互相引用(循环引用)
        o1.instance = o2;
        o2.instance = o1;

        o1 = null;
        o2 = null;
        //按理说引用计数不为0,不会进行回收
        System.gc();
    }
}

使用-XX:+PrintGCDetails -XX:+UseSerialGC参数运行结果如下:

也可以使用-XX:+PrintGC -XX:+UseSerialGC参数,运行结果如下:

-XX:+UseSerialGC参数所表示的含义为:使用Parallel Scavenge和Parallel Old垃圾回收器组合。

-XX:+PrintGCDetails参数和-XX:+PrintGC参数的区别在于,前者会打印GC回收详情
-verbose:gc参数同-XX:+PrintGC参数,二者的打印结果相同,可以将-verbose:gc参数理解为-XX:+PrintGC参数的别名,
通过运行结果可以看到在GC的时候对内存进行了回收。

问题3:
---实际运行结果和理论分析结果不一致
如果采用引用计数法来判断,上述代码的情况下o1对象和o2对象将永远不会回收。也就是说引用计数无法解决循环引用的问题
因此Java中并没有采用引用计数法,而是采用的可达性分析判断的

可达性分析

Java并不采用引用计数法来判断对象是否已“死”,而采用“可达性分析”来判断对象是否存活。
此算法的核心思想:通过一系列称为“GC Roots”的根对象作为起始点,从根节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。
简单说就是,从根节点顺着”线头“开始找,能找到的对象就不是垃圾,顺着线头找不到的对象全部判定为垃圾。

如下图中描述,四种对象可以被当成GC Roots。

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对像
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

本地方法栈中JNI引用的对象说明,如下示例所示,就是指Native方法中的引用对象

public class JNIGCRoots {

    //Person引用的对象可以作为GC Roots
    private native void demo(Person person);

    void test() {
        Person person = new Person();
        demo(person);
    }
}

class Person {
    private String name;
}

垃圾回收算法

垃圾回收算法只有三种,即标记清除算法复制算法标记整理算法。以下三种算法各有优缺点

分代收集

分带收集是一种策略,大部分的对象都很短命,都在很短的时间内都被回收了(IBM 专业研究表明,一般来说,98% 的对象都是朝生夕死的,经过一次 Minor GC 后就会被回收),所以分代收集策略根据对象存活周期的不同分成新生代老年代(Java8以前还有个永久代),默认比例为 1:2,新生代又分为Eden区, From Survivor区(简称s0),To Survivor区(简称 s1),三者的比例为 8:1:1。这样就可以根据新老生代的特点选择最合适的垃圾回收算法,把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Major GC)。

新生代约占堆内存的1/3大小,老年代约占堆内存的2/3大小
伊甸区、s0和s1区域按照新生代中8:1:1的区域放置,该比列可配置
大多数对象在Eden中被new出来,之所以不是全部对象,是大对象会直接在老年代进行创建。
From和To区的关系就是复制算法中的二者的关系,为互换的身份。

Full GC指收集整个Java堆和方法区的垃圾收集器。

注意:从JDK1.8开始,取消了永久代,改为了MetaSpack(元数据空间)。区别在于永久代在虚拟机当中;元数据空间不在虚拟机当中,而是用的本地内存;

从Eden到survivor使用复制算法,从survivor到老年代使用复制算法。但是在old中绝对不会使用复制算法。

问题4:
---为什么移除永久代?
参考官方解释http://openjdk.java.net/jeps/122
大概意思是移除永久代是为融合HotSpot与JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。

 


SafePoint和OopMap

safepoint 安全点顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread's representation of it's Java machine state is well described),比如记录OopMap的状态,从而确定GC Root的信息,使JVM可以安全的进行一些操作,比如开始GC。
safepoint指的特定位置主要有:
1. 循环的末尾 (防止大循环的时候一直不进入safepoint,而其他线程在等待它进入safepoint)
2. 方法返回前
3. 调用方法的call之后
4. 抛出异常的位置

OopMap用于枚举 GC Roots,本质是一中数据结构。告诉在safePoint可以回收那些数据。
OopMap是HotSpot中的概念,JRockit里叫做livemap,J9里叫做GC map。Apache Harmony的DRLVM也把它叫GCMap。
OopMap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。
一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 OopMap 的数据结构来记录这类信息。
一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。
通过上面的解释,我们可以很清楚的看到使用 OopMap 可以避免全栈扫描,加快枚举根节点的速度。但这并不是它的全部用意。它的另外一个更根本的作用是,可以帮助 HotSpot 实现准确式 GC (个人感觉这才是 OopMap 被设计出来的根本原因,提高 GC Roots Enumeration 速度更像是一个“意外的惊喜”)

抢先式中断:主动中断,没有执行到指safePoint的用户线程,继续执行到safePoint
缺点:假如线程处于wait或sleep时,就会导致很长时间无法执行到savePoint
主动式中断:当GC需要中断线程的时候,不直接对线程进行操作,仅仅简单的设置一个标志,让用户线程自己去检查,然后中断。

在GC时,会进行STW(stop the world)。正常执行的用户线程全部停止。
STW的象形例子:在打扫卫生的时候,不允许在扔垃圾。


垃圾回收器

使用垃圾回收器参数:+XX:+Use<垃圾回收器名称>GC

Serial收集器(新生代)

开启参数:-XX:+UseSerialGC

Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机 新生代收集的唯一选择。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接收的。
实际上到现在为止,它依然是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。它有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

Serial Old收集器(老年代)

开始参数:-XX:+UseSerialGC

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用 “标记-整理”(Mark-Compact)算法。
此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:

  • 在JDK1.6之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
  • 作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用。

ParNew收集器(新生代)

开启参数:-XX:+UseParNewGC

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是, 除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器。
ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在 多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用 -XX:ParallerGCThreads参数设置。

Parallel Scavenge收集器(新生代)

开启参数:-XX:+UseParallelGC

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。
吞吐量=用户代码执行时间/(GC时间+用户代码执行时间)
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。

Parallel Old收集器(老年代)

开启参数:-XX:+UseParallelOldGC

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和 “标记-整理”算法。前面已经提到过,这个收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,所以在Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同

Concurrent Mark Sweep收集器(老年代)

开启参数:-XX:+UseConcMarkSweepGC

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于 “标记-清除”算法实现的。
CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行 GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,因此CMS收集器也被称为 并发低停顿收集器(Concurrent Low Pause Collector)。
缺点

  • 对CPU资源非常敏感,其实面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是 当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
  • 无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为 “浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大.麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。

-XX:+UseCMSCompactAtFullCollection 用来设置CMS空间参数
-XX:+CMSFullGCsBeforeCompaction GC执行完成之后进行一次整理操作
-XX:+CMSInitiatingOccupancyFraction 执行多少次FullGC进行一次整理操作
只可以在老年代使用,因此对新生代无法控制,在运行的过程中新生代也会产生一定的垃圾,因此当老年代的空间达到68%时,默认激活CMS

G1收集器

开启参数:-XX:+UseG1GC

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
  • 空间整合 G1从整体来看是基于 “标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于 “复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

横跨整个堆内存
在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。

建立可预测的时间模型
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间, 优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

避免全堆扫描——Remembered Set
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable 把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要 停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking)从GC Root开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation)首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段(Safepoint处):

RememberedSet
RememberedSet 用于处理这类问题:比如说,新生代 gc (它发生得非常频繁)。一般来说, gc 过程是这样的:首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。但问题是,确实可能存在位于老年代的某个 GC Root,它引用了新生代的某个对象,这个对象是不能清除的。那怎么办呢?
仍然是拿空间换时间的办法。事实上,对于位于不同年代对象之间的引用关系,虚拟机会在程序运行过程中给记录下来。对应上面所举的例子,“老年代对象引用新生代对象”这种关系,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是 RememberedSet 。所以“新生代的GC Roots” + “RememberedSet 存储的内容”,才是新生代收集时真正的GC Roots 。然后就可以以此为据,在新生代上做可达性分析,进行垃圾回收。
G1 收集器使用的是化整为零的思想,把一块大的内存划分成很多个域( Region )。但问题是,难免有一个 Region 中的对象引用另一个 Region 中对象的情况。为了达到可以以 Region 为单位进行垃圾回收的目的, G1 收集器也使用了 RememberedSet 这种技术,在各个 Region 上记录自家的对象被外面对象引用的情况。


总结


JDK7/8默认的垃圾回收器是:ParallelScavenge(新生代)+ParallelOld(老年代)
JDK9默认的垃圾回收器是:G1

垃圾回收器使用示例,垃圾回收器使用demo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值