白话系列之二:JVM的垃圾回收

白话系列之二:JVM的垃圾回收

在垃圾回收这个领域,我们主要关注三个问题:

  1. 哪些内存需要回收(确定对象是否需要回收)
  2. 什么时候回收(判断是否需要回收的时间)
  3. 如何回收(确定回收的算法和具体实现)

本文主要针对以上几个问题从JVM的角度进行分析与解答

一、哪些内存需要回收

1.1 确定回收的对象

在上一篇博文中我们已经详细分析了JVM的内存区域划分,知道了JVM将内存分为了哪些区域、各有什么作用、分别会在什么时候报哪些错误。

显然,五种内存区域中的程序计数器、虚拟机栈和本地方法栈都是与线程的生命周期相同,随线程而生,随线程而灭,自然其内存区域的回收不需要太过关心。而方法区随着方法的执行油田不稳的执行者出栈、入栈操作,其内存区域同样不需要过分管理。而与以上四种内存截然不同的是Java堆。一个接口的多个实现类需要的内存可能不一样,一个方法的多个分支所需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道需要哪些对象、需要多少内存空间。

因此,在本文中所说的内存回收如无特殊说明均仅指堆内存。

个人理解:是否是需要进行垃圾回收检测的内存区域主要看是否是编译期已知的内存分配。以上五种类型的内存只有堆是运行期才可知的,其他均为编译期可知

1.2 扩展:编译期与运行期

  • 编译期:是指把源码交给编译器编译成计算机可以执行的文件的过程.在Java中也就是把Java代码编成class文件的过程.编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误

  • 运行期:是把编译后的文件交给计算机执行.直到程序运行结束.所谓运行期就把在磁盘中的代码放到内存中执行起来.在Java中把磁盘中的代码放到内存中就是类加载过程.类加载是运行期的开始部分,后面会介绍下类加载

  • 编译期分配内存并不是说在编译期就把程序所需要的空间在内存中分配好,而是说在编译期生成的代码中产生一些指令,在运行代码时通过这些指令把程序所需的内存分配好.只不过在编译期的时候就知道分配的大小,并且知道这些内存的位置.而运行期分配内存是指只有在运行期才确定内存的大小,存放的位置

1.3 可达性算法

解决了哪些对象是需要进行垃圾回收检测的这个首要问题,接下来的问题是如何判断堆中的对象是否需要进行垃圾回收?

需要进行内存回收的对象通常被称为已经“死去”,通常是指该对象无可能再被任何途径使用。举例如下:

String str1 = new String("hello");
String str2 = new String("world");
str1 = str2;

我们首先创建了一个字符串对象“hello”,这意味着在堆中有一块内存区域存放着这个对象,并且我们使用str1来指向它,同理使用str2指向了“world”。然后我们将str2这个变量的值(即“world”的地址)赋给了str1,此时str1和str2同时指向了“world”对象。这也就意味着“hello”这个对象没有任何变量引用它,没有任何途径去使用这个对象。因此在此时,“hello”对象就是已经死去的对象,其占有的内存应该被回收。

在Java中,JVM采用可达性算法进行判断对象是否“死亡”:

  • 可达性算法:以一系列被称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索的路径记为引用链,当一个对象无法通过引用链与任意GC Roots相连时,判定为可回收的对象

个人理解:使用图论的概念会更好理解。把每个对象视为图论中的点,对象间的引用关系视为图论中的有向边,那么Java堆其实是一个图论中的森林。如果一个点不在任何以GC Roots为根的树上,则视为不可达,即已经死去的对象,可以被回收了

1.4 扩展:Python的引用计数算法

新兴语言Python中使用了另一种被称为引用计数算法的判断对象是否存活的算法。其基本思想是:每个对象在创建时都会有一个引用计数器,当其被引用时则引用计数器+1,移除一个引用时引用计数器-1。当引用计数减为0时,说明没有任何对该对象的引用,则视为该对象已经死亡,可被回收

这种算法的优点是效率极高、资源占用少。当然,缺点也很明显,无法解决相互循环引用的问题。当对象A引用了B,对象B也引用了A时,即使A、B都被没有其他任何引用,应该均视为已经死亡的对象,但是由于AB之间的相互引用,其引用计数器永远为1,无法被该算法识别为可回收的对象

在Java的可达性算法中就不会出现该问题,当对象AB相互引用时,它们会形成一棵树,但是由于该树仍然没有雨GC Roots相连,因此都会被视为已经死亡的对象

因此,为了弥补引用计数算法的缺点,Python还引入了其他算法作为辅助,即分代算法(分代算法核心思想在Java的垃圾回收中也有体现,后面会有更详细的介绍)

1.5 引用的分类

Java是纯粹的面向对象的语言,对对象的使用一般也是通过引用来进行的。因此引用这个概念对理解Java非常重要。Java对引用进行了分类,其目的在于描述被引用的对象的重要性。例如有的对象非常重要,即使抛出内存不够的错误,也不能自动回收该对象的内存,而有的对象不太重要,当内存足够时可以保留,当内存资源紧缺时可以抛弃。

Java将引用的类型分为四种,分别简介如下:

  1. 强引用(Strong Reference)。强引用是使用最普遍的引用,类似Object obj = new Object()String str = "hello"。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题;

  2. 软引用(Soft Reference)。软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示,如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用通常用于网页缓存、图片缓存,防止内存溢出,在内存充足的时候,缓存对象会一直存在,在内存不足的时候,缓存对象占用的内存会被垃圾收集器回收。使用示例如下:

//example1
public void testSoftReference() {
Map<String,SoftReference<Bitmap>> imagesCache = new HashMap<String,SoftReference<Bitmap>>();
 Bitmap bitmap = getBitmap();
SoftReference<Bitmap> image1 = new SoftReference<Bitmap>(bitmap);
imagesCache.put("image1",image1);
SoftReference<Bitmap> result_SoftReference = imagesCache.get("image1");
Bitmap result_Bitmap = result_SoftReference .get();
}
//example2
import java.lang.ref.SoftReference;
 
public class Main {
    public static void main(String[] args) {
        SoftReference<String> sr = new SoftReference<String>(new String("hello"));
        System.out.println(sr.get());
    }
}
  1. 弱引用(Weak Reference)。弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,在java中用java.lang.ref.WeakReference类来表示。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以用于:单例类持有一个activity引用时,会造成内存泄露,把activity声明为弱引用,在activity销毁后,垃圾收集器扫描到activity对象时,会回收activity对象的内存。使用示例如下:
//example3

public class SingleTon1 {  
    private static final SingleTon1 mInstance = null;  
    private WeakReference<Context> mContext;
    private SingleTon1(WeakReference<Context> context) {  
  	mContext = context;
    }  
  
    public static SingleTon1 getInstance(WeakReference<Context> context) {  
        if (mInstance == null) {  
            synchronized (SingleTon1.class) {  
                if (mInstance == null) {  
                    mInstance = new SingleTon1(context);  
                }  
            }  
        }  
        return mInstance;  
    }  
}  
  
public class MyActivity extents Activity {
    public void onCreate (Bundle savedInstanceState){
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       SingleTon1 singleTon1 = SingleTon1.getInstance(new WeakReference<Context>(this));
   }
 
}

//example4
import java.lang.ref.WeakReference;
 
public class Main {
    public static void main(String[] args) {
     
        WeakReference<String> sr = new WeakReference<String>(new String("hello"));
         
        System.out.println(sr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}
  • 输出结果:hello、null。第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。
  1. 虚引用(Phantom Reference)。虚引用和软引用、弱引用不同,它并不影响对象的生命周期,也无法通过虚引用来取得一个对象实例,在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用,使用示例如下:
//example5
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
 
 
public class Main {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

二、什么时候回收对象

2.1 终结状态

每个在内存中的对象都会有一个终结状态。其取值为终结状态空间 F = {unfinalized, finalizable, finalized}之中的某一个。三种状态的含义分别为:

  • unfinalized : JVM未调用对象的finalize(), 也不准备调用对象的finalize()
  • finalizable: 表示JVM可调用对象的finalize(),但是还未调用
  • finalized: 表示JVM已经调用过了该对象的finalize()

2.2 可达状态

与终结状态类似,每个在内存中的对象都会有一个可达状态。其取值为可达状态空间 R = {reachable, finalizer-reachable, unreachable}之中的某一个。三种状态的含义分别为:

  • reachable: 表示GC Roots引用可达, 比如在栈中有变量引用该对象
  • finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
  • unreachable:对象不可通过上面两种途径可达, 也就是不可到达, 没有任何对象引用着

2.3 对象在内存回收时的状态变化

对象都由以上两种状态空间的某个取值组成,其状态会因为某些条件改变,具体条件如下:

  • 新建对象首先处于[reachable, unfinalized]状态
  • 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable或unreachable状态
  • 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable。
  • 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态, 这就是对象重生
  • 处于finalizable状态的对象不能同时是unreahable的,由上一点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable
  • 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
    若JVM检测到finalized状态的对象变成unreachable,回收其内存
  • 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象

三、如何回收内存

3.1 垃圾收集算法

在JVM规范中并没有明确GC的运作方式,各个厂商可以采用不同的方式去实现垃圾回收器。这里讨论几种常见的GC算法

  • 标记-清除算法(Mark-Sweep)
    思路:首先标记出需要回收的对象,在标记完成后统一回收掉所有的被标记对象。
    缺点:效率问题和空间问题(标记清除后会产生大量的不连续内存碎片,内存碎片过多可能会导致程序需要分配较大对象时找不到足够大的连续内存空间而不得不提前触发另一次垃圾回收动作)

  • 复制算法(Copying)
    思路:为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
    缺点:显而易见,可使用的内存降为原来一半

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

  • 分代收集算法(Generational Collection)
    分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

目前大部分JVM的GC对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。

对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中

3.2 扩展:Java堆内存的分代

(1)新生代:

新生代使用复制和标记-清除垃圾收集算法,研究表明,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。

新生代中98%情况下空白Survivor都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。

Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。

使用Java虚拟机-Xmn参数可以指定新生代内存大小。

(2)年老代:

年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。

Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

当新生代中无足够空间为对象创建分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代空间无法在扩展时,堆就会产生OutOfMemoryError异常。

Java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。

(3)永久代:

java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。

永久代也使用标记-整理算法进行垃圾回收,Java虚拟机参数-XX:PermSize和-XX:MaxPermSize可以设置永久代的初始大小和最大容量。

3.3 典型的垃圾收集器

(1)Serial收集器

  • Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择;

  • 特点:针对新生代;采用复制算法;单线程收集;进行垃圾收集时,必须暂停所有工作线程,直到完成;会"Stop The World";

  • 应用场景: 依然是HotSpot在Client模式下默认的新生代收集器;也有优于其他收集器的地方:简单高效(与其他收集器的单线程相比);对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

Stop The World:JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿,会带给用户不良的体验

(2)ParNew收集器

  • ParNew垃圾收集器是Serial收集器的多线程版本
  • 特点:除了多线程外,其余的行为、特点和Serial收集器一样;如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等; 两个收集器共用了不少代码;
  • 应用场景: 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销

(3)Parallel Scavenge收集器

  • Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)
  • 特点:有一些特点与ParNew收集器相似,比如新生代收集器、采用复制算法、多线程收集,但是其最主要的特点是实现一个可控制的吞吐量(Throughput)
  • 应用场景:高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互,例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序

(4)Serial Old收集器

  • Serial Old是 Serial收集器的老年代版本
  • 特点:针对老年代;采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);单线程收集
  • 应用场景:主要用于Client模式;而在Server模式有两大用途:在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

(5)Parallel Old收集器

  • Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本, JDK1.6中才开始提供
  • 特点:针对老年代;采用"标记-整理"算法;多线程收集
  • 应用场景: JDK1.6及之后用来代替老年代的Serial Old收集器,特别是在Server模式,多CPU的情况下, 这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合

(6)CMS收集器

  • 并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器
  • 特点:针对老年代;基于"标记-清除"算法(不进行压缩操作,产生内存碎片);以获取最短回收停顿时间为目标;并发收集、低停顿;需要更多的内存
  • 应用场景:与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用;
  • CMS收集器运作过程:
    (A) 初始标记(CMS initial mark)。仅标记一下GC Roots能直接关联到的对象,速度很快, 但需要"Stop The World";
    (B) 并发标记(CMS concurrent mark)。进行GC Roots Tracing的过程,刚才产生的集合中标记出存活对象,应用程序也在运行, 并不能保证可以标记出所有的存活对象
    (C)重新标记(CMS remark)。为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短,采用多线程并行执行来提升效率
    (D)并发清除(CMS concurrent sweep)。 回收所有的垃圾对象;

整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低

(7)G1收集器

  • G1(Garbage-First)是JDK7-u4才推出商用的收集器
  • 特点:
    – (A)并行与并发。能充分利用多CPU、多核环境下的硬件优势,也可以并发让垃圾收集与用户程序同时进行
    – (B)分代收集,收集范围包括新生代和老年代。能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配,能够采用不同方式处理不同时期的对象。虽然保留分代概念,但Java堆的内存布局有很大差别,将整个堆划分为多个大小相等的独立区域(Region) ,新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合
    – (C)结合多种垃圾收集算法,空间整合,不产生碎片。从整体看,是基于标记-整理算法, 从局部(两个Region间)看,是基于复制算法,这是一种类似火车算法的实现,都不会产生内存碎片,有利于长时间运行
    – (D)可预测的停顿:低停顿的同时实现高吞吐量。G1除了追求低停顿处,还能建立可预测的停顿时间模型,可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒
  • 应用场景:面向服务端应用,针对具有大内存、多处理器的机器,最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案, 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
  • G1可以建立可预测的停顿时间模型。这是因为:可以有计划地避免在Java堆的进行全区域的垃圾收集;G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);这就保证了在有限的时间内可以获取尽可能高的收集效率

3.2 吞吐量

吞吐量指的是CPU用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间

对于垃圾收集器,它主要关注三个方面的性能指标:

  • 停顿时间 。停顿时间越短就适合需要与用户交互的程序,良好的响应速度能提升用户体验

  • 吞吐量。高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务,主要适合在后台计算而不需要太多交互的任务

  • 覆盖区(Footprint)。 在达到前面两个目标的情况下,尽量减少堆的内存空间, 可以获得更好的空间局部性

3.3 一个对象被不同区域引用的问题

一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出): 回收新生代也不得不同时扫描老年代。 这样的话会降低Minor GC的效率

无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:

  • 每个Region都有一个对应的Remembered Set;
  • 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
  • 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。

四、 后记

4.1 参考文献

4.2 相关文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值