JVM理解(二):垃圾回收

目录

堆内存结构

堆内部结构

新生代

老年代

永久代

总结

堆外内存

分层编译(-XX:+TieredCompilation)

OOM

垃圾回收

四种引用类型

强引用

软引用

弱引用

幻象引用(虚引用)

判定对象是否存活的方法

引用计数算法

可达性分析算法

垃圾回收算法

标记-清除算法

复制算法

标记-整理算法

分代收集算法

垃圾回收器

Serial收集器

Serial Old收集器

ParNew收集器

Parallel Scavenge收集器 

Parallel Old收集器

CMS收集器

G1收集器

概述

Region

GC模式

G1GC特点

可预测的停顿


堆内存结构

堆内部结构

新生代

新生代是大部分对象创建和销毁的区域,在通常的Java应用中,绝大部分对象生命周期都是很短暂的。其内部又分为Eden区域,作为对象初始分配的区域;两个Survivor,有时候也叫from、to区域,被用来放置从Minor GC中保留下来的对象。

  • JVM会随意选取一个Survivor区域作为“to”,然后会在GC过程中进行区域间拷贝,也就是将Eden中存活下来的对象和from区域的对象,拷贝到这个“to”区域。这种设计主要是为了防止内存的碎片化,并进一步清理无用对象。
  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,Hotspot JVM还有一个概念叫做Thread Local Allocation Bufer(TLAB)。这是JVM为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度。

从图中可以看出,TLAB仍然在堆上,它是分配在Eden区域内的。其内部结构比较直观易懂,start、end就是起始地址,top(指针)则表示已经分配到哪里了。所以我们分配新对象,JVM就会移动top,当top和end相遇时,即表示该缓存已满,JVM会试图再从Eden里分配一块儿。 

老年代

放置长生命周期的对象,通常都是从Survivor区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。

永久代

这部分就是早期Hotspot JVM的方法区实现方式了,储存Java类元数据、常量池、Intern字符串缓存,在JDK 8之后就不存在永久代这块儿了。关于永久代和元数据区参考:http://www.cnblogs.com/paddix/p/5309550.html

总结

  • 最大堆体积:-Xmx value
  • 初始的最小堆体积:-Xms value
  • 老年代和新生代的比例:-XX:NewRatio=value。默认情况下,这个数值是3,意味着老年代是新生代的3倍大;换句话说,新生代是堆大小的1/4。当然也可以直接设置数值:-XX:NewSize=value
  • Eden和Survivor的大小是按照比例设置的,如果SurvivorRatio是8,那么Survivor区域就是Eden的1/8大小,也就是新生代的1/10,因为YoungGen=Eden + 2*Survivor,JVM参数:-XX:SurvivorRatio=value
  • 在JVM内部,如果Xms小于Xmx,堆的大小并不会直接扩展到其上限。Virtual区域表示暂时保留的区域。

堆外内存

在了解之前先解释一下分层编译

分层编译(-XX:+TieredCompilation)

     除了纯编译和默认的mixed之外,jvm 从jdk6u25 之后,引入了分层编译。HotSpot 内置两种编译器,分别是client启动时的c1编译器和server启动时的c2编译器,c2在将代码编译成机器代码的时候需要搜集大量的统计信息以便在编译的时候进行优化,因此编译出来的代码执行效率比较高,代价是程序启动时间比较长,而且需要执行比较长的时间,才能达到最高性能;与之相反, c1的目标是使程序尽快进入编译执行的阶段,所以在编译前需要搜集的信息比c2要少,编译速度因此提高很多,但是付出的代价是编译之后的代码执行效率比较低,但尽管如此,c1编译出来的代码在性能上比解释执行的性能已经有很大的提升,所以所谓的分层编译,就是一种折中方式,在系统执行初期,执行频率比较高的代码先被c1编译器编译,以便尽快进入编译执行,然后随着时间的推移,执行频率较高的代码再被c2编译器编译,以达到最高的性能。

首先开启NMT并选择summary模式:

-XX:NativeMemoryTracking=summary

为了方便获取和对比NMT输出,选择在应用退出时打印NMT统计信息

-XX:+UnlockDiagnosicVMOptions -XX:+PrintNMTStatisics

执行一个简单的在标准输出打印HelloWorld的程序,就可以得到下面的输出

 

  • 什么是mmap: mmap对于c程序员很熟悉,对于java程序员有点陌生。简而言之,将文件直接映射到用户态的内存地址,这样对文件的操作不再是write/read,而是直接对内存地址的操作。 
  • 第一部分非常明显是Java堆。
  • 第二部分是Class内存占用,它所统计的就是Java类元数据所占用的空间,对于本例,因为HelloWorld没有什么用户类库,所以其内存占用主要是启动类加载器(Bootstrap)加载的核心类库。
  • 下面是Thread,这里既包括Java线程,如程序主线程、Cleaner线程等,也包括GC等本地线程。
  • 接下来是Code统计信息,显然这是CodeCache相关内存,也就是JIT compiler存储编译热点方法等信息的地方,JVM提供了一系列参数可以限制其初始值和最大值等:-XX:InitialCodeCacheSize=value -XX:ReservedCodeCacheSize=value
  • 下面就是GC部分了,G1等垃圾收集器其本身的设施和数据结构就非常复杂和庞大,例如Remembered Set通常都会占用20%~30%的堆空间
  • Compiler部分,就是JIT的开销,显然关闭TieredCompilation会降低内存使用。
  • 其他一些部分占比都非常低,通常也不会出现内存使用问题,请参考官方文档。唯一的例外就是Internal部分,其统计信息包含着Direct Bufer的直接内存,这其实是堆外内存中比较敏感的部分,很多堆外内存OOM就发生在这里。原则上Direct Bufer是不推荐频繁创建或销毁的,如果你怀疑直接内存区域有问题,通常可以通过类似instrument构造函数等手段,排查可能的问题。

OOM

除了程序计数器,其他区域都有可能会因为可能的空间不足发生OutOfMemoryError,简单总结如下:

  • 堆内存不足是最常见的OOM原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小;或者出现JVM处理引用不及时,导致堆积起来,内存无法释放等。
  • 而对于Java虚拟机栈和本地方法栈,这里要稍微复杂一点。如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM实际会抛出StackOverFlowError;当然,如果JVM试图去扩展栈空间的的时候失败,则会抛出OutOfMemoryError。
  • 对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似Intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
  • 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。
  • 直接内存不足,也会导致OOM

垃圾回收

GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。

  • 对新生代的对象的收集称为minor GC;
  • 对老生代的对象的收集称为majorGC;
  • 程序中主动调用System.gc()强制执行的GC为Full GC(Full GC包括minor GC和major GC)。

四种引用类型

强引用

最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域,或者显示的被引用赋值为null,就可以被垃圾收集了,当然还得看垃圾收集策略。

软引用

是一种相对强引用弱化一点的引用,可以让对象豁免一些垃圾收集,只有当JVM认为内存不足时,才会回收软引用指向的对象。JVM在确保OOM之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存。

软引用特点:

软引用通过SoftReference类实现。软引用的生命周期比强引用短一些,软引用可以和引用队列(ReferenceQueue)联合使用,如果软引用被垃圾回收器回收,JVM就会把软引用加入到与之

关联的引用队列中,后续,我们可以通过poll()方法来检查是否有他关心的对象被回收,如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

实例:

Browser prev = new Browser();              
SoftReference sr = new SoftReference(prev);      
if(sr.get()!=null) {
    rev = (Browser) sr.get();
} else {
    prev = new Browser();
    sr = new SoftReference(prev);
}

弱引用

弱引用并不能使对象豁免垃圾收集,仅仅是让对象提供一种在弱引用状态下访问的途径。这就可以用来构建一种没有特定约束的关系。比如维护一种非强制性的约束关系,试图获取时对象还在,就使用他,否则重新实例化。同样是很多缓存实现的选择。应用场景也包括静态内部类中,经常会使用弱引用。

弱引用特点:

弱引用生命周期比软引用还短,通过WeakReference类实现。在垃圾回收器线程扫描它所管辖的内存区域时,一旦发现具有弱引用的对象,就会收回。由于垃圾回收器是一个优先级很低的线程,

因此不一定会很快回收弱引用的对象,弱引用可以和引用队列(ReferenceQueue)联合使用,如果弱引用被垃圾回收器回收,JVM就会把软引用加入到与之关联的引用队列中。

 示例如下:如果类 B 不是弱引用类 A 的话,执行 main 方法会出现内存泄漏的问题, 因为类 B 依然依赖于 A。

public class Main {
    public static void main(String[] args) {
        A a = new A();
        B b = new B(a);
        a = null;
        System.gc();
        System.out.println(b.getA());
    }
}
 
class  A {}
 
class B {
    A a = null;
    public B (A a) {
        a = new A();
    }
 
    public void setA(A a) {
        this.a = a;
    }
 
    public A getA(){
        return a;
    }
 
}
// 使用WeakReference
class A {}
class B {
    WeakReference<A> weakReference;
    public B(A a) {
        weakReference = new WeakReference<>(a);
    }
    public A getA() {
        return weakReference.get();
    }
}

幻象引用(虚引用)

也叫虚引用,不能通过它访问对象,幻像引用仅仅是确保对象被finalize后做某些事情的机制,比如通常做所谓的Post-Modern清理机制,还有Cleaner机制,有人利用幻象引用监控对象的创建和销毁。

幻象引用特点:

无法通过幻象引用访问对象的任何属性和函数,如果一个对象仅持有幻象引用,就和没有引用一样,任何时候都可能被回收,幻象引用必须和引用队列联合使用,当垃圾回收器线程准备回收一个对象时,如果发现它还有幻象引用,就会在垃圾回收之前,把幻象引用加入到与之关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了幻象引用,来了解被引用的对象是否即将被垃圾回收,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

应用场景:可以用来跟踪对象被垃圾回收的活动,当一个幻象引用的对象被垃圾回收前一定会收到一条系统通知。

示例:

Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);

判定对象是否存活的方法

引用计数算法

引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

可达性分析算法

该算法的思想是:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象可以被回收。

在java中可以作为GC Roots的对象有以下几种:

  • 虚拟机栈中引用的对象
  • 方法区类静态属性引用的对象
  • 方法区常量池引用的对象
  • 本地方法栈JNI引用的对象
  1. 虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象不一定会被回收。当一个对象不可达GC Root时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记,如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已被虚拟机调用过,那么就认为是没必要的,等待回收。
  2. 如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
  3. 如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合。

垃圾回收算法

标记-清除算法

最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。

它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制算法

为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。 

标记-整理算法

复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。 

分代收集算法

新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。

老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。

新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。

大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。没熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

垃圾回收器

垃圾收集算法是方法论,垃圾收集器是具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。JDK7/8后,HotSpot虚拟机所有收集器及组合(连线)如下:

Serial收集器

Serial收集器是最基本、历史最久的收集器,曾是新生代手机的唯一选择。他是单线程的,只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它在收集的时候,必须暂停其他所有的工作线程,直到它结束,即“Stop the World”。

尽管如此,它仍然是虚拟机运行在client模式下的默认新生代收集器:简单而高效(与其他收集器的单个线程相比,因为没有线程切换的开销等)。

Serial Old收集器

Serial 收集器的老年代版本,单线程,“标记整理”算法,主要是给Client模式下的虚拟机使用。

JDK 1.5之前的版本中与Parallel Scavenge 收集器搭配使用。可以作为CMS的后背方案,在CMS发生Concurrent Mode Failure是使用

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样。

是许多运行在Server模式下的JVM中首选的新生代收集器,其中一个很重还要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作。

Parallel Scavenge收集器 

新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

Parallel Old收集器

Parallel Scavenge的老年代版本,多线程,“标记整理”算法,JDK 1.6才出现。

Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。

基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:

  1. 初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”
  2. 并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。
  3. 重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”
  4. 并发清除:清除标记为可以回收对象,可以和用户线程并发执行

由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

CMS的缺点:

  • 对CPU资源非常敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
  • 无法处理浮动垃圾:在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现"Concurrent Mode Failure"失败。并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生
  • CMS基于"标记-清除"算法,清除后不进行压缩操作产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作 

G1收集器

概述

在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存。每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。

Region

堆内存中一个Region的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是1M、2M、4M、8M、16M和32M,总之是2的幂次方,如果G1HeapRegionSize为默认值,则在堆初始化时计算Region的实践大小,具体实现如下:

默认把堆内存按照2048份均分,最后得到一个合理的大小。

GC模式

G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。

young gc

发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

mixed gc

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

和CMS类似,mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.

mixed gc的执行过程有点类似cms,主要分为以下几个步骤:

  1. initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象
  2. concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
  3. remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
  4. clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中

full gc

如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.

G1GC特点

  • 并行与并发:能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。
  • 分代收集:G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象。
  • 空间整合:整体上看采用标记整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。
  • 可预测的停顿:除了追求低停顿还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。

可预测的停顿

  • 是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。
  • G1收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。
  • G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
  • 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
  • 这就保证了在有限的时间内可以获取尽可能高的收集效率。

对象被其他Region的对象引用了怎么办?

判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出):新生代回收的时候不得不扫描老年代?无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:每个Region都有一个对应的Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier 暂时中断操作;然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象);如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;进行垃圾收集时,在GC根节点的枚举范围加入 Remembered Set ,就可以保证不进行全局扫描,也不会有遗漏。

猜你喜欢

转载自blog.csdn.net/m0_37683758/article/details/87030481