【Java引用规范】弱引用

上一期,对于强软引用我们已经有了一个认知了,这期介绍一下弱引用。

弱引用(Weak Reference)

概念和特性

我们在研究一个类的用处时,可以先看一下这个类上的注释,JDK上的注释对于该类的说明以及作用都会有着详细说明。

Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed.

弱引用对象,这不会阻止它们的引用成为可终结的结束的,之后被回收。
说人话就是弱引用与引用对象之间的关系相当于没有,不会让引用对象无法被垃圾处理器回收,只是一个定义而已。
弱引用其实在我们日常开发中也经常用到,那就是隐藏在ThreadLocal类中的Entry类。Entry继承了弱引用,并存放了在线程上下文都能访问的对象,以便在线程内共享变量。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

插播一下广告,在使用ThreadLocal会有内存泄漏的风险,这一点在我的专栏《ThreadLocal详解》中会说到,大家可以去看一下😀。

创建和使用示例

Object object = new Object();
WeakReference<Object> weakReference = new WeakReference<>(object);

使用上面的代码就能创建一个弱引用的对象了。

弱引用对垃圾回收的影响

跟上期一样,从结果看本质,我们还是根据程序在内存中的变化来梳理出,弱引用在垃圾回收时,JVM到底做了啥,从而看出弱引用对垃圾回收的影响。

示例代码

使用程序代码与上期介绍软引用的类似,不过在每次循环中增加了转储内存快照的功能。

package com.zsk;

import com.sun.management.HotSpotDiagnosticMXBean;

import javax.management.MBeanServer;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Main {
    public static void main(String[] args) throws InterruptedException, IOException {
        weakReference();
    }

    private static void weakReference() throws InterruptedException, IOException {
        List<WeakReference<int[]>> arrayList = new ArrayList<>();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        while (true) {
            Thread.sleep(1000L);
            WeakReference<int[]> weakReference = new WeakReference<>(new int[(int) 1e7]);
            arrayList.add(weakReference);
            Date date = new Date();
            String format = simpleDateFormat.format(date);
            System.out.println(format);
            dumpHeap(format);
        }
    }

    private static void dumpHeap(String time) throws IOException {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        String diagnosticName = "com.sun.management:type=HotSpotDiagnostic";

        HotSpotDiagnosticMXBean diagnosticMXBean = ManagementFactory.newPlatformMXBeanProxy(
                mbs, diagnosticName, HotSpotDiagnosticMXBean.class);
        String dumpFilePath = "D:\\file\\Azir\\hprof\\" + time + ".hprof";
        try {
            // 生成堆转储
            diagnosticMXBean.dumpHeap(dumpFilePath, false);
            System.out.println("Heap dump created at: " + dumpFilePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

启动JVM参数:
-Xms512m -Xmx512m -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps
-Xloggc:D:\file\Azir\1.log

运行产出

在程序运行一段时间后,我们能得到3个东西

  1. 堆内存的运行变化图

我们通过IDEA的profiler功能(该功能的开启可以看看我上一期写的文章),就能看到下面这个有规律的内存运行变化图了。
20240118151936_rec_.gif

  1. GC的log日志

image.png

  1. 每秒钟转储的内存快照

image.png
根据这3个东西,我们就能分析出在垃圾回收时是怎么特殊处理弱引用的。

GC日志学习

在分析之前,先学习一下并行垃圾回收器垃圾回收前后的日志。在程序运行一段时间后,我们可以从log文件中得到类似下面的日志文件,随机提取一段来解析一下。
image.png
蓝框1:GC前堆内存的使用情况

  • 绿框3:年轻代的使用情况
日志块含义
PSYoungGen年轻代GC情况
total 171008K总共171008K≈167M
used 133536K已使用159938K≈156.1M
0x00000000f5580000使用的内存开始地址
0x0000000100000000目前使用到的地址
0x0000000100000000使用的内存结束地址
eden spaceeden区

- 167424K
总共167424K≈163.5M

- 95% used
已使用95%的内存空间

根据上面的日志,我们可以计算出整个年轻代一共申请的空间为:
0x0000000100000000-0x00000000f5580000174592K170.5M
计算得出的空间(170.5M)与显示的(167M)不一致,这个问题大家可以想想为什么,留个课外作业。
eden区 总共167424K≈163.5M 95%使用率≈155.325M

  • 绿框4:老年代的使用情况
  • 绿框5:元空间的使用情况(元空间所使用的内存空间并不占用堆内存的空间)

蓝框2:GC后堆内存的使用情况


蓝框1和绿框1中间夹着的就是本次垃圾回收的简略信息了

2024-01-18T16:08:25.999+0800: 77.382: [GC (Allocation Failure) [PSYoungGen: 159938K->192K(171008K)] 396850K->276254K(520704K), 0.0155949 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
日志块含义
2024-01-18T16:08:25.999+0800程序运行的当前时间
77.382启动到本次GC,程序总共运行时间
Allocation Failure空间分配失败,常见于对象创建
PSYoungGen年轻代的GC
159938K之前使用的内存大小
192K垃圾回收后的内存大小
171008K总内存大小
396850K垃圾回收前堆内存的大小
276254K垃圾回收后堆内存的大小
520704K堆内存大小
0.0155949 secs本次垃圾回收的持续时间
user=0.01用户CPU时间
sys=0.00系统CPU时间
real=0.01CPU总共使用的时间,专业一点叫墙钟时间(wall clock time)

GC做了啥

对于弱引用来说,GC究竟做了啥呢,我们可以抽取两段比较有意思的内存变化趋势来分析一下。
image.png
分别为25-26秒以及35-36秒,这两个区间的内存快照

25-26秒

这个时间段内存变化量不大,但是也有一定的降低量。肯定是进行了一次GC,至于是什么类型的GC。我们则需要到GC日志中看一下。

{Heap before GC invocations=17 (full 0):
 PSYoungGen      total 171008K, used 159938K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 167424K, 95% used [0x00000000f5580000,0x00000000ff198b80,0x00000000ff900000)
  from space 3584K, 2% used [0x00000000ffc80000,0x00000000ffc98000,0x0000000100000000)
  to   space 3584K, 0% used [0x00000000ff900000,0x00000000ff900000,0x00000000ffc80000)
 ParOldGen       total 349696K, used 236912K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 67% used [0x00000000e0000000,0x00000000ee75c078,0x00000000f5580000)
 Metaspace       used 9512K, capacity 9832K, committed 9984K, reserved 1058816K
  class space    used 1077K, capacity 1165K, committed 1280K, reserved 1048576K
2024-01-18T16:08:25.999+0800: 77.382: [GC (Allocation Failure) [PSYoungGen: 159938K->192K(171008K)] 396850K->276254K(520704K), 0.0155949 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap after GC invocations=17 (full 0):
 PSYoungGen      total 171008K, used 192K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 167424K, 0% used [0x00000000f5580000,0x00000000f5580000,0x00000000ff900000)
  from space 3584K, 5% used [0x00000000ff900000,0x00000000ff930000,0x00000000ffc80000)
  to   space 3072K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x0000000100000000)
 ParOldGen       total 349696K, used 276062K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 78% used [0x00000000e0000000,0x00000000f0d97a88,0x00000000f5580000)
 Metaspace       used 9512K, capacity 9832K, committed 9984K, reserved 1058816K
  class space    used 1077K, capacity 1165K, committed 1280K, reserved 1048576K
}

上面这段内存就是25-26秒中,GC回收的日志了。经过上一节对于GC日志的学习,我们可以很简单的看出,是由于年轻代内存分配失败,导致进行了一次GC。
我们对这段GC日志再分析一下,一共有几个点在GC前后是有变化的。

  1. eden区的占用变化了,从95%降低到了0%,回收了159052K155.3M

由于我们程序只有一个功能,那就是创建一个内存占用为38M的大数组,并放在ArrayList中。此时,eden区被回收的内存155M刚好是38M的四倍,并且本次GC是因为内存空间分配失败导致的,所以我们可以得出第一个结论:

在eden区创建第五个大数组时,由于内存不足,进行了一次年轻代的GC。

  1. from区的占用变化了,从2%升高到了5%,增加了107.5K0.1M

内存变化太少,分析不出什么结论。

  1. 老年代的占用变化了,从67%升高到了78%,增加了38466K37.5M

这个37.5M与我们大数组的空间38M相差无几,由于GC日志显示的百分比是有误差的,所以我们再去看看老年代真正增加了多少内存空间的使用。
GC前:total 349696K, used 236912K
GC后:total 349696K, used 276062K
GC后增加了39150K38.2M,这个数值与大数组的内存相同。此时,我们又可以得出第二个结论:

在进行年轻代的GC前后,有一个大数组晋级到了老年代中。

根据日志分析得出的两个结论再去内存快照中验证一下,看是否与我们分析的一致。


将25秒和26秒的内存快照导入到MAT工具中分析后,对比一下GC前后,有哪几个大数组被清理了,有哪个大数组晋级到了老年代了。
image.png

左边为GC前,右边为GC后

我们可以看到,GC后58、59、60号的大数组对象被回收了,那么被晋级到老年代的大数组肯定就是57号大数组了。这个结论我们可以根据GC前后57号大数组的内存地址对比一下。
image.png
GC前0xf5580000位于年轻代中[0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
GC后0xee772078位于老年代中[0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
所以上面得出的两个结论是正确的:

  1. 在eden区创建第五个大数组时,由于内存不足,进行了一次年轻代的GC。
  2. 在进行年轻代的GC前后,有一个大数组晋级到了老年代中。

根据这两个结论,我们可以归纳总结一下,那就是:

在年轻代内存空间不足时,会将弱引用引用的对象进行清理

对象的晋级是一个通用的规则,而不是弱引用导致的。

35-36秒

这个时间段中,内存的变化量极大。一眼就能看出,肯定进行了一次Full GC了,这个我们去看一下GC日志也能得出。

{Heap before GC invocations=18 (full 0):
 PSYoungGen      total 171008K, used 160025K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 167424K, 95% used [0x00000000f5580000,0x00000000ff196768,0x00000000ff900000)
  from space 3584K, 5% used [0x00000000ff900000,0x00000000ff930000,0x00000000ffc80000)
  to   space 3072K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x0000000100000000)
 ParOldGen       total 349696K, used 276062K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 78% used [0x00000000e0000000,0x00000000f0d97a88,0x00000000f5580000)
 Metaspace       used 9525K, capacity 9832K, committed 9984K, reserved 1058816K
  class space    used 1077K, capacity 1165K, committed 1280K, reserved 1048576K
2024-01-18T16:08:34.178+0800: 85.561: [GC (Allocation Failure) [PSYoungGen: 160025K->128K(171520K)] 436088K->315285K(521216K), 0.0157927 secs] [Times: user=0.19 sys=0.02, real=0.02 secs] 
Heap after GC invocations=18 (full 0):
 PSYoungGen      total 171520K, used 128K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 168448K, 0% used [0x00000000f5580000,0x00000000f5580000,0x00000000ffa00000)
  from space 3072K, 4% used [0x00000000ffd00000,0x00000000ffd20000,0x0000000100000000)
  to   space 3072K, 0% used [0x00000000ffa00000,0x00000000ffa00000,0x00000000ffd00000)
 ParOldGen       total 349696K, used 315157K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 90% used [0x00000000e0000000,0x00000000f33c5498,0x00000000f5580000)
 Metaspace       used 9525K, capacity 9832K, committed 9984K, reserved 1058816K
  class space    used 1077K, capacity 1165K, committed 1280K, reserved 1048576K
}

{Heap before GC invocations=19 (full 1):
 PSYoungGen      total 171520K, used 128K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 168448K, 0% used [0x00000000f5580000,0x00000000f5580000,0x00000000ffa00000)
  from space 3072K, 4% used [0x00000000ffd00000,0x00000000ffd20000,0x0000000100000000)
  to   space 3072K, 0% used [0x00000000ffa00000,0x00000000ffa00000,0x00000000ffd00000)
 ParOldGen       total 349696K, used 315157K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 90% used [0x00000000e0000000,0x00000000f33c5498,0x00000000f5580000)
 Metaspace       used 9525K, capacity 9832K, committed 9984K, reserved 1058816K
  class space    used 1077K, capacity 1165K, committed 1280K, reserved 1048576K
2024-01-18T16:08:34.194+0800: 85.577: [Full GC (Ergonomics) [PSYoungGen: 128K->0K(171520K)] [ParOldGen: 315157K->2574K(349696K)] 315285K->2574K(521216K), [Metaspace: 9525K->9525K(1058816K)], 0.0077757 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap after GC invocations=19 (full 1):
 PSYoungGen      total 171520K, used 0K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 168448K, 0% used [0x00000000f5580000,0x00000000f5580000,0x00000000ffa00000)
  from space 3072K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x0000000100000000)
  to   space 3072K, 0% used [0x00000000ffa00000,0x00000000ffa00000,0x00000000ffd00000)
 ParOldGen       total 349696K, used 2574K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 0% used [0x00000000e0000000,0x00000000e0283be0,0x00000000f5580000)
 Metaspace       used 9525K, capacity 9832K, committed 9984K, reserved 1058816K
  class space    used 1077K, capacity 1165K, committed 1280K, reserved 1048576K
}

从日志文件中可以看出,一共进行了两次GC。分别为:

  1. 年轻代内存分配失败导致的GC
  2. 由于Ergonomics导致的Full GC

第一次GC的原因与25-26秒的原因一致,就不再重复了。在这一节,主要分析一下Full GC的原因。
什么是Ergonomics呢?
image.png
百度翻译为人体工学,对于我们分析来说毫无意义。这里我先提一嘴吧,这个单词在HotSpot虚拟机的C++代码中可以找到,位于gcCause.cpp文件中,含义为adaptive_size_policy,就是自适应大小策略的意思。
image.png

有关HotSpot虚拟机对于引用是怎么进行垃圾回收的,在后面我也会讲到,大家敬请期待。


话说回来,因为自适应大小策略的原因,导致了一次Full GC,那么针对内存大小,GC前后有哪些变化呢?

  1. 年轻代空间被完全回收,128K降低至0K。

内存变化太少,分析不出什么结论。

  1. 老年代空间被完全回收,315157K降低至2574K,减少了312583K305M

老年代的内存被全部回收了,回收的内存空间也正好是8个大数组的空间,所以也能得出一个结论,弱引用也有软引用的一个特性:在OOM抛出之前,会将引用的对象全部清空。
用内存快照来验证一下上面得出的结论:
image.png

左边为GC前,右边为GC后

可以看到GC后,ArrayList中存放的弱引用所引用的大数组都被回收完了。

结论

弱引用不但有着软引用的特性之外,还存在着哪个内存分层空间中达到上限后,都会将弱引用给清空。
所以在动图的每一次内存使用空间的降低时,都触发了一次垃圾回收。

下期预告:虚引用以及神秘的引用队列

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值