JVM(二)——对象与垃圾回收策略

前言

上一篇博客只是对内存区域做了一个简单的描述,如果想要更深入的理解,其实还远远不够,只需要明确堆与栈的区别即可。这一篇博客就从对象出发,总结对象在堆上分配的几个原则,同时总结一下JVM的垃圾回收策略,对于具体的垃圾回收器在后续中进行总结。

对象在堆上的表现

通过总结对象在堆上的表现,正好可以复习一下堆的各个组成区间,并引入一些垃圾回收算法。访问对象是常有的事情,即使是最简单的对象访问也会涉及Java栈,Java堆和方法区三个最重要的内存区域。

对象的结构

还是从之前总结的JVM内存模型出发,之前说过——在本地变量表(位于JVM stack的栈帧中)中,只有8中基本的数据类型,还有一种就是引用数据类型,这种数据类型指向了堆中的一块地址,这块堆中的地址就存储这对象的实例数据,但是还有一句话,对象的描述信息存储与方法区,所以我们可以大致描绘一个以下的图(出自:https://blog.csdn.net/zqz_zqz/article/details/70246212

所以我们可以看出,对象的组成似乎分为了两个部分,一个是具体的数据部分(对象实例)存在堆上一个就是这个对象的描述部分(类元信息) 存放在方法区。依旧是如上图所示,堆上的对象实例必须有一个指针指向在方法区上的元信息,否则怎么知道这个对象是啥子类型的。同时,我们是否还能记起,似乎每一个对象都能作为synchronized的锁呢?这个也和对象的结构有关系(参见之前的博客:偏向锁,轻量级锁,重量级锁)。其实一个对象的信息分为对象头,实例数据和对其填充三个部分

对象头

对象头又分为Mark word,指向类信息的指针,数组长度(只有数组对象才有),在上一篇博客中介绍过mark word 如下所示:

è¿éåå¾çæè¿°

这里不解释Mark word的具体信息。指向类信息的指针——即是指向Java对象的类数据保存在方法区。对其填充只是为了对齐相应的字段长度。

对象的访问定位

之前提到过,在操作数栈通过一个引用类型去获取对象信息和数据,但是针对访问方式其实并没有具体的探讨,在《深入理解Java虚拟机》一书中,针对这个问题做过总结——主流的访问方式又两种:使用句柄和直接指针的方式访问

使用句柄

这种方式其实就是在java堆中划分出一块内存来作为句柄池,reference中存储的就是句柄地址,并且句柄中包含了对象的实例数据和类型数据的地址。

扫描二维码关注公众号,回复: 9011889 查看本文章

直接指针

 这种方式,reference类型中存储的就是对象在堆中的地址。

两种对象访问的方式都有优势,第一种:对象地址变化了,reference不用变更,维护起来比较方便。针对第二种:最大的好处就是访问速度更快。HotSpot采用的是第二种,因此之前我们介绍对象信息的时候,也已HotSpot为例。

对象在堆内存中的分配策略

对象的分配,往大方向上将其实就是在堆上分配对象,对象分配的规则不是100%固定的,这个和垃圾收集器和内存大小的参数有关

1、优先在Eden区域分配

实例代码:

/**
 * autor:liman
 * createtime:2019/11/26
 * comment: 对象优先分配在eden区
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC 
 */
public class AllocationEden {

    private static final int _1MB = 1024*1024;

    public static void main(String[] args) {
        byte[] allocation01,allocation02,allocation03,allocation04;
        allocation01 = new byte[2*_1MB];
        allocation02 = new byte[2*_1MB];
        allocation03 = new byte[2*_1MB];
        allocation04 = new byte[4*_1MB];//这里会发生一次GC,分配失败
    }

}

关于如何看懂GC日志,这个可以参看大牛的这一篇博客——看懂GC日志

配置如下参数:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

运行之后的GC日志如下:

[GC (Allocation Failure) [DefNew: 8134K->657K(9216K), 0.0050231 secs] 8134K->6801K(19456K), 0.0050633 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4919K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0297c0, 0x00000000ff400000)
  from space 1024K,  64% used [0x00000000ff500000, 0x00000000ff5a4790, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 367K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

配置的堆大小为20M,且不可扩展,一半分配给了新生代,一半分配给了老年代。新生代按照8:1:1的比列分配给eden区,from,to区,按照优先分配的原则,在1~3行代码中,都会分配在eden区。在分配allocation4的时候所需要的4MB空间的时候,发现eden区不够了,这个时候触发MinorGC(年轻代的GC) ,GC的时候发现前面的三个allocation对象在survivor区域中也无法放下,所以就只好把这三个对象放到老年代去,GC结束之后,allocation4被放到eden区,老年代被allocation1~3占用

2、大对象直接进入老年代

虚拟机提供了一个PretenureSizeThreshold的参数,这个参数指定一个大小,大于这个大小的对象会被直接被分配到老年代(这个参数只对Serial和ParNew垃圾收集器有效,垃圾收集器会在后面进行总结)

实例代码:

这里在上一个实例的基础上加入了PretenureSizeThreshold的参数,指定其为3M大小,我们分配的allocation数组大于3M因此会直接进入老年代。

/**
 * autor:liman
 * createtime:2019/11/26
 * comment: 大对象直接进入老年代
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
 * PretenureSizeThreshold这个参数只对Serial和ParNew两款垃圾收集器有效
 */
public class PretenureSizeThresholdDemo {

    private static final int _1MB = 1024*1024;

    public static void main(String[] args) {
        byte[] allocation;
        allocation=new byte[4*_1MB];
    }
}

运行后的GC日志如下所示:

 可以看到新生代几乎没有使用,老年代却使用了40%。

Heap
 def new generation   total 9216K, used 2154K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee1aa88, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 3433K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 373K, capacity 388K, committed 512K, reserved 1048576K

3、长期存活的对象会进入老年代

虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在eden出生,并经过一次Minor GC 之后仍然存活,并且能被Survivor容纳,则将其移动到Survivor空间中,并且这个时候年龄计数器+1经历过一次Minor GC,对象的年龄计数器+1,当这个计数器大于-XX:MaxTenuringThreshold时,则会晋升到老年代中。

这个直接先上实例:

/**
 * autor:liman
 * createtime:2019/11/26
 * comment: 验证长期存活的对象进入老年代
 * -verbose:gc -Xms80M -Xmx80M -Xmn40M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 * 对象的元数据会占用一些空间,需要将堆空间分配的足够大
 */
public class TenuringThreshold {

    private static final int _1MB = 1024 * 1024;

    @SuppressWarnings("unused")
    public static void main(String[] args) {
        byte[] allocation01, allocation02, allocation03,allocation04;
        allocation01 = new byte[_1MB ];
        allocation02 = new byte[16 * _1MB];
        allocation03 = new byte[16 * _1MB];  //这里发生第一次GC
        allocation03 = null;
        allocation03 = new byte[16 * _1MB];  //这里发生第二次GC
    }

}

 -XX:MaxTenuringThreshold=1时的GC日志:

[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 1 (max 1)
- age   1:    1731256 bytes,    1731256 total
: 20686K->1690K(36864K), 0.0100287 secs] 20686K->18074K(77824K), 0.0100809 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 1 (max 1)
: 18074K->0K(36864K), 0.0016417 secs] 34458K->18062K(77824K), 0.0016683 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 36864K, used 16712K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
  eden space 32768K,  51% used [0x00000000fb000000, 0x00000000fc052040, 0x00000000fd000000)
  from space 4096K,   0% used [0x00000000fd000000, 0x00000000fd000000, 0x00000000fd400000)
  to   space 4096K,   0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
 tenured generation   total 40960K, used 18062K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
   the space 40960K,  44% used [0x00000000fd800000, 0x00000000fe9a3b78, 0x00000000fe9a3c00, 0x0000000100000000)
 Metaspace       used 3440K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 374K, capacity 388K, committed 512K, reserved 1048576K

可以看到from/to空间的使用率为0

  -XX:MaxTenuringThreshold=15时的GC日志:

[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   1:    1706376 bytes,    1706376 total
: 20030K->1666K(36864K), 0.0127152 secs] 20030K->18050K(77824K), 0.0127653 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   1:      25504 bytes,      25504 total
- age   2:    1693592 bytes,    1719096 total
: 18378K->1678K(36864K), 0.0021712 secs] 34762K->18062K(77824K), 0.0021943 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 36864K, used 20029K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
  eden space 32768K,  56% used [0x00000000fb000000, 0x00000000fc1eba08, 0x00000000fd000000)
  from space 4096K,  40% used [0x00000000fd000000, 0x00000000fd1a3b38, 0x00000000fd400000)
  to   space 4096K,   0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
 tenured generation   total 40960K, used 16384K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
   the space 40960K,  40% used [0x00000000fd800000, 0x00000000fe800010, 0x00000000fe800200, 0x0000000100000000)
 Metaspace       used 3438K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 374K, capacity 388K, committed 512K, reserved 1048576K

这个时候发现from区域并不为空,因为allocation01并没有进入到老年代。 

4、动态对象年龄判定

虚拟机并不永远要求对象的年龄达到-XX:MaxTenuringThreshold设定的值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,则大于等于该年龄的对象,都会进入到老年代。这个时候并不会理会XX:MaxTenuringThreshold设定的参数

如下代码:

/**
 * autor:liman
 * createtime:2019/11/26
 * comment: 验证长期存活的对象进入老年代
 * -verbose:gc -Xms80M -Xmx80M  -Xmn40M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 */
public class TenuringThresholdAge {

    private static final int _1MB = 1024 * 1024;

    @SuppressWarnings("unused")
    public static void main(String[] args) {
        byte[] allocation01, allocation02, allocation03,allocation04;
        allocation01 = new byte[_1MB];
        allocation02 = new byte[_1MB ];
        allocation03 = new byte[16 * _1MB];  //这里发生第一次GC
        allocation04 = new byte[16 * _1MB];  //这里发生第一次GC

        allocation04 = null;
        allocation04 = new byte[16 * _1MB];  //这里发生第二次GC
    }

}

将-XX:MaxTenuringThreshold设置为15的GC日志如下:

[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 1 (max 15)
- age   1:    2779840 bytes,    2779840 total
: 21710K->2714K(36864K), 0.0113288 secs] 21710K->19098K(77824K), 0.0113711 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
: 19098K->0K(36864K), 0.0020399 secs] 35482K->19086K(77824K), 0.0020638 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 36864K, used 16712K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
  eden space 32768K,  51% used [0x00000000fb000000, 0x00000000fc052040, 0x00000000fd000000)
  from space 4096K,   0% used [0x00000000fd000000, 0x00000000fd000000, 0x00000000fd400000)
  to   space 4096K,   0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
 tenured generation   total 40960K, used 19086K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
   the space 40960K,  46% used [0x00000000fd800000, 0x00000000feaa3b78, 0x00000000feaa3c00, 0x0000000100000000)
 Metaspace       used 3438K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 374K, capacity 388K, committed 512K, reserved 1048576K

发现from区并没有被使用,老年代却有46%被使用。

5、空间分配担保

从名字可以看出来,所谓的空间分配担保,无非就是老年代对新生代分配不了的对象做一个担保。《深入理解Java虚拟机》一书中对这个模块做了写总结,但是觉得写得有点混乱,其实只是不同的垃圾收集器,担保策略不同而已。

在执行任何一次Minor GC,JVM会检查一下老年代可用的内存空间,是否大于新生代所有对象的总大小(因为有可能一次GC之后,新生代所有的对象都存活下来了,都需要进入老年代,如果老年代没有足够的空间承接这些对象,那不就尴尬了)。如果发现大于,则放心GC如果不大于,这个时候就需要看一个-XX:-HandlerPromotionFailure的参数是否设置了,如果有这个参数,就会进行下一步判断,看老年代可用的内存大小是否大于之前每一次Minor GC后进入老年代的对象的平均大小。如果大于则冒险直接来一次Minor GC ,如果小于则先来一次Full GC (老年代的垃圾回收)然后在Minor GC,如果还是没有足够的空间,则抛出OOM。整个过程文字表述太臃肿,还是来图吧

这里直接上代码示例

/**
 * author:liman
 * createtime:2019/11/27
 */
public class HandlerPromotionTest {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation01, allocation02, allocation03, allocation04, allocation05, allocation06, allocation07;
        allocation01 = new byte[8 * _1MB];
        allocation02 = new byte[8 * _1MB];
        allocation03 = new byte[8 * _1MB];
        allocation01 = null;
        allocation04 = new byte[8 * _1MB];  //这里会发生GC,这次GC之后,01被回收,02,03号对象进入到老年代,04被分到eden区
        allocation05 = new byte[8 * _1MB];
        allocation06 = new byte[8 * _1MB];
        allocation04 = null;
        allocation05 = null;
        allocation06 = null;
        allocation07 = new byte[8 * _1MB];  //这里会发生第二次GC,GC完成之后,07直接分配在了新生代
    }
}

p.s:HandlePromotionFailure在JDK 1.5之前是默认关闭的,需要手动指定,在1.6之后就默认开启了。 

[GC (Allocation Failure) [DefNew: 27854K->728K(36864K), 0.0215016 secs] 27854K->17112K(77824K), 0.0475125 secs] [Times: user=0.00 sys=0.01, real=0.05 secs] 
[GC (Allocation Failure) [DefNew: 25936K->724K(36864K), 0.0037041 secs] 42320K->17108K(77824K), 0.0037575 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 36864K, used 9899K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
  eden space 32768K,  28% used [0x00000000fb000000, 0x00000000fb8f5dc8, 0x00000000fd000000)
  from space 4096K,  17% used [0x00000000fd000000, 0x00000000fd0b51d8, 0x00000000fd400000)
  to   space 4096K,   0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
 tenured generation   total 40960K, used 16384K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
   the space 40960K,  40% used [0x00000000fd800000, 0x00000000fe800020, 0x00000000fe800200, 0x0000000100000000)
 Metaspace       used 3451K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K

 最终两次GC之后,02和03号对象进入老年代,所占空间比例为8*2/40=40%符合预期(详细的GC步骤分析在代码中)

一个小结

这一部分是参考的《深入理解Java虚拟机》一书,感觉写的有些零散,这里做一些总结,大体对象分配流程如下:

上图中在经历survivor分配,进入老年代还有可能是对象年龄计数器达到阈值,则进入老年代。

垃圾收集的一些思想

垃圾收集过程其实包含两个大的过程,一个是标记,一个是清除。标记——既该回收的对象给其打上标记,清除就是释放该对象所在的内存空间。

标记对象的算法

其实有些资料都会提到应用计数和根搜索的方法,大体上也只有这两种。

引用计数

给对象中增加一个引用计数器,每当有一个地方引用它的时候,计数器+1,当引用失效的时候,计数器-1,任何时刻计数器为0的对象就会被回收。这种方式实现起来非常简单,但是很难解决循环依赖的问题(这里不再赘述)。但是提到引用这个概念,其实就得好好探讨一下了。

到底有几种引用

如下所示,根据局部变量表中对堆对象引用的强度分为强引用,软引用,弱引用和虚引用。后面会针对软引用,弱引用和虚引用举一些实例,这些实例可以不用深入,只需要理解各自垃圾回收时的表现即可。

强引用

我们通常所谓新建对象的赋值语句,即为强引用。

强引用可以直接访问目标对象,强引用所指的对象在任何时候都不会被回收,宁愿抛出OOM也不会回收。

StringBuffer str = new StringBuffer("hello world");

软引用

当堆内存空间不足的时候,就会被回收。

import java.lang.ref.SoftReference;

/**
 * autor:liman
 * createtime:2019/11/28
 * comment:软引用实例
 * -Xms16M -Xmx16M -Xmn8M -XX:SurvivorRatio=8
 * 软引用:被GC标记之后,如果内存分配不足的时候才会被回收
 */
public class SoftRefDemo {

    public static class User{
        public User(int id,String name){
            this.id = id;
            this.name=name;
        }

        private int id;
        private String name;

        private byte[] data = new byte[1024*1024];

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        User u = new User(1,"liman");
        SoftReference<User> userSoftReference = new SoftReference<User>(u);//构建一个软引用
        u = null;   //去除强引用

        System.out.println(userSoftReference.get()); //从软引用中获取对象
        System.gc();    //进行第一次GC
        System.out.println("After GC:");
        System.out.println(userSoftReference.get());    //垃圾回收之后,从软引用中获取对象

        byte[] b = new byte[1024*1024*7];    //分配大对象,系统会自动进行GC
        System.gc();
        System.out.println(userSoftReference.get());    //还是从软引用中获得对象
    }

}

弱引用

发现之后,不管如何都会被GC回收

import java.lang.ref.WeakReference;

/**
 * autor:liman
 * createtime:2019/11/28
 * comment: 弱引用实例
 * -Xms16M -Xmx16M -Xmn8M -XX:SurvivorRatio=8
 * 弱引用,发现之后,不管如何都会被GC回收
 */
public class WeakReferenceDemo {

    public static class User{
        public User(int id,String name){
            this.id = id;
            this.name=name;
        }

        private int id;
        private String name;

        private byte[] data = new byte[1024*1024];

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        User u = new User(1,"liman");
        WeakReference<User> userWeakReference = new WeakReference<User>(u);
        u = null;
        System.out.println(userWeakReference.get());
        System.gc();
        System.out.println("After GC:");
        System.out.println(userWeakReference.get());
    }

}

虚引用

和几乎没有引用一样,随时都可以被回收

import java.lang.ref.WeakReference;

/**
 * autor:liman
 * createtime:2019/11/28
 * comment: 虚引用实例
 * -Xms16M -Xmx16M -Xmn8M -XX:SurvivorRatio=8
 * 虚引用,随时可被GC回收
 */
public class WeakReferenceDemo {

    public static class User{
        public User(int id,String name){
            this.id = id;
            this.name=name;
        }

        private int id;
        private String name;

        private byte[] data = new byte[1024*1024];

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        User u = new User(1,"liman");
        WeakReference<User> userWeakReference = new WeakReference<User>(u);
        u = null;
        System.out.println(userWeakReference.get());
        System.gc();
        System.out.println("After GC:");
        System.out.println(userWeakReference.get());
    }

}

可达性分析

通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径为引用链,当一个对象引用链表示这个对象不可达(既从根节点无法到达该节点,类似树形结构的遍历),则该对象会被标记为待回收对象。

那这里我们再进一步思考一下,是不是只要不会被回收的对象就可以作为GC Roots对象,实时证明确实如此,懒得打字了直接上图吧

清除对象的算法

这里并不是真正意义上的算法,应该说是一种垃圾回收的思想。同时需要明确的是,没有一个垃圾回收算法是十全十美的,只有最合适的

标记清除

标记清除是垃圾回收的思想基础,标记清除将垃圾回收分为两个阶段——标记和清除,在标记阶段标记可达的对象,这个阶段完成之后,没被标记的对象就是垃圾对象。在清除阶段将这些对象回收。

缺点:回收后的空间是不连续的,这对大对象的分配很不友好。

复制

复制算法的思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。(上一篇博客中对堆进行了详细的划分,其中的from/to空间采用的就是复制回收算法)

优点:不会有碎片,效率较高

缺点:系统空间使用率折半

标记整理

前面两种算法常用于新生代,但是对于老年代,更常见的是大部分对象都是存活的,如果依然使用复制算法,由于存活对象较多,复制的成本会很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法——这就是标记整理(标记压缩)算法。

标记整理算法在标记清除算法的基础上进行了优化,和标记算法一样,标记整理算法也会先标记需要回收的对象,之后清理边界外所有的空间,这种方法即避免了碎片的产生,不需要两块相同的内存空间了,因此,其性价比比较高。

分代算法

分代算法,并不是一个具体的算法,而是将内存划分为几个区针对不同的区采用不同的算法,这里承接上一篇博客最后内存区域的划分图如下:这里补充说明一下,新生代中默认eden区和from/to区的比例为8:1:1

总结

本篇博客其实是对《深入理解Java虚拟机》和《实战Java虚拟机》两本书相关内容的一个总结,主要总结了对象分配的几种可能,同时小结了一下垃圾回收的几种基础思想,这个是后面总结垃圾回收器的重点。

这里顺便总结一下对象会进入老年代的几种可能情况

1.躲过指定的gc次数,达到高龄之后进入老年代.这个高龄由一些参数指定,默认为15。

2.动态年龄判定规则,如果Survivor区域内年龄1+年龄2+年龄3+年龄n的对象总和大于Survivor区的50%,此时年龄n以上的对象会进入老年代,不一定要达到15岁.

3.对象过大,直接进入老年代

4.一次Minor GC之后,对象存活过多,survivor区容纳不下,则这些对象直接进入老年区

参考资料

《实战JAVA虚拟机  JVM故障诊断与性能优化》

《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》

目前看来,这两本书比网上90%讲JVM的博客质量都要好。

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/103245838
今日推荐