写点什么

JVM 引用数据类型分析

作者:andy
  • 2022-11-12
    北京
  • 本文字数:4948 字

    阅读完需:约 16 分钟

JVM 引用数据类型分析

一、根搜索算法


把内存中的每个对象看作一个节点,定义对象作为根节点“GC Roots”。如果一个对象有另一个对象引用,则认为这个对象有一条指向另一个对象的边。


二、根节点(GC Roots)类型


  • 第一种:虚拟机栈中的引用的对象;

  • 第二种:类中定义的全局的静态对象;

  • 第三种:使用关键字 static final 的常量引用;

  • 第四种:JNI 技术,使用 native 方法。


三、JVM 中的引用类型


JVM 中主要分为四种引用类型,分别是“强引用”、“软引用”、“弱引用”、“幽灵引用”,接下来就说明这四种引用。


(1)强引用


使用关键 new 生成的对象的引用,如 Object obj = new Object(),只要 obj 对象生命周期未结束,或者没有显示把 obj 赋为 null,JVM 不会进行回收。


当内存容量不足时,宁愿出现 OOM 错误,也要一直保存的引用,不会对其对象进行回收。强引用是 Java 默认支持的模式,只要栈内存中还有指向堆内存的引用,则当内存不足时,只会抛出 OutOfMemoryError 错误,不会对对象进行回收。


因强引用是我们开发一直使用的模式,并且具有这样的异常错误产生,所以,尽量少实例化对象,避免因无法回收对象造成内存溢出。


代码示例如下:


/** * 强引用代码示例 * @author andy */public class StrongReferenceDemo {
private static Logger logger = LoggerFactory.getLogger(StrongReferenceDemo.class);
public static void main(String[] args) { // 对象声明 Object object = new Object();
// 垃圾回收前对象日志打印 logger.info("垃圾回收前对象日志打印:{}", object);
// 执行垃圾回收 System.gc();
// 垃圾回收后进行对象日志打印 // 打印结果显示对象依然存在 logger.info("垃圾回收后进行对象日志打印:{}", object);
}}
复制代码


程序执行结果如下:


INFO  StrongReferenceDemo - 垃圾回收前对象日志打印:java.lang.Object@5123a213INFO  StrongReferenceDemo - 垃圾回收后进行对象日志打印:java.lang.Object@5123a213
复制代码


由此测试可知,强引用的对象是不会被垃圾收集器回收的。


(2)软引用


当内存不足时,才会对软引用对象进行回收,常应用于高速缓存。Mybatis 持久层开发框架使用的高速缓存,采用的便是软引用方式。


对于当下的开源项目,使用软引用作为项目的缓存组件。当内存充足时,不进行对象回收;反之,则进行对象回收。软引用使用 java.lang.ref.SoftReference 类实现,这里说明一下,所有的引用类都在 java.lang.ref 包下。


SoftReference 类的主要使用方法如下:


  • 构造方法:public SoftReference​(T referent)

  • 取得对象:public T get​()


示例代码如下:


/** * 软引用代码示例 * @author andy */public class SoftReferenceDemo {
private static Logger logger = LoggerFactory.getLogger(SoftReferenceDemo.class);
public static void main(String[] args) {
// 初始化对象 String softReferenceStr = "soft reference"; SoftReference<String> softReference = new SoftReference<>(softReferenceStr);
// 为了体现软引用,将 softReferenceStr 赋值为空 softReferenceStr = null; logger.info("垃圾回收前的软引用对象地址:{}", softReference.get());
// 为了体现软引用被垃圾回收器回收,借助另一个对象进行持续性赋值,最后抛出内存异常错误 // 这样的情况下,模拟出最后内存不够,进而进行 GC 操作 String str = "Garbage"; try { while (true) { str += str; } } catch (Throwable e) { // 由于内存溢出是Error错误,故而以捕获 Throwable 接口实现类为主 logger.error("内存溢出异常", e); } finally { System.gc(); try { // 垃圾回收需要时间,故而等待 10 s Thread.sleep(100000); } catch (Exception e) { logger.error("线程类延时程序异常", e); } logger.info("软引用垃圾回收处理后对象地址:{}", softReference.get()); } }
}
复制代码


程序执行结果日志如下:


INFO  SoftReferenceDemo - 垃圾回收前的软引用对象地址:soft referenceERROR SoftReferenceDemo - 内存溢出异常java.lang.OutOfMemoryError: Java heap space	at java.base/java.util.Arrays.copyOfRange(Arrays.java:4030)	at java.base/java.lang.StringLatin1.newString(StringLatin1.java:715)	at java.base/java.lang.StringBuilder.toString(StringBuilder.java:448)	at com.ailpha.practice.reference.SoftReferenceDemo.main(SoftReferenceDemo.java:31)INFO  SoftReferenceDemo - 软引用垃圾回收处理后对象地址:soft reference
复制代码


这里先留下一个小问题,留待未来进行处理。


(3)弱引用


无论内存是否充足,只要出现了垃圾,便会对其进行回收处理。可以这么理解,简直是“弱不禁风,弱爆了”。


当内存进行 GC 处理时,弱引用对象便会进行垃圾回收,因此,不建议使用。一旦发生 GC,则必须进行对象处理,很容易数据丢失。对于弱引用的实现,可以采用以下类方式:java.util.WeakHashMap 和 WeakReference。


代码示例 1:


/** * 弱引用代码示例 * @author andy */public class WeakReferenceDemo {
private static Logger logger = LoggerFactory.getLogger(WeakReferenceDemo.class);
public static void main(String[] args) {
// 初始化对象 String key = new String("stack"); String value = "heap";
// 弱引用使用 WeakHashMap 类实现 Map<String,String> weakReference = new WeakHashMap<>(); weakReference.put(key,value);
// 垃圾回收前的对象情况 logger.info("弱引用的对象情况:{}",weakReference);
// 对key进行赋值为 null key = null; logger.info("垃圾回收前的弱引用的对象情况:{}",weakReference);
// 垃圾回收查看弱引用对象情况 System.gc(); logger.info("垃圾回收后的弱引用的对象情况:{}",weakReference);
// 延伸思考,new String()和直接赋值结果是否相同? // String key = "stack"; // String value = "heap";
}
}
复制代码


执行结果如下:


INFO  WeakReferenceDemo - 弱引用的对象情况:{stack=heap}INFO  WeakReferenceDemo - 垃圾回收前的弱引用的对象情况:{stack=heap}INFO  WeakReferenceDemo - 垃圾回收后的弱引用的对象情况:{}
复制代码


代码示例 2:


/** * 弱引用代码示例 * @author andy * */public class WeakReferenceDemo2 {
private static Logger logger = LoggerFactory.getLogger(WeakReferenceDemo2.class);
public static void main(String[]atgs){
// 对象初始化 String str = new String("weakReference");
// 使用 WeakReference 类实现弱引用 WeakReference<String> weakReference = new WeakReference<>(str); logger.info("弱引用的对象情况:{}", weakReference.get());
// 对象赋值为空,解除强引用 str = null; logger.info("垃圾回收前的弱引用的对象情况:{}", weakReference.get());
// 垃圾回收 System.gc(); logger.info("垃圾回收后的弱引用的对象情况:{}", weakReference.get());
// 延伸思考,new String()和直接赋值,结果是否相同? // String str = "weakReference"; }
}
复制代码


执行结果如下:


INFO  WeakReferenceDemo2 - 弱引用的对象情况:weakReferenceINFO  WeakReferenceDemo2 - 垃圾回收前的弱引用的对象情况:weakReferenceINFO  WeakReferenceDemo2 - 垃圾回收后的弱引用的对象情况:null
复制代码


使用弱引用需要注意以下几点:


1、弱引用不建议使用,就是因为一旦发生 GC,就会被清空;

2、使用字符串举例,需要小心对象自动入池的问题,一旦入池,是无法清空的,所以,建议使用 new String()方式;

3、字符串对象一旦声明,不可修改。


(4)幽灵引用(虚引用)


幽灵引用相当于没有引用。


永远得不到的数据叫做幽灵引用,更多意义在于提出了一种思想。所有保存在幽灵引用中的数据都不会真正保留。对象无法回收,finalize() 方法无法执行。幽灵引用可以使用以下类实现:java.lang.ref.PhantomReference


/** * 幽灵引用代码示例 * @author andy */public class PhantomReferenceDemo {
private static Logger logger = LoggerFactory.getLogger(PhantomReferenceDemo.class);
public static void main(String[] args) throws Exception{
// 对象初始化 String str = new String("PhantomReference");
// 引用队列 ReferenceQueue<String> referenceQueue = new ReferenceQueue<>(); // 幽灵引用 PhantomReference<String> ref = new PhantomReference<>(str,referenceQueue); logger.info("引用队列的对象情况:{}", referenceQueue.poll());
// 垃圾回收 System.gc(); logger.info("垃圾回收后的引用队列的对象情况:{}", referenceQueue.poll()); }
}
复制代码


执行结果如下:


INFO  PhantomReferenceDemo - 引用队列的对象情况:nullINFO  PhantomReferenceDemo - 垃圾回收后的引用队列的对象情况:null
复制代码


四、引用队列


引用队列就是对要回收的对象进行保存,在对象回收前可以进行操作处理。对于对象的回收都是从根对象开始向下扫描的。对于 GC 而言,想要确定哪些对象被回收,确定好引用的强度即引用路径的设定。以下展示引用路径。



对于对象的引用,要考虑到引用的关联,因此必须找到强关联。爲了避免非强关联对象所带来的内存引用的问题,故而提出了引用队列的概念。如果创建软引用和弱引用使用了引用队列,那么,在引用对象回收前会保存到引用队列之中。引用队列的主要作用就是对被回收对象的控制。可以使用 java.lang.ref.ReferenceQueue 类实现。


代码示例如下:


/** * 引用队列代码示例 * @author andy */public class ReferenceQueueDemo {
private static Logger logger = LoggerFactory.getLogger(ReferenceQueueDemo.class);
public static void main(String[]atgs) throws Exception{
// 对象初始化 String str = new String("referenceQueue");
// 引用队列 ReferenceQueue<String> referenceQueue = new ReferenceQueue<>(); WeakReference<String> ref = new WeakReference<>(str, referenceQueue);
// 引用队列情况 logger.info("引用队列的对象情况:{}", referenceQueue.poll());
// 对象赋值为null str = null; logger.info("垃圾回收前的引用队列的对象情况:{}", referenceQueue.poll());
// 垃圾处理 System.gc(); Thread.sleep(100); logger.info("垃圾回收后的引用队列的对象情况:{}", referenceQueue.poll());
// 延伸思考,new String()和直接赋值的结果是否相同? // String obj = "referenceQueue"; }
}
复制代码


执行结果如下:


INFO  ReferenceQueueDemo - 引用队列的对象情况:nullINFO  ReferenceQueueDemo - 垃圾回收前的引用队列的对象情况:nullINFO  ReferenceQueueDemo - 垃圾回收后的引用队列的对象情况:java.lang.ref.WeakReference@6d3af739
复制代码


五、总结


本文主要是对 JVM 中的引用类型进行相应的解释分析,并附上了代码示例。以上介绍的引用,排除强引用之外,虽然在很多开发场景下不怎么使用到,但是,这对于理解 JVM 的机制以及引用机制还是有帮助的。


这里可以做一个思考延伸,问题如下:


1、String 字符串如何存储?构造方法(new String(""))和直接赋值("")有何区别?

2、1、WeakHashMap 和 HashMap 的区别?


发布于: 刚刚阅读数: 3
用户头像

andy

关注

强准备 + 强执行 + 强信仰 2019-11-21 加入

以前是T型人才,当下是π型人才,未来是梳子型人才

评论

发布
暂无评论
JVM 引用数据类型分析_andy_InfoQ写作社区