Java基础之《JVM性能调优(6)—对象死亡算法》

一、如何判断一个对象的死亡

1、一个对象的死亡
当一个对象已经不再被任何的存活对象继续引用时(即没人用),就可以宣判为已经死亡,判断对象存活的算法一般有两种:引用计数算法和可达性分析算法。

2、什么是引用计数器
引用计数器算法(Reference Counting)比较简单,对每个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效后,引用计数器就减1。
只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

3、该算法的优缺点
优点:
(1)实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟性。
缺点:
(1)它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
(2)每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
(3)引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

4、什么是循环引用

(1)创建对象a,存储在堆中,main方法的栈帧中的局部变量a引用了堆空间Test01实例a地址。
(2)创建对象b,存储在堆中,main方法的栈帧中的局部变量b引用了堆空间Test01实例b地址。
(3)Test01实例a的obj引用了Test01实例b地址。
(4)Test01实例b的obj引用了Test01实例a地址。

5、循环引用会有什么问题
 
当执行a=null; b=null; 时,就变成如图,将会导致堆循环相互引用,从而导致gc回收失败。

二、Java是否采用了引用计数算法

1、例子

package deathAlgorithm;

public class Test01 {
    private Test01 obj = null;

    byte[] bytes = new byte[2*1024*1024];

    public static void main(String[] args) {
        //一:直接引用
        //a引用实例a,所以实例a引用数为1
        Test01 a = new Test01();
        //b引用实例b,所以实例b引用数为1
        Test01 b = new Test01();

        //二:相互引用
        //a.obj引用了实例b,故实例b引用数+1=2
        a.obj = b;
        //b.obj引用了实例a,故实例a引用数+1=2
        b.obj = a;

        //三:删除引用
        a = null;
        b = null;

        //四:垃圾清除,如果采用引用算法的话,会导致内存无法释放
        System.gc();
    }
}

2、设置参数
-Xms30M
-Xmx30M
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC

执行结果:

{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 9216K, used 6125K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 74% used [0x00000000ff600000,0x00000000ffbfb6c8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 20480K, used 0K [0x00000000fe200000, 0x00000000ff600000, 0x00000000ff600000)
  object space 20480K, 0% used [0x00000000fe200000,0x00000000fe200000,0x00000000ff600000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K
2022-02-03T16:40:57.367+0800: [GC (System.gc()) [PSYoungGen: 6125K->776K(9216K)] 6125K->784K(29696K), 0.0012983 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 9216K, used 776K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
  from space 1024K, 75% used [0x00000000ffe00000,0x00000000ffec2020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 20480K, used 8K [0x00000000fe200000, 0x00000000ff600000, 0x00000000ff600000)
  object space 20480K, 0% used [0x00000000fe200000,0x00000000fe202000,0x00000000ff600000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K
}
{Heap before GC invocations=2 (full 1):
 PSYoungGen      total 9216K, used 776K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
  from space 1024K, 75% used [0x00000000ffe00000,0x00000000ffec2020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 20480K, used 8K [0x00000000fe200000, 0x00000000ff600000, 0x00000000ff600000)
  object space 20480K, 0% used [0x00000000fe200000,0x00000000fe202000,0x00000000ff600000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K
2022-02-03T16:40:57.368+0800: [Full GC (System.gc()) [PSYoungGen: 776K->0K(9216K)] [ParOldGen: 8K->617K(20480K)] 784K->617K(29696K), [Metaspace: 3251K->3251K(1056768K)], 0.0031266 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
Heap after GC invocations=2 (full 1):
 PSYoungGen      total 9216K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 20480K, used 617K [0x00000000fe200000, 0x00000000ff600000, 0x00000000ff600000)
  object space 20480K, 3% used [0x00000000fe200000,0x00000000fe29a700,0x00000000ff600000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K
}
Heap
 PSYoungGen      total 9216K, used 166K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 2% used [0x00000000ff600000,0x00000000ff629aa0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 20480K, used 617K [0x00000000fe200000, 0x00000000ff600000, 0x00000000ff600000)
  object space 20480K, 3% used [0x00000000fe200000,0x00000000fe29a700,0x00000000ff600000)
 Metaspace       used 3272K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 354K, capacity 388K, committed 512K, reserved 1048576K

gc前年轻代使用了used 6125K,gc后年轻代使用了used 776K,说明Java没有使用引用计数算法

三、什么是可达性分析算法

1、可达性分析算法
(1)可达性分析算法也称为引用链法(GCRoots),它的核心目的就是判断java对象是否存活?
(2)相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

2、什么是GCRoots
所谓“GCRoots”,或者说tracingGC的“根集合”就是一组必须活跃的引用。

3、什么是可达性分析
(1)基本思路就是通过一系列名为“GCRoots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GCRoots没有任何引用链相连时,则说明此对象不可用。
(2)也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的就自然被判定为不可达(暂时处于“缓刑”阶段)。

四、哪些对象可以作为GCRoots

1、什么是GCRoots

第1类:栈帧
虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
第2类:方法区
方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
第3类:本地方法栈
本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

2、详细

(1)reference_a、reference_b、reference_c都是GCRoots
他们和堆的引用关系如下:
reference_a -> object_a -> object_d
reference_b -> object_b
reference_c -> object_c
通过引用关系,可以得出object_a、object_b、object_c、object_d都是GCRoots可达性(存活对象),不会被GC回收。
(2)简单总结:可达=存活=不可gc回收
(3)但是对于object_e、object_f没有GCRoots相连,他们便是不可达对象。
没有任何引用链相连 = GCRoot到对象不可达 = 对象不可用

五、可达性分析算法如何判定一个对象是否死亡


1、可达性分析
在可达性分析算法中不可达的对象(例如前面的object_e、object_f对象为不可达),它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,需要经历2次标记过程。

2、是否可达
触发标记的前提是:不可达的对象,对象没有与GCRoots相连接的引用链。

3、第一次标记
筛选的条件:此对象是否有必要执行finalize()方法。(用来复活一个对象)
(a)若有必要执行(人为设置),则筛选出来,进入下一阶段(第二次标记&筛选)
(b)若没必要执行,判断该对象死亡,不筛选并等待回收
特别说明:当对象无finalize()方法或finalize()已被虚拟机调用过,则视为“没必要执行”

4、第二次标记
经过第一次标记后,这个对象被判定为有必要执行finalize()方法。
这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。
这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。

5、为什么要用F-Queue队列,而且还要用Finalizer线程去执行?
原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

六、如何把一个死亡对象复活

1、例子FinalizerTest.java

package deathAlgorithm;

public class FinalizerTest {
    public static FinalizerTest obj = null;

    public static void main(String[] args) throws InterruptedException {
        obj = new FinalizerTest();
        obj = null;

        //这两句保证进行垃圾回收标记,finalize能进行挽救自己
        System.gc();
        //因为finalize方法优先级很低,所以暂停1秒等待它
        Thread.sleep(1000);

        if (obj == null) {
            System.out.println("obj 死亡1");
        } else {
            //这里被执行,成功挽救自己
            System.out.println("obj 存活1");
        }

        //一样的代码
        obj = null;

        //这两句保证进行垃圾回收标记,finalize能进行挽救自己
        System.gc();
        //因为finalize方法优先级很低,所以暂停1秒等待它
        Thread.sleep(1000);

        if (obj == null) {
            //这里被执行,无法挽救,finalize只能执行一次
            System.out.println("obj 死亡2");
        } else {
            System.out.println("obj 存活2");
        }

        Thread.sleep(10 * 1000);
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        obj = this;
        System.out.println("触发 finalize");
    }
}

2、执行结果

触发 finalize
obj 存活1
obj 死亡2

3、Object对象中的finalize()方法
(1)调用finalize()方法的时机:当JVM确定不再有指向对象的引用时,垃圾收集器在对象上调用该方法。
(2)finalize()方法的作用:JVM调用该方法,表示该对象即将“死亡”,之后JVM就可以回收该对象了。有点类似对象生命周期的临终方法。
(3)通过使用finalize方法可以实现对象的自我拯救,但是只能拯救一次。
(4)打开jvisualvm,查看Finalizer线程。

七、内存溢出OOM怎么看,GCRoots分析定位原因

1、例子OOM.java

package deathAlgorithm;

import java.util.ArrayList;
import java.util.List;

public class OOM {
    static class OOMObject {
        byte[] allocation = new byte[1024];
    }

    /**
     * -Xmx20M -Xms20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\file
     * 参数说明:
     * -XX:+HeapDumpOnOutOfMemoryError   参数表示当JVM发生OOM时,自动生成DUMP文件
     * -XX:HeapDumpPath=${目录}   参数表示生成dump文件的路径,也可以指定文件名称,需要保证目录的文件夹是存在的
     * @param args
     */
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}

2、执行结果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\file\java_pid23920.hprof ...
Heap dump file created [21171459 bytes in 0.014 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at deathAlgorithm.OOM$OOMObject.<init>(OOM.java:8)
	at deathAlgorithm.OOM.main(OOM.java:21)

3、OOM分析
JProfiler是一款性能瓶颈分析工具。
其特点:
(1)界面操作友好
(2)对被分析的应用影响小
(3)CPU、Thread、Memory分析功能尤其强大
(4)支持对jdbc,nosql,jsp,servlet,socket等进行分析
(5)支持多种模式(离线,在线)的分析

第一步,找到问题的类,一般我们自己创建对象最多的类是有问题的

 第二步,双击,查看被谁引用

 第三步,查看它的GCRoots路径

 可以看到是在main方法的栈帧里引用的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值