欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM - 垃圾收集器与内存分配策略

程序员文章站 2022-07-12 20:09:37
...

1. 概述

JVM的运行时数据区中,程序计数器,本地方法栈,虚拟机栈都是线程私有的,随线程生而生,随线程灭而灭,他们的内存分配和回收都具有确定性,但是堆和方法区则不同,一个接口的多个实现类需要的内存可能不一样,一个方法中多个分支所需要的内存可能也不一样,这部分的内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存

2. 判断垃圾

垃圾指的是不可能再被使用的对象,主要有两种方法判断哪个对象成为了垃圾(死亡),一个是引用计数法,另一个是可达分析

2.1 引用计数法

引用计数法是为每个对象分配一个引用计数器实现的,引用计数器记录一个对象当前被引用的次数,当一个地方引用这个对象,它的引用计数器的值就+1,当引用实现,计数器的值就-1;

引用计数器存在循环引用的问题: 当两个对象相互引用对方,初次之外没有其他地方再去引用这两个对象,这时虽然这两个对象已经不可能再访问了,但是由于他们还互相引用着彼此,导致他们的引用计数不为0,无法被回收

public class ReferenceCountingGc{
	private Object instance = null;
}
public static void main(){
	ReferenceCountingGc objA = new ReferenceCountingGc();//CountingA = 1
	ReferenceCountingGc objB = new ReferenceCountingGc();//CountingB = 1
	
	objA.instance = objB;//CountingA = 2
	objB.instance = objA;////CountingB = 2
	
	objA = null;//CountingA = 1
	objB = null;//CountingB = 1
	//访问不到这两个对象,但是无法GC
}

为了解决这个问题,虚拟机使用的是可达性分析算法

2.2 可达性分析算法

可达性分析算法的基本思路是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索所走过的路径称为引用链;
判断堆中的一个对象是不是垃圾,就扫描堆中的对象,看是否能够沿着GC Roots对象为起点的引用链找到该对象,找不到表示GC Roots到这个对象不可达,那么就是垃圾
JVM - 垃圾收集器与内存分配策略
上图中,对象object 5object 6object 7虽然互有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象

到底什么是GC Roots

准确地说,GC Roots其实不是一组对象,而通常是一组特别管理的指向引用类型对象的指针,这些指针是tracing GCtrace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针,这也是tracing GC算法不会出现循环引用问题的基本保证

哪些对象可以作为GC Roots对象?
深入理解JVM(2)——GC算法与内存分配策略

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    GCRootDemo gc1 = new GCRootDemo();

  • 方法区中类静态属性引用的对象
    private static GCRootDemo gc2;

  • 方法区中常量所引用的对象
    private static final GCRootDemo gc3 = new GCRootDemo();

  • 本地方法栈中native方法引用的对象

可以概括得出,可作为GC Roots的节点主要在全局性的引用与执行上下文中。要明确的是,tracing gc必须以当前存活的对象集为Roots,因此必须选取确定存活的引用类型对象

GC管理的区域是Java堆,虚拟机栈、方法区和本地方法栈不被GC所管理,因此选用这些区域内引用的对象作为GC Roots,是不会被GC所回收的。其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是GC roots的一部分

3. 四种引用

在前面两种判断垃圾的方法中都提到了引用的概念,JDK1.2以前,Java中的引用定义十分简单:

如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用

这样的定义很纯粹,对象只有被引用和未被引用两种状态,那么一个对象只要没被引用,不管内存够不够都会被回收;
我们更希望的是有这样的对象:在内存空间足够的时候,能保持在内存中,当内存空间进行垃圾回收后还十分紧张的时候就抛弃这部分对象,于是就有了其他几种引用

① 强引用

在代码中最为普遍的,类似Object A = new Object();只有强引用还存在,垃圾回收器就永远不会回收被引用的对象

② 软引用

用来描述还有用但不是必须的对象;如果一个对象被软引用引用,那么只有当系统将要发生内存溢出,才会把他列入第二次垃圾回收的范围(没其他比他强的引用引用它),如果这次回收还没有足够的内存,才会抛出内存溢出异常

这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等

//-Xmx20m 堆内存20M
public class ReferenceTest {
    private static final int _4MB = 4*1024*1024;//1M = 1024KB 1KB = 1024B 1B = 8bit

    public static void main(String[] args) {
//        List<byte[]> list = new ArrayList<>();
//        for(int i = 0; i<5; i++){
//            list.add(new byte[_4MB]);
//            //堆内存20M
//            //Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
//        }
        soft();
    }

    public static void soft(){
        //list --> SoftReference --> byte[]
        List<SoftReference<byte[]>> list = new ArrayList<SoftReference<byte[]>>();
        for(int i = 0; i<5; i++){
            SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束,list.size()= " +list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println("SoftReference: "+ref);
            System.out.println("SoftReference所引用的byte[]: "+ref.get());
        }
    }
}

可以看到我把堆内存设置为20M,之后向list中不断加入大小为4M的byte数组,在加的第五次因为对内存空间不足而发生了OutOfMemoryError

改进这个程序,让list不直接去引用byte数组,而是去引用软引用对象SoftReferenceSoftReference再去软引用byte数组,不断向list中添加引用了数组的软引用对象,打印结果如下

[B@677327b6
1
[B@14ae5a5
2
[B@7f31245a
3
[GC (Allocation Failure)  14199K->13007K(19968K), 0.0016675 secs]
[B@6d6f6e28
4
//将要放第五次的时候堆内存空间不足,于是进行了一次GC
[GC (Allocation Failure) -- 17215K->17215K(19968K), 0.0015455 secs]
[Full GC (Ergonomics)  17215K->17065K(19968K), 0.0049125 secs]
//GC完成后发现堆内存还是不够,于是要去清除软引用对象,即被SoftReference引用的byte数组
[GC (Allocation Failure) -- 17065K->17073K(19968K), 0.0009366 secs]
[Full GC (Allocation Failure)  17073K->663K(14848K), 0.0077677 secs]
[B@135fbaa4
5
循环结束,list.size()= 5
//可以看到,完成以后list中前4个SoftReference所引用的byte数组都因为堆内存空间不足而被释放掉了
SoftReference: java.lang.ref.SoftReference@45ee12a7
SoftReference所引用的byte[]: null
SoftReference: java.lang.ref.SoftReference@330bedb4
SoftReference所引用的byte[]: null
SoftReference: java.lang.ref.SoftReference@2503dbd3
SoftReference所引用的byte[]: null
SoftReference: java.lang.ref.SoftReference@4b67cf4d
SoftReference所引用的byte[]: null
SoftReference: java.lang.ref.SoftReference@7ea987ac
SoftReference所引用的byte[]: [B@135fbaa4

但是我们的前四个软引用对象所引用的对象已经是null了,自然他们没必要再保存在我们的list中了,为了清理这个软引用队列,我们需要使用软引用队列,把软引用关联引用队列,当软引用所关联的对象被回收时,软引用自己会加入queue,之后我们只需要根据queue中的软引用去list中删除就行了
JVM - 垃圾收集器与内存分配策略

public static void softqueue(){
    List<SoftReference<byte[]>> list = new ArrayList<SoftReference<byte[]>>();
    //引用队列
    ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
    for(int i = 0; i<5; i++){
        //关联引用队列,当软引用所关联的对象被回收时,软引用自己会加入queue
        SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[_4MB], queue);
        System.out.println(ref.get());
        list.add(ref);
        System.out.println(list.size());
    }
    Reference<? extends byte[]> poll = queue.poll();
    while (poll!=null){
        list.remove(poll);
        poll = queue.poll();
    }
}
③ 弱引用

强度比软引用更低,被弱引用关联的对象只能存活到下次垃圾收集发生之前,当垃圾收集器开始工作以后,无论内存是否充足都会将其回收;(没其他比他强的引用引用它)

/**
 * 弱引用关联对象何时被回收
 */
public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        //100M的缓存数据
        byte[] cacheData = new byte[100 * 1024 * 1024];
        //将缓存数据用软引用持有
        WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
        System.out.println("第一次GC前" + cacheData);
        System.out.println("第一次GC前" + cacheRef.get());
        //进行一次GC后查看对象的回收情况
        System.gc();
        //等待GC
        Thread.sleep(500);
        System.out.println("第一次GC后" + cacheData);
        System.out.println("第一次GC后" + cacheRef.get());

        //将缓存数据的强引用去除
        cacheData = null;
        System.gc();
        //等待GC
        Thread.sleep(500);
        System.out.println("第二次GC后" + cacheData);
        System.out.println("第二次GC后" + cacheRef.get());
    }
}
第一次GC前[B@7d4991ad
第一次GC前[B@7d4991ad
第一次GC后[B@7d4991ad
第一次GC后[B@7d4991ad
第二次GC后null
第二次GC后null

从上面的代码中可以看出,弱引用关联的对象是否回收取决于这个对象有没有其他强引用指向它。这个确实很难理解,既然弱引用关联对象的存活周期和强引用差不多,那直接用强引用好了,干嘛费用弄出个弱引用呢?

static Map<Object,Object> container = new HashMap<>();
public static void putToContainer(Object key,Object value){
    container.put(key,value);
}

public static void main(String[] args) {
    //某个类中有这样一段代码
    Object key = new Object();
    Object value = new Object();
    putToContainer(key,value);

    //..........
    /**
     * 若干调用层次后程序员发现这个key指向的对象没有用了,
     * 为了节省内存打算把这个对象抛弃,然而下面这个方式真的能把对象回收掉吗?
     * 由于container对象中包含了这个对象的引用,所以这个对象不能按照程序员的意向进行回收.
     * 并且由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。
     * 很可能会造成内存泄漏。
     */
    key = null;
}

我在学习TreadLocal的时候遇到过一次弱引用的使用,他是为了解决内存泄漏问题,其中的HashMap使用弱引用保存键,保证没有其他强引用引用key的时候,key都能被顺利回收;

Java还设计 WeakHashMap类来解决这样的问题

④ 虚引用(幽灵引用)

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外,所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作

4. 回收方法区

Java虚拟机规范中并不要求方法区进行垃圾回收,因为方法区的垃圾回收性价比很低;
方法区如果要进行垃圾回收,主要是回收两部分内容:无用的类和废弃常量
对于废弃常量来说,以常量池中的字面量回收为例,如果没有String对象叫这个,这个字面量就会被移除串池(jdk1.8把串池放到了堆里)

而对于无用的类,需要满足下面三个条件

  • 该类的所有实例都被回收
  • 加载该类的ClassLoader已经被回收
  • 该类对应的Class对象没有在任何地方被引用

5. 垃圾收集算法

5.1 标记-清除算法

最基础的收集算法就是标记清除算法,算法分为标记和清除两个阶段:首先标记处所有需要回收的对象,在标记完成后统一或是所有被标记的对象
JVM - 垃圾收集器与内存分配策略
标记清除算法有两点不足:
① 空间问题:标记清除后会产生大量的不连续的内存碎片,可能会导致以后分配大对象(数组对象,需要连续的内存空间)的时候明明在整个堆内存还够用,而找不到足够大的连续内存无法分配成功

② 效率问题:标记和清除两个过程的效率都不高

5.2 复制算法

复制算法为了解决效率问题,将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次性清理掉;
这样使得每次都是堆半个区域进行内存回收,内存分配时也不用考虑内存碎片的问题

JVM - 垃圾收集器与内存分配策略
优点:
没有内存碎片
缺点:
但是这样将每次使用的内存都缩小到了原来的一半

5.3 标记整理算法

标记整理算法的标记过程和标记清除算法一致,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存
JVM - 垃圾收集器与内存分配策略
优点
这样就没有内存碎片
缺点
但是在整理的过程中涉及到对象的移动,效率不高

5.4 分代的垃圾回收机制

JVM并没有只选用上面的一种算法来进行垃圾回收,而是上面的算法协调合作,根据对象的存活的周期不同将内存划分为几块,每一块使用适当的收集算法;

Java堆被分为新生代和老年代,新生代又分为伊甸园区,幸存区From和幸存区To;
JVM - 垃圾收集器与内存分配策略
新生代
新生代中,每次垃圾回收都有大量死去的对象,所有采用复制算法;

但是新生代中的对象98%都是朝生夕死的,所以并不需要1:1的划分这片内存区域,而是将它划分为一块较大的Eden空间和两块较小的Survivor空间(8:1:1),每次使用Eden和其中的一块Survivor,当回收时,将EdenSurvivor中还存活的对象一次性复制到另一块Survivor空间

结合下面的代码看

//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public class HeapDemo {
    private static final int _512KB = 512*1024;
    private static final int _1MB = 1024*1024;
    private static final int _6MB = 6*1024*1024;
    private static final int _7MB = 7*1024*1024;
    private static final int _8MB = 8*1024*1024;
    
    public static void main(String[] args) {
    	List<byte[]> list = new ArrayList<byte[]>();
        list.add(new byte[_1MB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_7MB]);
    }
}

我给虚拟机指定了20M的堆内存,堆内存的最初使用情况是这样的

Heap
 //新生代 新生代有10M的内存空间,eden+from+to = 10M
 def new generation   total 9216K, used 2163K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  //伊甸园区 由于java程序的启动本身需要实例化类,所以最初伊甸园区就有对象
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee1ce90, 0x00000000ff400000)
  //幸存区FROM 可以看到eden:from:to = 8:1:1
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  //幸存区To
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 //老年代 10M 还未使用
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)

新创建的对象都会首先被分配到Eden区中,对应list.add(new byte[_1MB]);

Heap
 def new generation   total 9216K, used 3013K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  //新的数组对象被放到了eden中
  eden space 8192K,  36% used [0x00000000fec00000, 0x00000000feef1618, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)

Eden放满了就会触发一次Minor GC,利用可达性分析算法找到幸存对象,复制到幸存区To中,然后把幸存的对象的寿命+1,然后幸存区From幸存区To交换位置;这样Eden就空了,可以放置对象;对应list.add(new byte[_6MB]);

[GC (Allocation Failure) [DefNew: 3019K->687K(9216K), 0.0032421 secs] 3019K->1711K(19456K), 0.0032863 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //发生了一次Minor GC
Heap
 def new generation   total 9216K, used 6913K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  76% used [0x00000000fec00000, 0x00000000ff214930, 0x00000000ff400000)
  from space 1024K,  67% used [0x00000000ff500000, 0x00000000ff5abe48, 0x00000000ff600000)//利用可达性分析算法找到幸存对象,复制到`幸存区To`中,然后把幸存的对象的寿命+1,然后`幸存区From`和`幸存区To`交换位置
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 1024K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  10% used [0x00000000ff600000, 0x00000000ff700010, 0x00000000ff700200, 0x0000000100000000)

Eden区域又满了,那么又出触发Minor GC,这次不仅仅是在Eden中找幸存对象,还会在幸存区From中找幸存对象,复制到幸存区To中,对象寿命+1,清理垃圾,交换幸存区From幸存区To的位置

直到对象的生命周期到达了一个阈值(最大是15),或者新生代整个都满了就把对象晋升到老年代去list.add(new byte[_7MB]);

[GC (Allocation Failure) [DefNew: 3016K->687K(9216K), 0.0020489 secs] 3016K->1711K(19456K), 0.0020927 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 6831K->0K(9216K), 0.0055219 secs] 7855K->7850K(19456K), 0.0055553 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 7250K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  88% used [0x00000000fec00000, 0x00000000ff314930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7850K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  76% used [0x00000000ff600000, 0x00000000ffdaa818, 0x00000000ffdaaa00, 0x0000000100000000)

大对象直接晋升到老年代
大对象在老年代空间足够,但是新生代空间不够,不会发生GC而是直接晋升到老年代

STW
注意,在Minor GC的时候会触发一次stop the world,暂停其他用户的线程,当垃圾回收线程结束,其他用户线程才恢复运行,这是因为在Minor GC的时候会涉及到对象的复制移动,为了线程安全,所以暂停其他用户的线程;但是由于复制的对象很少,所以STW的时间很短

老年代
老年代的存活率更高,当老年代也基本满了,会先触发一次Minor GC看新生代能不能是放一部分内存,如果新生代也放不下了,就会触发一次Full GC,进行一次整体的清理,老年代采用的是标记清除或标记整理算法,Full GC也会触发STW

如果内存都不够就会发生OOM,但是一个线程的OOM并不会终止其他线程,因为OOM的线程会把被它占用的堆内存都释放

6. 垃圾收集器

前面说的收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现;虚拟机包含的所有的收集器如下所示,其中所在的区域表示用于新生代还是老年代;连线表示可以搭配使用
JVM - 垃圾收集器与内存分配策略

6.1 串行收集器

  • 是一个单线程的垃圾回收器,这个单线程指的是垃圾回收时必须暂停其他工作线程
  • 适用于堆内存较小的时候,CPU核数少的个人电脑
Serial收集器

Serial收集器是最基本的垃圾回收器,采用复制算法,使用在新生代,它是单线程的,这个单线程指的是垃圾回收时必须暂停其他工作线程

Serial Old收集器

单线程,使用在老年代,使用的是标记整理算法

6.2 吞吐量优先的垃圾回收器

  • 多线程的垃圾回收器
  • 要在单位时间内STW的时间最短
  • 适合于堆内存大,多核CPU
Parallel Scavenge收集器

新生代的收集器,是对线程的,使用多条线程进行垃圾收集,使用的是复制算法,关注点在于达到一个可控的吞吐量,即运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)尽可能短;

高吞吐量可以高效的利用CPU的时间,尽快的完成程序的运算任务

并行的垃圾回收线程默认是和CPU的数量相同,让所有的CPU都去进行垃圾回收

Parallel Scavenge不能和CMS搭配使用

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用标记整理算法

6.3 响应时间优先的垃圾回收器

  • 多线程的垃圾回收器
  • 垃圾回收时单次的STW的时间尽可能短
  • 适合于堆内存大,多核CPU
ParNew收集器

ParNew收集器实际上就是Serial收集器的多线程版,除了使用多条线程进行垃圾收集之外,其他和Serial收集器并无他样,并且关注点是在于尽可能缩短垃圾收集时用户线程的停顿时间

停顿时间越短越适合需要与用户交换的程序,良好的响应速度可以提升用户体验

ParNew收集器可以可以配合CMS使用

CMS收集器

CMS收集器(Concurrent Mark Sweep)是第一款真正意义上的并发收集器,用在老年代,能让垃圾回收线程和用户线程同时工作,且是一种以获取最短回收停顿时间为目标的收集器

CMS收集器基于标记清除算法的,整体运作包括四个部分:
① 初始标记: 需要STW,只是标记以下GC Roots能直接关联到的对象,速度很快

② 并发标记: 进行GC Roots Tracing,这部分耗时最长,但是不需要STW,和其他线程一起工作

③ 重新标记: 需要SWT,为的是修正并发标记期间变动的对象

④ 并发清除

可以看到最耗时的GC Roots Tracing是在并发标记阶段和用户线程一同进行的,整体效率高

但是其有三个缺点:
① 对CPU资源敏感,并发阶段会占用线程导致总吞吐量降低,而导致应用程序变慢

② 无法处理浮动垃圾,在并发清除的过程中,用户线程也在执行,会产生新的垃圾,CMS无法在这次进行清除,而要等到下次垃圾回收;同时因为会产生浮动垃圾,所以并不能等到老年代满了再进行垃圾回收,而是要预留一部分内存

③ 使用的是标记清除的算法,会有大量的不连续空间的出现

6.4 G1垃圾回收器

  • 同时注重吞吐量和低延迟
  • 适合超大堆内存,化整为零,把堆划分为多个大小相等的Reigon
  • 整体上是标记整理算法,两区域之间是复制算法
  • 可预测停顿,设置目标,能够让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒

之前说到的收集器进收集的范围都是整个新生代或者老年代,而G1不再是这样,使用G1收集器的时候,Java堆的内存布局就与其他的收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Reigon)虽然还保留着新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分Reigon(不需要连续)的集合
JVM - 垃圾收集器与内存分配策略
G1之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个堆中进行全区域的垃圾收集;G1跟踪各个Reigon中的垃圾堆积的价值大小(回收所获得空间大小以及回收所需要的实践经验值),在后台维护一个优先队列,每次根据允许的收集时间,优先回收价值最大的Reigon(正是因为每次先回收价值最大的,所以叫Garbage First);

新生代对象的跨代引用问题
在我们判断一个新生代的对象是否存活的时候,要去判断这个对象和GC Roots之间有没有引用链,自然我们要去找到GC Roots对象,对于GC Roots对象,他不一定和当前对象在同一个Region中,他有可能存放在老年代,老年代中对象众多,遍历整个空间区寻找GC Roots自然效率低下;在G1收集器使用了Remembered Set来解决这个问题

把老年代再进行一个划分,划分为一个一个card,每个card是512K,如果老年代的一个card中的对象引用了新生代的对象,就把这个card标记为脏卡,将来进行GC Roots遍历的时候只需要遍历脏卡就行了;

在新生代这边有一个Remembered Set记录外部都有谁对我引用,即谁对我进行了引用,之后再判断新生代对象是否存活的时候,只需要遍历Remembered Set记录中的GC Roots就行(典型的空间换时间的操作)
JVM - 垃圾收集器与内存分配策略

G1 垃圾回收阶段
G1 垃圾收集器介绍