Java 中强引用、软引用、弱引用、虚引用用于控制堆内实例被回收时的特性,如下表所示:
引用类型 回收时机 强引用 StrongReference never 软引用 SoftReference 内存不足 弱引用 WeakReference GC 时 虚引用 PhantomReference Unknown
最基础的引用方式就是强引用,如果一个对象被强引用着,那么这个对象就无法被 GC。
因此测试软引用、弱引用、虚引用必须确保对象本身没有被强引用,否则实例总是无法被回收。或者说要确保该实例仅仅被SoftReference、WeakReference 或者 PhantomReference 实例引用。
下面是测试代码,可以在如下仓库中找到源码。
Spongecaptain/referenceInJava: Java 强引用、软引用、弱引用以及虚引用测试项目 (github.com)
为了实验的完整性,在此说明 JDK 版本:
$ java -version
openjdk version "1.8.0_282"
OpenJDK Runtime Environment (Zulu 8.52.0.23-CA-macos-aarch64) (build 1.8.0_282-b08)
OpenJDK 64-Bit Server VM (Zulu 8.52.0.23-CA-macos-aarch64) (build 25.282-b08, mixed mode)
强引用的实例是一种无论何时都不会被 GC 回收的实例,测试代码如下:
public class TestStrongReference {
public static void main(String[] args) {
byte[] bytes = new byte[1024*1024*9];
byte[] bytes_2 = bytes;
bytes = null;
System.gc();
//make sure gc is done try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bytes_2);
}
}
输出:
[B@28d93b30
软引用的实例会在内存不足时会被回收,测试代码如下:
import java.lang.ref.SoftReference;
public class TestSoftReference {
public static void main(String[] args) {
byte[] bytes = new byte[1024*1024*9];
SoftReference<byte[]> softReference = new SoftReference<>(bytes);
bytes = null;//delete strong reference
System.out.println("before gc");
System.out.println(softReference.get());
//内存充足的情况下进行 GC System.gc();
//make sure gc is done try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after gc");
System.out.println(softReference.get());
//手动导致内存不足的 OOM Error boolean isOOM = false;
try{
System.out.println("after memory is not enough");
byte[] bytes_2 = new byte[1024*1024*4];
}catch (OutOfMemoryError e1){
isOOM = true;
e1.printStackTrace();
System.out.println(softReference.get());
}
if(!isOOM){
System.out.println(softReference.get());
}
}
}
输出:
before gc
[B@28d93b30
after gc
[B@28d93b30
after memory is not enough
null
上述代码需要将堆内存大小进行限制,可以在 IntelliJ 的 Run/Debug Configurations 中配置 VM Options 为-Xms10m -Xmx10m。可以这么认为:
注意事项:任意一次内存申请超过最大堆内存时会抛出 OOM Error,此时也会导致内存不足,释放软引用实例。
虚引用的实例会在 GC 时就会被回收,测试代码如下:
import java.lang.ref.WeakReference;
public class TestWeakReference {
public static void main(String[] args) {
byte[] bytes = new byte[1024 * 1024 * 9];
WeakReference<byte[]> weakReference = new WeakReference<>(bytes);
bytes = null;//
System.out.println("before gc");
System.out.println(weakReference.get());
//内存充足的情况下进行 GC System.gc();
//make sure gc is done try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after gc");
System.out.println(weakReference.get());
}
}
输出:
before gc
[B@28d93b30
after gc
null
虚引用的实例在何时被回收是不确定的。并且其必须结合 ReferenceQueue 一起使用,因为其构造器定义如下:
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
虚拟用的测试代码如下:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class TestPhantomReference {
public static void main(String[] args) {
byte[] bytes = new byte[1024 * 1024 * 9];
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
PhantomReference<byte[]> weakReference = new PhantomReference<>(bytes,referenceQueue);
//bytes = null;//无论是否进行强引用的删除,下面这一步总是返回 null System.out.println(weakReference.get());
}
}
输出:
null
虚拟引用 weakReference/#get 总是会返回 null,引用形同虚设。
你可以在 Reference 类的 Javadocs 中看到,一个被 Reference 引用的实例处于如下几种可能状态:
可见,Reference 实例的状态与 ReferenceQueue 队列以及 ReferenceHandler 线程密切相关。
下图是 Reference 类的继承关系:
Reference 拥有一个静态字段 ReferenceHandler,后者是 Thread 的子类。可见,ReferenceHandler 线程是全局共享的静态实例。
ReferenceHandler 线程在 Reference 的 static 语句块中被初始化并启动,其线程优先级被设置为最高。
ReferenceHandler 作为 daemon 线程而存在,其运行逻辑就是在while(true)循环中不断调用 Reference/#tryHandlePending 方法。此方法的运行逻辑主要为:
Reference<Object> pending
但是你会发现,没有任何 Java 回负责 Reference/#pending 字段的赋值。事实上,这个字段由 GC 线程负责赋值。
不仅仅 PhantomReference 虚引用的构造器可以接收一个 ReferenceQueue 实例,事实上,上面的 SoftReference、WeakReference 类的构造器也能接收一个 ReferenceQueue 实例,如下所示:
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
并且 SoftReference、WeakReference 内部都有 ReferenceQueue 实例:
那么问题来了,谁来负责此队列上 Reference 实例的处理呢?
因此,就是我们程序员自己需要负责从 ReferenceQueue 队列中取出 Reference 元素,否则就不要选择带有 ReferenceQueue 的 Reference 类构造器。
换言之,ReferenceQueue 起到了一个延迟回收的机制:
基于这种思路,我们可以写出如下的代码:
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicLong;
public class TestReferenceQueue {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
test();
}
}
private static void test() throws InterruptedException {
byte[] bytes = new byte[1024 * 10];
ReferenceQueue<MyWeakReference> referenceQueue = new ReferenceQueue<>();
MyWeakReference<byte[]> weakReference = new MyWeakReference<>(bytes,referenceQueue);
bytes = null;
System.gc();
//make sure gc is done Thread.sleep(1000);
//在 GC 后,GC 线程将 WeakReference 实例内部的 referent 字段被置为 null //并且 WeakReference 实例被 ReferenceHandler 线程加入到 ReferenceQueue 队列中 MyWeakReference poll = (MyWeakReference) referenceQueue.poll();
System.out.println(poll==weakReference);
System.out.println(poll.get());
poll.releaseOneReference();
System.out.println(poll.getCount());
}
static class MyWeakReference<T> extends WeakReference{
private static AtomicLong count = new AtomicLong(0L);
public MyWeakReference(Object referent) {
super(referent);
}
public MyWeakReference(Object referent, ReferenceQueue q) {
super(referent, q);
}
public long getCount(){
return count.get();
}
public void releaseOneReference(){
count.incrementAndGet();
}
}
}
如下是一个 WeakHashMap 的使用案例:
import java.util.WeakHashMap;
public class TestWeakHashMap {
public static void main(String[] args) throws InterruptedException {
//init WeakHashMap<String, String> map = new WeakHashMap<String, String>();
String key = new String("hello");
map.put(key,"world");
//检查 WeakHashMap 是否有该 key System.out.println(map.get("hello"));
key = null;//delete strong reference System.gc();
//make sure gc is done Thread.sleep(2000);
System.out.println(map.get("hello"));
}
}
WeakHashMap 基于如下技术实现:
其中 Finalizer 也是一种 Reference 列实例,其内部拥有一个全局静态 FinalizerThread 线程实例。
WeakHashMap 类的内部原理如下:
调用 WeakHashMap/#expungeStaleEntries 方法的线程分为两种类型:
注意事项:很多文章并没有注意到 Finalizer/#FinalizerThread 线程也会负责调用 WeakHashMap/#expungeStaleEntries 方法,事实上,如果你在案例代码中
System.out.println(map.get("hello"));
打上断点,早在执行 get 方法前,WeakHashMap 内部的 size 就已经是 0 了。
内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
Netty 利用:
来检测内存泄漏,具体来说,Netty 通过如下的方式实现:
Netty 的将内存泄漏定义为:资源已经被 GC 回收,但是并没有执行指定次数的 release 方法。如果光这样说,你一定难以理解这种情况为啥算是内存泄漏了?这里解释一下。Netty 中内存分配即申请一个 ByteBuf 实例,ByteBuf 可以是池化的,也可以是未池化的。如果 ByteBuf 未池化,最终被 GC 线程回收,那也无妨。但是如果 GC 了内存池中的实例,那么相当于没有利用到内存池能够重用内存的特性,这是不符合要求的。
总之,本节内存泄漏实际上指的是:该返回内存池的内存应当返回内存池,而不是交给 GC 线程来回收。因为内存池的目的就在于重用内存,避免 GC,提高运行效率。
回过头来,下面说明一下 Netty 内存泄漏检测的内部执行原理:
1.内存申请
内存申请时,计数器+1,并将 ByteBuf 对应的弱引用对象 DefaultResourceLeak 加入到 ResourceLeakDetector.allLeaks 队列中;
ByteBuf buffer = ctx.alloc().buffer();
ResourceLeakDetector.allLeaks 存放着弱引用对象,弱引用对象内部维护着一个资源释放计数器。allLeaks 本质是一个 Hashset,利用 ConcurrentHashMap 实现。
注意:正如前面章节所述,allLeaks 链表引用弱引用 DefaultResourceLeak 实例并不会影响 DefaultResourceLeak 对应的 ByteBuf 资源被 GC 线程回收。
2.内存释放
内存释放时,将 ByteBuf 在 ResourceLeakDetector.allLeaks 的 DefaultResourceLeak 实例内部计数器 -1。如果计数器的个数为 0,那么说明内存正确被释放了,那么就将此资源对应的 DefaultResourceLeak 从 allLeaks 中移除。
3.GC 时
当 ByteBuf 被 GC 回收时,通过 GC 线程、ReferenceHandler 线程的合作,会将 ByteBuf 对应的弱引用对象 DefaultResourceLeak 实例放入到 ResourceLeakDetector/#refQueue 队列。
4.内存泄漏检测
当 NioEventLoop 在 ChannelPipeline 中试图向内存池申请内存时,就会先进行一次内存泄漏检测,其方式是调用 ResourceLeakDetector/#track0 方法,然后进入最核心的 ResourceLeakDetector/#reportLeak 方法。
可见,Netty 内存泄漏检测的本质就是维护两个数据结构:
本质就是基于队列+异步线程(GC 线程、ReferenceHandler 线程、NioEventLoop 用户线程)维护的一个异步消息通知与回调机制。通知是资源被 GC 了,回调则是用户线程自己负责执行。
Java 中强引用、软引用、弱引用、虚引用用于控制堆内实例被回收时的特性,如下表所示:
引用类型 回收时机 强引用 StrongReference never 软引用 SoftReference 内存不足 弱引用 WeakReference GC 时 虚引用 PhantomReference Unknown
GC 线程会负责一件事:
ReferenceHandler 线程在 Reference 的 static 语句块中启动,其负责如下的策略:
用户线程则可以选择继承 Reference 以及 ReferenceQueue 队列,负责从队列中取出 Reference 实例,做出相应处理后,最终释放 Reference 实例。
其中,基于 ReferenceQueue 可以实现一种回调通知机制,通知的方式是:ReferenceHandler 线程负责将 Reference 实例放入到 ReferenceQueue 队列中。回调策略由用户线程编写,ReferenceQueue 内部的 Reference 实例也由用户线程负责取出以及处理。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://spongecaptain.cool/post/java/referenceinjava/
内容来源于网络,如有侵权,请联系作者删除!