在上一期,对于强软引用我们已经有了一个认知了,这期介绍一下弱引用。
弱引用(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个东西
- 堆内存的运行变化图
我们通过IDEA的profiler功能(该功能的开启可以看看我上一期写的文章),就能看到下面这个有规律的内存运行变化图了。
- GC的log日志
- 每秒钟转储的内存快照
根据这3个东西,我们就能分析出在垃圾回收时是怎么特殊处理弱引用的。
GC日志学习
在分析之前,先学习一下并行垃圾回收器垃圾回收前后的日志。在程序运行一段时间后,我们可以从log文件中得到类似下面的日志文件,随机提取一段来解析一下。
蓝框1:GC前堆内存的使用情况
- 绿框3:年轻代的使用情况
日志块 | 含义 |
---|---|
PSYoungGen | 年轻代GC情况 |
total 171008K | 总共171008K≈167M |
used 133536K | 已使用159938K≈156.1M |
0x00000000f5580000 | 使用的内存开始地址 |
0x0000000100000000 | 目前使用到的地址 |
0x0000000100000000 | 使用的内存结束地址 |
eden space | eden区 |
- 167424K | 总共167424K≈163.5M |
- 95% used | 已使用95%的内存空间 |
根据上面的日志,我们可以计算出整个年轻代一共申请的空间为:
0x0000000100000000-0x00000000f5580000≈174592K≈170.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.01 | CPU总共使用的时间,专业一点叫墙钟时间(wall clock time) |
GC做了啥
对于弱引用来说,GC究竟做了啥呢,我们可以抽取两段比较有意思的内存变化趋势来分析一下。
分别为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前后是有变化的。
- eden区的占用变化了,从95%降低到了0%,回收了159052K≈155.3M。
由于我们程序只有一个功能,那就是创建一个内存占用为38M的大数组,并放在ArrayList中。此时,eden区被回收的内存155M刚好是38M的四倍,并且本次GC是因为内存空间分配失败导致的,所以我们可以得出第一个结论:
在eden区创建第五个大数组时,由于内存不足,进行了一次年轻代的GC。
- from区的占用变化了,从2%升高到了5%,增加了107.5K≈0.1M。
内存变化太少,分析不出什么结论。
- 老年代的占用变化了,从67%升高到了78%,增加了38466K≈37.5M。
这个37.5M与我们大数组的空间38M相差无几,由于GC日志显示的百分比是有误差的,所以我们再去看看老年代真正增加了多少内存空间的使用。
GC前:total 349696K, used 236912K
GC后:total 349696K, used 276062K
GC后增加了39150K≈38.2M,这个数值与大数组的内存相同。此时,我们又可以得出第二个结论:
在进行年轻代的GC前后,有一个大数组晋级到了老年代中。
根据日志分析得出的两个结论再去内存快照中验证一下,看是否与我们分析的一致。
将25秒和26秒的内存快照导入到MAT工具中分析后,对比一下GC前后,有哪几个大数组被清理了,有哪个大数组晋级到了老年代了。
左边为GC前,右边为GC后
我们可以看到,GC后58、59、60号的大数组对象被回收了,那么被晋级到老年代的大数组肯定就是57号大数组了。这个结论我们可以根据GC前后57号大数组的内存地址对比一下。
GC前0xf5580000
位于年轻代中[0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
GC后0xee772078
位于老年代中[0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
所以上面得出的两个结论是正确的:
- 在eden区创建第五个大数组时,由于内存不足,进行了一次年轻代的GC。
- 在进行年轻代的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。分别为:
- 年轻代内存分配失败导致的GC
- 由于
Ergonomics
导致的Full GC
第一次GC的原因与25-26秒的原因一致,就不再重复了。在这一节,主要分析一下Full GC的原因。
什么是Ergonomics
呢?
百度翻译为人体工学,对于我们分析来说毫无意义。这里我先提一嘴吧,这个单词在HotSpot虚拟机的C++代码中可以找到,位于gcCause.cpp
文件中,含义为adaptive_size_policy,就是自适应大小策略的意思。
有关HotSpot虚拟机对于引用是怎么进行垃圾回收的,在后面我也会讲到,大家敬请期待。
话说回来,因为自适应大小策略的原因,导致了一次Full GC,那么针对内存大小,GC前后有哪些变化呢?
- 年轻代空间被完全回收,128K降低至0K。
内存变化太少,分析不出什么结论。
- 老年代空间被完全回收,315157K降低至2574K,减少了312583K≈305M。
老年代的内存被全部回收了,回收的内存空间也正好是8个大数组的空间,所以也能得出一个结论,弱引用也有软引用的一个特性:在OOM抛出之前,会将引用的对象全部清空。
用内存快照来验证一下上面得出的结论:
左边为GC前,右边为GC后
可以看到GC后,ArrayList中存放的弱引用所引用的大数组都被回收完了。
结论
弱引用不但有着软引用的特性之外,还存在着哪个内存分层空间中达到上限后,都会将弱引用给清空。
所以在动图的每一次内存使用空间的降低时,都触发了一次垃圾回收。
下期预告:虚引用以及神秘的引用队列