02 Java的引用类型以及应用场景
1 JVM如何判断对象可以回收?
1-1 概述
1)采用引用计数法,引用为0的对象就是可以回收的对象。
- 存在问题:循环引用的问题需要去特殊考虑。
2)可达性分析算法(JVM实际采用的方法)
- 基本思想:需要确定一系列根对象(根对象肯定不能被垃圾回收),然后对空间进行扫描,判断每一个对象是否直接或者间接被根对象引用,如果是的话,那么这个对象不能被垃圾回收。
方法论:可达性分析算法中哪些对象可以作为根对象?
结合JMAP与内存分析工具
jmap -dump:format=b,file=filename pid
jmap -dump:format=b,live,file=file2.bin 7640 // live确保dump之前进行一次垃圾回收
实例
package part2;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class test1 {
public static void main(String[] args) throws IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();
list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end");
}
}
- 上面代码中在打印1和2之后分别用jmap命令保存内存状态,获得2个bin文件。
jmap -dump:format=b,live,file=file1.bin 16180
jmap -dump:format=b,live,file=file2.bin 16180
使用MAP工具打开file1.bin
1)使用GC root工具
2)可以看到该工具将root类分为四种类型:
- system class:系统类都是核心类,虚拟机运行过程中核心对象
- JNI(Java Native Interface) Global:操作系统资源调用时所使用的Java类对象
- Busy Montor: 锁对象(锁对象作为root类可以保证由于加锁被阻塞的类不会被垃圾回收)
- Thread:线程中的对象
3)main线程中可以看到当前正在被引用的ArrayList<>()对象 。
List<Object> list1 = new ArrayList<>();
4)打开file2.bin,可以发现ArrayList<>()对象 不在主线程中,已经被回收了。
作为 GC Roots 的对象包括下面几种:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
2)各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
3)方法区中类静态属性引用的对象;java 类的引用类型静态变量。
4)常量引用的对象;比如:字符串常量池里的引用。
5) 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
6) 所有被同步锁(synchronized 关键)持有的对象。(非重点)
7) JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。(非重点)
8)JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(非重点)
9)JVM 实现中的“临时性”对象,跨代引用的对象
1-2 Java的引用类型(重要知识点)
1)强引用(上图中的实现都是强引用)
- 特点
- 沿着GC root对象的引用链能够找到该对象。上图A1对象与C对象之间就是强引用
- 强引用对象不会被垃圾回收
2)软引用(下图中的A2对象就是软引用的对象)(SoftReference )
软引用特点:
- 软引用对象在没有其他对象引用的情况下,内存不足时,JVM会垃圾回收这种类型的对象。
3)弱引用(上图中的A3对象就是软引用)(WeakReference )
特点:
- 只要发生垃圾回收,弱引用对象都会被回收。
软引用与弱引用对象的区别?
**共同点: **软引用对象如果没有被强引用都会被垃圾回收。可以配合引用队列,也可以不配合引用队列使用。
不同点: 回收的时机存在差异,弱引用只要发生GC,必定被回收,软引用在内存不足的时候,GC时才会回收。
注意:
- 软弱引用的对象被垃圾回收后,引用本身会进入到引用队列,由于引用本身也占用空间,如果像释放
引用所占的空间,那么就通过引用队列找到哪些引用并释放。
虚引用与终结器引用的特点:这两种类型的引用的必须关联引用队列
4)虚引用(PhantomReference )
典型应用:当虚引用的对象被垃圾回收的时候,虚引用本身就会被放入引用队列,会有一个专门的线程扫描引用队列,调用虚引用关联的方法。
实例:比如直接内存由于不受JVM垃圾回收管理,因此需要通过虚引用的方式去对内存进行释放,具体的流程就是
虚引用cleaner存储直接内存的地址,当虚引用的的对象bytebuffer被垃圾回收后会进入队列,该队列会有线程扫描找到虚引用,调用unsafe的本地方法释放直接内存。
特点:如果一个对象仅持有虚引用,那它就和没有任何引用一样。
5)终结器引用(FinalReference )
特点:
- 终结器引用配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象
注意:Finalizer 线程的优先级非常低,导致垃圾回收并不太及时,因此终结器引用很少使用到。
五种引用的总结
引用名称 | 与垃圾回收的关系 | 备注 |
---|---|---|
强引用 | 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 | 最常见的应引用 |
软引用 | 1)仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。2)可以配合引用队列来释放软引用自身 | 注意软引用对象垃圾回收的时机, 常用来实现缓存技术,比如网页缓存,图片缓存等 |
弱引用 | 1)仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。2)可以配合引用队列来释放弱引用自身 | 注意与软引用区分 |
虚引用 | 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 | 主要用于直接内存释放 |
终结器引用 | 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象 | 垃圾回收效率低 |
1-3 软引用的应用场景
场景下的问题:一些大体积资源加载到内存中进行显示,但是这些资源又不是特别重要,如果采用强引用的方式,由于这些大体积的资源无法及时得到释放,很容易造成heap space溢出。
准备:通过JVM参数限制堆内存大小为20M
package part2;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/*-Xmx20m 设置堆内存大小为20M,每个byte数组大小为4M
* */
public class test2 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
}
执行结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at part2.test2.main(test2.java:19)
溢出原因:堆内存资源只有20M,但是5个byte数组资源就占用了20M,造成堆内存不足,又由于这些byte数组都被根对象强引用,所以无法进行垃圾回收,造成堆内存溢出exception.
软引用的应用(内存敏感的应用有益处)
package part2;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用
* -XX:+PrintGCDetails -verbose:gc (打印垃圾回收的详细信息)
*/
/*-Xmx20m (设置堆内存大小为20M,每个byte数组大小为4M)
*
* */
public class test2 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
// list --> SoftReference --> byte[]
/*让list先引用软引用,然后软引用在引用byte数组*/
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
执行结果
上述代码中通过弱引用去引用byte数组,这样当内存不足的时候发生垃圾回收,会将之前的弱引用对象全部会后,从结果中可以看到垃圾回收发生在第5个byte数组new出来之前。
[B@45ee12a7
1
[B@330bedb4
2
[B@2503dbd3
3
[B@4b67cf4d
4
[B@7ea987ac
5
循环结束:5
null
null
null
null
[B@7ea987ac
第二次循环,发现前面四个byte数组已经被垃圾回收了。
设置详细的垃圾回收信息打印
执行结果
[B@45ee12a7
1
[B@330bedb4
2
[B@2503dbd3
3 // 内存不足了,第一次对YoungGen进行垃圾回收
[GC (Allocation Failure) [PSYoungGen: 2338K->488K(6144K)] 14626K->13019K(19968K), 0.0071512 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@4b67cf4d
4 // 第二次对YoungGen进行垃圾回收
[GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17228K->17244K(19968K), 0.0012066 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 第三次触发Full GC,可以看到回收后的内存占用并没有下降多少。
[Full GC (Ergonomics) [PSYoungGen: 4696K->4560K(6144K)] [ParOldGen: 12547K->12497K(13824K)] 17244K->17058K(19968K), [Metaspace: 3537K->3537K(1056768K)], 0.0057532 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
// 内存依旧不足,造成内存分配失败
[GC (Allocation Failure) --[PSYoungGen: 4560K->4560K(6144K)] 17058K->17082K(19968K), 0.0007331 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 第四次触发GC,这个时候之前的软引用对象进行了回收,可以看到内存占用下降了很多
[Full GC (Allocation Failure) [PSYoungGen: 4560K->0K(6144K)] [ParOldGen: 12521K->655K(8704K)] 17082K->655K(14848K), [Metaspace: 3537K->3537K(1056768K)], 0.0079717 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@7ea987ac
5
循环结束:5
null
null
null
null
[B@7ea987ac
Heap
PSYoungGen total 6144K, used 4376K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 77% used [0x00000000ff980000,0x00000000ffdc6288,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 8704K, used 655K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
object space 8704K, 7% used [0x00000000fec00000,0x00000000feca3e80,0x00000000ff480000)
Metaspace used 3544K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 396K, capacity 428K, committed 512K, reserved 1048576K
总结:从上面过程中可以看到,软引用的特点
- 垃圾回收的时机是堆内存分配失败,不足的时候,才会被真正回收
- 软引用与弱引用可以应用于一些对于内存比较敏感的场景。
1-4 使用引用队列清理无用的软引用
package part2;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.*;
/*虚拟机环境配置如下:
*-Xmx20m 设置堆内存大小为20M,每个byte数组大小为4M
* */
public class test3 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// 将软引用对象与引用队列进行关联
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
Reference<? extends byte[]> poll = queue.poll();
// 从引用队列中确定哪些引用没有作用了并进行移除
while(poll != null){
list.remove(poll);
poll = queue.poll();
}
System.out.println("=================");
// 链表中无用的引用在上一步中已经被移除
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
执行结果
通过配合引用队列,移除没有作用的软引用对象。
[B@45ee12a7
1
[B@330bedb4
2
[B@2503dbd3
3
[B@4b67cf4d
4
[B@7ea987ac
5
循环结束:5
=================
[B@7ea987ac
1-5 弱引用的应用
package part2;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.*;
/*虚拟机环境配置如下:
*-Xmx20m -XX:+PrintGCDetails -verbose:gc设置堆内存大小为20M,每个byte数组大小为4M
* */
public class test4 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// 将软引用对象与引用队列进行关联
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
System.out.println("========================");
for (WeakReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
执行结果
- 弱引用对象会被垃圾回收
[B@45ee12a7
1
[B@330bedb4
2
[B@2503dbd3
3
[GC (Allocation Failure) [PSYoungGen: 2338K->488K(6144K)] 14626K->13023K(19968K), 0.0018467 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@4b67cf4d
4
[GC (Allocation Failure) [PSYoungGen: 4696K->488K(6144K)] 17232K->13031K(19968K), 0.0010357 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@7ea987ac
5
循环结束:5
========================
[B@45ee12a7
[B@330bedb4
[B@2503dbd3
null
[B@7ea987ac
Heap
PSYoungGen total 6144K, used 4752K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 75% used [0x00000000ff980000,0x00000000ffdaa018,0x00000000fff00000)
from space 512K, 95% used [0x00000000fff80000,0x00000000ffffa040,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 13824K, used 12543K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
object space 13824K, 90% used [0x00000000fec00000,0x00000000ff83fdb0,0x00000000ff980000)
Metaspace used 3544K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 396K, capacity 428K, committed 512K, reserved 1048576K