项目中的全局缓存导致了内存泄露?

微信扫一扫,分享到朋友圈

项目中的全局缓存导致了内存泄露?

项目中的全局缓存导致了内存泄露?

对于项目中的数据,为了提升访问速度,或是为了多个业务子模块代码间的解耦,往往通过中间的缓存对象来统一管理。但是随着请求量的增加,简单的 HashMap 缓存功能,却导致了项目中的内存泄露,线上环境请求量一旦过高,就出现大量 Full GC.

为了解决问题,我们必须从 JDK 的引用谈起。

手机用户请 横屏 获取最佳阅读体验, REFERENCES 中是本文参考的链接,如需要链接和更多资源,可以加入『知识星球』获取长期知识分享服务。

JDK 引用

引用与对象

每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。 在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。

“每种编程语言都有自己的数据处理方式。有些时候,程序员必须注意将要处理的数据是什么类型。你是直接操纵元素,还是用某种基于特殊语法的间接表示(例如C/C++里的指针)来操作对象。所有这些在 Java 里都得到了简化,一切都被视为对象。因此,我们可采用一种统一的语法。尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(reference)。”《Java编程思想》

//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;
复制代码

通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。

String str = new String("abc");
System.out.println(str.toString());
复制代码

在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。 Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。 在不同垃圾回收算法中,对引用的判断方式有所不同:

  • 引用计数法:为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,则认为该对象可以被回收(目前在Java中已经弃用这种方式了)。
  • 可达性分析算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

JDK1.2 之前,一个对象只有“已被引用”和”未被引用”两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。

内存自动管理

可达性分析算法

Java执行GC时,需要判断对象是否存活。判断一个对象是否存活使用了”可达性分析算法”。

基本思路就是通过一系列称为 GC Roots 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达时,证明此对象不可用。

可以作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈JNI引用的对象

往往到达一个对象的引用链会存在多条,垃圾回收时会依据两个原则来判断对象的可达性:

  • 单一路径中,以最弱的引用为准
  • 多路径中,以最强的引用为准

数据存储方式

局部变量/方法参数

对于局部变量和方法传递的参数在jvm中的存储方式是相同的,都是存储在栈上开辟的空间中。方法参数空间在进入方法时开辟,方法退出时进行回收。以32为JVM为例,boolean、byte、short、char、int、float以及对应的引用类型都是分配4字节大小的空间,long、double分配8字节大小空间。对于每一个方法来说,最多占用空间大小是固定的,在编译时就已经确定了。当在方法中声明一个int变量i=0或Object变量obj=null时,此时仅仅在栈上分配空间,不影响到堆空间。当new Object()时,将会在堆中开辟一段内存空间并初始化Object对象。

数组类型引用和对象

当声明数组时,int[] arr=new int[2];数组也是对象,arr实际上是引用,栈上占用4个字节大小的存储空间,而是会在堆中开辟相应大小空间进行存储,然后arr变量指向它。当声明一个二维数组时,如:int[][] arr2=new int[2][4],arr2同样在栈中占用4个字节,在堆内存中开辟长度为2,类型为int[]的数组对象,然后arr2指向这个数组。这个数组内部有两个引用类型(大小为4个字节),分别指向两个长度为4类型为int的数组。内存分布如图:

所以当传递一个数组给一个方法时,数组的元素在方法内部是可以被修改的,但是无法让数组引用指向新的数组。其实,还可以声明:int [][] arr3=new int[3][],内存分布如下:

String类型数据

对于String类型,其对象内部需要维护三个成员变量,char[] chars,int startIndex, int length。chars是存储字符串数据的真正位置,在某些情况下是可以共用的,实际上String类型是不可变类型。例如:String str=new String(“hello”),内存分布如下:

四种引用类型

所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。

强引用

Java中默认声明的就是强引用,比如:

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null
复制代码

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。

软引用

软引用是用来描述一些非必需但仍有用的对象。 在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常 。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。 在 JDK1.2 之后,用 java.lang.ref.SoftReference 类来表示软引用。

下面以一个例子来进一步说明强引用和软引用的区别: 在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M ,将 JVM 的初始内存设为2M,最大可用内存为 3M

首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常:

public class JvmReferenceTest {
public static void main(String[] args) {
//-Xms2M -Xmx3M
testStrongReference(3);
}
private static void testStrongReference(int m) {
byte[] buff = new byte[1024 * 1024 * m];
}
}
复制代码

内存不够使用,程序直接报错,强引用并不会被回收

//m = 3时,即 3M的 byte 数组定义时,申请的内存空间不足
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.example.jvm.reference.JvmReferenceTest.testStrongReference(JvmReferenceTest.java:27)
at com.example.jvm.reference.JvmReferenceTest.main(JvmReferenceTest.java:23)
复制代码

如果是软引用呢?

我们发现无论循环创建多少个软引用对象,打印结果总是只有最后2个对象被保留,其他的obj全都被置空回收了。这里就说明了在内存不足的情况下,软引用将会被自动回收。值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference 指向的对象 buff 仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。

public class JvmReferenceTest {
public static void main(String[] args) {
//-Xms2M -Xmx3M
//testStrongReference(3);
//-Xms2M -Xmx3M
testSoftReference(10);
}
//get available memory in MB
public static long getFreeMemory() {
return Runtime.getRuntime().freeMemory() / (1024 * 1024);
}
//get total memory in MB
public static long getTotalMemory() {
return Runtime.getRuntime().totalMemory() / (1024 * 1024);
}
private static void testSoftReference(int m) {
List<SoftReference<Object>> list = new ArrayList<>();
for (; m > 0; m--) {
// 局部变量,JVM 运行后判断后续未使用的话就会自动释放
byte[] buff = new byte[1024 * 1024 * 2];
list.add(new SoftReference<>(buff));
System.out.println("Free memory after add list size:  " + list.size()
+ " is " + getFreeMemory() + "MB");
}
System.out.println("Current memory use: " + (getTotalMemory()  - getFreeMemory()) + "MB");
System.out.println("Total memory is: " + getTotalMemory() + "MB");
//输出
for (SoftReference<Object> item : list) {
System.out.println(item.get());
}
}
private static void testStrongReference(int m) {
byte[] buff = new byte[1024 * 1024 * m];
}
}
复制代码

日志

Free memory after add list size:  1 is 0MB
Free memory after add list size:  2 is 1MB
Free memory after add list size:  3 is 1MB
Free memory after add list size:  4 is 1MB
Free memory after add list size:  5 is 1MB
Free memory after add list size:  6 is 1MB
Free memory after add list size:  7 is 1MB
Free memory after add list size:  8 is 1MB
Free memory after add list size:  9 is 1MB
Free memory after add list size:  10 is 1MB
Current memory use: 2MB
Total memory is: 3MB
null
null
null
null
null
null
null
null
null
[B@47d384ee
复制代码

可以看出内存不足时,自动释放了。最后的 list 集合中只保留了一个 byte 数组,刚好时 2M的大小。

如果略微调整下 buff 变量的位置,会发生一件很有意思的事情。

private static void testSoftReference(int m) {
// 声明为方法内可见的局部全局变量
byte[] buff = null;
List<SoftReference<Object>> list = new ArrayList<>();
for (; m > 0; m--) {
buff = new byte[1024 * 1024 * 2];
SoftReference<Object> sr = new SoftReference<>(buff);
list.add(sr);
System.out.println("Free memory after add list size:  " + list.size()
+ " is " + getFreeMemory() + "MB");
}
System.out.println("Current memory use: " + (getTotalMemory() - getFreeMemory()) + "MB");
System.out.println("Total memory is: " + getTotalMemory() + "MB");
//输出
for (SoftReference<Object> item : list) {
System.out.println(item.get());
}
System.out.println("--------------------------------");
}
复制代码
Free memory after add list size:  1 is 0MB
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.example.jvm.reference.JvmReferenceTest2.testSoftReference(JvmReferenceTest2.java:69)
at com.example.jvm.reference.JvmReferenceTest2.main(JvmReferenceTest2.java:30)
复制代码

从日志可以看出抛出了 OOM 内存溢出的问题。 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。

如果一个对象惟一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。

弱引用

弱引用的引用强度比软引用要更弱一些, 被弱引用关联的对象只能存活到下一次垃圾回收发生之前。当发生GC时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象 。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

我们以与软引用同样的方式来测试一下弱引用:

private static void testWeakReference(int m) {
List<WeakReference<Object>> list = new ArrayList<>();
for (; m > 0; m--) {
byte[]  buff = new byte[1024 * 1024 * 2];
WeakReference<Object> sr = new WeakReference<>(buff);
list.add(sr);
System.out.println("Free memory after add list size:  " + list.size()
+ " is " + getFreeMemory() + "MB");
}
System.out.println("Current memory use: " + (getTotalMemory() - getFreeMemory()) + "MB");
System.out.println("Total memory is: " + getTotalMemory() + "MB");
System.gc();
//输出
for (WeakReference<Object> item : list) {
System.out.println(item.get());
}
System.out.println("--------------------------------");
}
复制代码

日志

Free memory after add list size:  1 is 0MB
Free memory after add list size:  2 is 1MB
Free memory after add list size:  3 is 1MB
Free memory after add list size:  4 is 1MB
Free memory after add list size:  5 is 1MB
Free memory after add list size:  6 is 1MB
Free memory after add list size:  7 is 1MB
Free memory after add list size:  8 is 1MB
Free memory after add list size:  9 is 1MB
Free memory after add list size:  10 is 1MB
Current memory use: 2MB
Total memory is: 3MB
null
null
null
null
null
null
null
null
null
null
--------------------------------
复制代码

注意代码中的手动触发 GC 操作, System.gc(); 之后,所有的弱引用内存会被回收。

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能够在这个对象被垃圾回收器回收掉后收到一个通知。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

使用PhantomReference类实现虚引用。

public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent.  Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return  <code>null</code>
*/
public T get() {
return null;
}
/**
* Creates a new phantom reference that refers to the given object and
* is registered with the given queue.
*
* <p> It is possible to create a phantom reference with a <tt>null</tt>
* queue, but such a reference is completely useless: Its <tt>get</tt>
* method will always return null and, since it does not have a queue, it
* will never be enqueued.
*
* @param referent the object the new phantom reference will refer to
* @param q the queue with which the reference is to be registered,
*          or <tt>null</tt> if registration is not required
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
复制代码

引用队列(ReferenceQueue)

引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

与软引用、弱引用不同,虚引用必须和引用队列一起使用。

Reference 与 ReferenceQueue 之间是如何工作的呢?

SoftReference,WeakReference,PhantomReference 拥有共同的父类 Reference,看一下其内部实现:

Reference的构造函数最多可以接受两个参数: Reference(T referent, ReferenceQueue<? super T> queue)

referent:即 Reference 所包装的引用对象

queue:此 Reference 需要注册到的引用队列

ReferenceQueue本身提供队列的功能,ReferenceQueue对象同时保存了一个Reference类型的 head 节点,Reference封装了next字段,这样就是可以组成一个单向链表。

ReferenceQueue主要用来确认Reference的状态。Reference对象有四种状态:

  • active

    GC会特殊对待此状态的引用,一旦被引用的对象的可达性发生变化(如失去强引用,只剩弱引用,可以被回收),GC会将引用放入pending队列并将其状态改为pending状态

  • pending

    位于pending队列,等待ReferenceHandler线程将引用入队queue

  • enqueue

    ReferenceHandler将引用入队queue

  • inactive

    引用从queue出队后的最终状态,该状态不可变

Reference 里有个静态字段 pending,同时还通过静态代码块启动了Reference-handler thread。当一个 Reference 的 referent 被回收时,垃圾回收器会把 reference 添加到 pending 这个链表里,然后 Reference-handler thread 不断的读取 pending 中的reference,把它加入到对应的ReferenceQueue中。

当 reference 与 referenQueue 联合使用的主要作用就是当 reference 指向的 referent 回收时,提供一种通知机制,通过 queue 取到这些 reference,来做额外的处理工作。

用PhantomReference来自动关闭文件流

public class ResourcePhantomReference<T> extends PhantomReference<T> {
private List<Closeable> closeables;
public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> resource) {
super(referent, q);
closeables = resource;
}
public void cleanUp() {
if (closeables == null || closeables.size() == 0)
return;
for (Closeable closeable : closeables) {
try {
closeable.close();
System.out.println("clean up:"+closeable);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
复制代码

守护者线程利用ReferenceQueue做自动清理

public class ResourceCloseDeamon extends Thread {
private static ReferenceQueue QUEUE = new ReferenceQueue();
//保持对reference的引用,防止reference本身被回收
private static List<Reference> references=new ArrayList<>();
@Override
public void run() {
this.setName("ResourceCloseDeamon");
while (true) {
try {
ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove();
reference.cleanUp();
references.remove(reference);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void register(Object referent, List<Closeable> closeables) {
references.add(new ResourcePhantomReference(referent,QUEUE,closeables));
}
}
复制代码

封装的文件操作

public class FileOperation {
private FileOutputStream outputStream;
private FileInputStream inputStream;
public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) {
this.outputStream = outputStream;
this.inputStream = inputStream;
}
public void operate() {
try {
inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel());
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码

测试代码

ublic class PhantomTest {
public static void main(String[] args) throws Exception {
//打开回收
ResourceCloseDeamon deamon = new ResourceCloseDeamon();
deamon.setDaemon(true);
deamon.start();
// touch a.txt b.txt
// echo "hello" > a.txt
//保留对象,防止gc把stream回收掉,起不到演示效果
List<Closeable> all=new ArrayList<>();
FileInputStream inputStream;
FileOutputStream outputStream;
for (int i = 0; i < 100000; i++) {
inputStream = new FileInputStream("/Users/robin/a.txt");
outputStream = new FileOutputStream("/Users/robin/b.txt");
FileOperation operation = new FileOperation(inputStream, outputStream);
operation.operate();
TimeUnit.MILLISECONDS.sleep(100);
List<Closeable>closeables=new ArrayList<>();
closeables.add(inputStream);
closeables.add(outputStream);
all.addAll(closeables);
ResourceCloseDeamon.register(operation,closeables);
//用下面命令查看文件句柄,如果把上面register注释掉,就会发现句柄数量不断上升
//jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs  lsof -p  | grep /User/robin
System.gc();
}
}
}
复制代码

WeakHashMap

WeakHashMap实现原理很简单,它除了实现标准的Map接口,里面的机制也和HashMap的实现类似。从它entry子类中可以看出,它的key是用WeakReference封装的。

WeakHashMap里声明了一个queue,Entry继承WeakReference,构造函数中用key和queue关联构造一个weakReference。当key所封装的对象被GC回收后,GC自动将key注册到queue中。

WeakHashMap中有代码检测这个queue,取出其中的元素,找到WeakHashMap中相应的键值对进行remove。这部分代码就是expungeStaleEntries方法:

private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
复制代码

这段代码会在 resize,getTable,size 里执行,清除失效的entry。

FinalReference

在 Reference 的子类中,还有一个名为 FinalReference 的类,这个类用来做什么呢?

FinalReference仅仅继承了Reference,没有做其他的逻辑,只是将访问权限声明为package,所以我们不能够直接使用它。

需要关注的是其子类 Finalizer,看一下他的实现:

首先,哪些类对象是Finalizer reference类型的 reference 呢? 只要类覆写了Object 上的finalize方法,方法体非空。那么这个类的实例都会被Finalizer引用类型引用。这个工作是由虚拟机完成的,对于我们来说是透明的。

Finalizer 中有两个字段需要关注:

queue: private static ReferenceQueue queue = new ReferenceQueue() 即上文提到的ReferenceQueue,用来实现通知

unfinalized: private static Finalizer unfinalized 维护了一个未执行 finalize 方法的 reference 列表。维护静态字段unfinalized 的目的是为了一直保持对未未执行 finalize 方法的 reference 的强引用,防止被 gc 回收掉。

在 Finalizer 的构造函数中通过 add() 方法把 Finalizer 引用本身加入到 unfinalized 列表中,同时关联 finalizee 和 queue ,实现通知机制。

final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
same package as the Reference
class */
// 引用队列
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
// 链表结构
private Finalizer
next = null,
prev = null;
private Finalizer(Object finalizee) {
// 调用父类构造器 FinalReference<T> 传入 finalizee 作为 <T> referent
super(finalizee, queue);
add();
}
//...
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
//...
}
复制代码

Finalizer静态代码块里启动了一个 deamon 线程 FinalizerThread,FinalizerThread run方法不断的从 queue 中去取 Finalizer 类型的reference,然后调用 Finalizer 的 runFinalizer 方法,该方法最后执行了 referent 所重写的 finalize 方法。

private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
// 调用父类 Reference 的 get 方法获取 <T> referent
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
//静态代码块开启线程
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
//处理线程
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
// in case of recursive call to run()
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called.  Wait until JavaLangAccess is available
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
复制代码

观察上面的代码, hasBeenFinalized() 判断了 finalize 是否已经执行,如果执行,则把这个 referent 从 unfinalized 队列中移除。 所以,任何一个对象的 finalize 方法只会被系统自动调用一次。 当下一次GC发生时,由于 unfinalized 已经不再持有该对象的 referent,故该对象被直接回收掉。

从上面的过程也可以看出,覆盖了 finalize 方法的对象至少需要两次GC才可能被回收。第一次 GC 把覆盖了 finalize 方法的对象对应的 Finalizer reference 加入 referenceQueue 等待 FinalizerThread 来执行 finalize 方法。第二次 GC 才有可能释放 finalizee 对象本身,前提是 FinalizerThread 已经执行完 finalize 方法了,并把 Finalizer reference 从 Finalizer 静态 unfinalized 链表中剔除,因为这个链表和 Finalizer reference 对 finalizee 构成的是一个强引用。

HashMap 内存泄露

如果有一个值,对应的键不再使用他了,但由于key与value之间存在强引用,是不会被垃圾回收的。垃圾回收器跟踪活动的对象,只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。《Java 核心技术(一)》

public class TestJava {
public static void main(String[] args) {
HashMap map = new HashMap();
Test t1 = new Test();
Test t2 = new Test();
map.put(t1, "1");
map.put(t2, "2");
t1 = null;
System.gc();
System.out.println("第1步" + map);
t2 = null;
System.gc();
System.out.println("第2步" + map);
map.clear();
System.gc();
System.out.println("第3步" + map);
}
}
class Test {
private String strTest = "该Test对象还存在";
@Override
public String toString() {
return strTest;
}
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
System.out.println("该Test对象被释放了");
}
}
复制代码

日志

第1步{该Test对象还存在=2, 该Test对象还存在=1}
第2步{该Test对象还存在=2, 该Test对象还存在=1}
第3步{}
该Test对象被释放了
该Test对象被释放了
复制代码

显然,GC不会回收这两个垃圾,这个跟Java中的HashMap默认是强引用。

原因

HashMap存进去的是t1跟t2指向的地址(堆内存中两条黑色的线)作为key,但进行t1=null,t2=null的时候,本来按照常理来说,Java回收机制会对那些没有引用的堆内存对象进行回收,但不幸的是,HashMap依旧会强引用着t1跟t2的堆内存对象,导致GC无法对其进行回收。

如果是使用 软引用 作为 Value 的话呢?

public class TestJava2 {
public static void main(String[] args) {
HashMap map = new HashMap();
WeakReference t1 = new WeakReference<>(new Test2());
WeakReference t2 = new WeakReference<>(new Test2());
map.put(t1, "1");
map.put(t2, "2");
t1 = null;
System.gc();
System.out.println("第1步" + map);
System.out.println("第1步 get t2: " + map.get(t2));
t2 = null;
System.gc();
System.out.println("第2步" + map);
map.clear();
System.gc();
System.out.println("第3步" + map);
}
}
class Test2 {
private String strTest = "该Test对象还存在";
@Override
public String toString() {
return strTest;
}
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
System.out.println("该Test对象被释放了");
}
}
复制代码

日志

该Test对象被释放了
该Test对象被释放了
第1步{java.lang.ref.WeakReference@574caa3f=2, java.lang.ref.WeakReference@6842775d=1}
第1步 get t2: 2
第2步{java.lang.ref.WeakReference@574caa3f=2, java.lang.ref.WeakReference@6842775d=1}
第3步{}
复制代码

可以看出来,当触发 GC 的话,内存会被回收,对于平时的开发过程中,如果对于全局存储的数据,在内存不存时我们可以使用 软引用,需要在 GC 时清理的我们可以使用 弱引用。

小结

Java 4种引用的级别由高到低依次为: 强引用 > 软引用 > 弱引用 > 虚引用

级别 什么时候被垃圾回收 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象简单?缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 任何时候 跟踪对象被垃圾回收的活动 无,只记录对象销毁的事件

REFERENCES

https://www.cnblogs.com/liyutian/p/9690974.html
https://www.jianshu.com/p/f86d3a43eec5
https://www.jianshu.com/p/f0da6c1af815

小结

希望这次的整理可以帮助到看到此文的同学们,如果觉得还不错的话,右下角点个在看哦,谢谢支持啦!

微信扫一扫,分享到朋友圈

项目中的全局缓存导致了内存泄露?

被群嘲的她,还有救吗?

上一篇

Mybatis如何执行Select语句,你真的知道吗?

下一篇

你也可能喜欢

项目中的全局缓存导致了内存泄露?

长按储存图像,分享给朋友