JVM垃圾回收 - 《深入理解Java虚拟机》读书笔记

对象访问

句柄访问:Java堆中将会划分出一块内存来作为句柄池,Java栈本地变量表reference中存储的就是对象的句柄地址,而句柄地址包含了对象实例数据和类型数据各自的具体地址信息。

直接指针访问:Java栈本地变量表reference直接存储的是对象地址。

判断对象是否需要被回收算法:

1. 引用计数算法(Reference Counting):

给对象中添加一个引用计数器,每当有一个地方引用时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器都为0的对象就是不可能再被使用的。

弊端:很难解决对象之间的相互循环引用的问题,互相引用导致计数都不为0.

2. 根搜索算法(GC Roots Tracing):

通过一系列的名为“GC Roots”的对象最为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,则证明此对象是不可用的。

GC Roots对象:

虚拟机栈(栈帧中的本地变量表)中的引用的对象

方法区中的类静态属性引用的对象

方法区中的常量引用的对象

本地方法栈中JNI(即Native方法)的引用的对象

 

引用分类:

1. 强引用:只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

2. 软引用:系统将要发生内存溢出异常之前,将会把这些对象裂锦回收范围之中并进行第二次回收

3. 弱引用:只能生存岛下一次垃圾收集发生之前

4. 虚引用:虚引用完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例

垃圾收集算法:

1. 标记-清除算法(Mark-Sweep):

标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

弊端: 效率问题,标记和清除过程效率都不高;空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当要分配一个较大对象时无法找到足够且连续的内存而不得不提前触发GC

 

 

2. 复制算法(Copying):

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。Eden(伊甸区)、Survivor1(存活区1)、Survivor2(存活区2)三块,比例为8:1:1(HotSpot),每次使用其中的Eden+Survivor1或者使用Eden+Survivor2,当GC时,将Eden及Survivor1中的存活对象拷贝到Survivor2中,然后清理掉Eden+Survivor1。由永久代(老年代)进行分配担保。

3. 标记整理算法(Mark-Compact):

让所有存货的对象都向一端移动,然后直接清理掉端边界以外的内存

 

4. 分代收集算法(Generational Collection):

根据对象的存货周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代,新生代采用复制算法,老年代存活率搞,没有额外空间进行分配担保,则必须使用标记清理或标记整理算法。

 

垃圾收集器:

Serial收集器:新生代采用复制算法,单线程收集器,暂停所有用户线程。

Serial Old 收集器:Serial收集器的老年代版本,单线程收集器,使用标记-整理算法,暂停所有用户线程。

ParNew收集器:新生代采用复制算法,暂停所有用户线程,多线程收集器,暂停所有用户线程。

Paraller Scavenge收集器:新生代收集器,采用复制算法,使用多线程收集器,关注GC时的停顿时间,目标是达到一个可控制的吞吐量(Throughput)(CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),可直接控制最大垃圾收集停顿时间(-XX:MaxGCPauseMillis)及吞吐量大小(-XX:GCTimeRatio)。

Paraller Old收集器:Paraller Scavenge收集器老年代版本。使用多线程标记-整理算法(JDK1.6开始提供)。

CMS(Concurrent Mark Sweep)收集器:也被称为并发低停顿收集器,是一种以获取最短回收停顿时间为目标的收集器,侧重于服务响应速度,采用标记-清除算法。分为4个步骤:

  初始标记(CMS initial mark):暂停用户所有线程

  并发标记(CMS concurrent mark):

  重新标记(CMS remark):暂停用户所有线程

  并发清除(CMS concurrent sweep):

G1(Garbage First)收集器:采用标记-整理算法,可以精确控制停顿。G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

 

对象分配:

将Jvm参数设置为:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails

可看到如下日志:

Heap
  PSYoungGen total 9216K, used 5162K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    eden space 8192K, 63% used [0x00000000ff600000,0x00000000ffb0abb8,0x00000000ffe00000)
    from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
    to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
  Metaspace used 3419K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 371K, capacity 388K, committed 512K, reserved 1048576K

 

执行如下代码后:

public class JvmGCTest {

    private static int _1MB = 1024*1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }
}

输出结果为:

[GC (Allocation Failure) [PSYoungGen: 7209K->1002K(9216K)] 7209K->3814K(19456K), 0.0014217 secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
Heap
  PSYoungGen total 9216K, used 5336K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    eden space 8192K, 52% used [0x00000000ff600000,0x00000000ffa3ba28,0x00000000ffe00000)
    from space 1024K, 97% used [0x00000000ffe00000,0x00000000ffefa8b0,0x00000000fff00000)
    to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  ParOldGen total 10240K, used 6907K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    object space 10240K, 67% used [0x00000000fec00000,0x00000000ff2befe8,0x00000000ff600000)
  Metaspace used 3512K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 378K, capacity 388K, committed 512K, reserved 1048576K

1. 对象优先分配在Eden:对象创建时优先分配在Eden,当没有足够空间时,将发起一次Minor GC,其中Eden依然存活的对象移到Survior其中的from,当Survivor空间不足以放入移动的对象时,则将对象提前放到老年代。

2. 大对象直接进入老年代:大对象是指,需要大量连续内存空间的Java对象,如大字符串及数组。-XX:PretenureSizeThreshold可设置具体值,大于此值即可直接分配到老年代,避免新生代发生大量的内存拷贝。

3. 长期存活的对象将进入老年代:对象在Survivor中每经历过一次Minor GC,对象年龄(Age)计数器(从Eden转移到Survivor时初始化为1岁)便会加1岁,当年龄增加到一定程度(默认15岁),就会被晋升到老年代中。对象晋升的年龄由参数-XX:MaxTenuringThreshold来设置

4. 动态对象年龄判定: 虚拟机并不总是要求对象的年龄必须达到MaxTenuingThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

5. 空间分配担保: 在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC;如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,则只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。

图片待补充

-- 摘自《深入理解Java虚拟机:JVM高级特性与最佳实践》

 

posted @ 2020-09-27 14:54  Loser100  阅读(115)  评论(0)    收藏  举报