概念
Java 执行 GC 判断对象是否存活有两种方式:
- 引用计数
- Java 堆中每一个对象都有一个引用计数属性,引用每新增 1 次计数加 1,引用每释放 1 次计数减 1。
- 对象可达性分析:通过一些列:“GC ROOTS” 的对象作为起点进行向下搜索,一旦对象没有在 “GC ROOTS” 中没有相连的引用链,那么该对象是可回收的;
在Java语言中,可作为GC Roots的对象包含以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
- 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
- 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
- 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)
在 JDK 1.2 以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于(reachable)可达状态,程序才能使用它。
从 JDK 1.2 版本开始,对象的引用被划分为 4 种级别,从而使程序能更加灵活地控制对象的生命周期。
这 4 种级别由高到低依次为:
- 强引用
- 软引用
- 弱引用
- 虚引用
Java 中提供这四种引用类型主要有两个目的:
- 可以让程序员通过代码的方式决定某些对象的生命周期
- 有利于JVM进行垃圾回收
强引用(StrongReference)
强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的 object 和 str 都是强引用:
public void test() {
String str = "hello";
}
只要某个对象有强引用与之关联,GC 必定不会回收这个对象,即使在内存不足的情况下,Java 虚拟机宁愿抛出OutOfMemoryError 错误也不会回收这种对象。比如下面这段代码:
public static void main(String[] args) {
fun1();
}
public static void fun1() {
Object object = new Object();
Object[] objArr = new Object[1000];
}
当运行至 Object[] objArr = new Object[1000]
这段代码时,如果内存不足,JVM 会抛出 OOM 错误也不会回收 object 指向的对象。
不过要注意的是,当 fun1 运行完之后,object 和 objArr 都已经不存在了,所以它们指向的对象都会被 JVM 回收。
如果强引用对象不使用时,需要弱化从而使GC
能够回收,如下:
str = null;
显式地设置 str
对象为 null
,或让其超出对象的生命周期范围,则 GC
认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于 GC
算法。
在一个方法的内部有一个强引用,这个引用保存在 Java 栈中,而真正的引用内容 (Object) 保存在 Java 堆中。
当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。
但是如果这个 str 是全局变量时,就需要在不用这个对象时赋值为 null,因为强引用不会被垃圾回收。
软引用(SoftReference)
软引用对象,由垃圾收集器根据内存需求自行清除。软引用最常用于实现对内存敏感的缓存。
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
public void test() {
String str = new String("SoftReference");
SoftRenference<String> soft = new SoftRenference(str);
}
通过一个 SoftRenference 对象来创建一个软引用。
软引用还可以和一个引用队列(ReferenceQueue
)联合使用。如果软引用所引用对象被垃圾回收,JAVA
虚拟机就会把这个软引用加入到与之关联的引用队列中。
@Test
public void test1() {
String string = "ReferenceQueue";
ReferenceQueue<String> queue = new ReferenceQueue<>();
SoftReference<String> reference = new SoftReference<>(string, queue);
System.out.println(reference.get()); // ReferenceQueue
System.out.println(queue.poll()); // null
string = null;
System.gc();
System.out.println(reference.get()); // ReferenceQueue
System.out.println(queue.poll()); // null
}
注意:
软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用,JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。
当内存不足时,JVM
首先将软引用中的对象引用置为null
,然后通知垃圾回收器进行回收。也就是说,GC 会在虚拟机抛出 OutOfMemoryError 之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的“较新的”软对象会被虚拟机尽可能保留,这就是引入引用队 ReferenceQueue 的原因。
应用场景:
-
浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
-
如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
伪代码例子:
public void test() {
// 获取浏览器对象进行浏览
Browser browser = new 浏览器对象
// 从后台程序加载浏览页面
BrowserPage page = browser.getPage();
// 将浏览完毕的页面置为软引用
SoftReference softReference = new SoftReference(page);
// 回退或者再次浏览此页面时
if(softReference.get() != null) {
// 内存充足,还没有被回收器回收,直接获取缓存
page = softReference.get();
} else {
// 内存不足,软引用的对象已经回收
page = browser.getPage();
// 重新构建软引用
softReference = new SoftReference(page);
}
}
弱引用(WeakReference)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
实例代码:
@Test
public void test2() {
String string = new String("WeakRenference");
WeakReference<String> weakReference = new WeakReference<>(string);
System.out.println(weakReference.get()); // WeakRenference
string = null;
System.gc();
System.out.println(weakReference.get()); // null
}
注意:
如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的 GC 垃圾回收,那么应该用 WeakReference 来记住此对象。
下面的代码会让一个弱引用再次变为一个强引用:
public void test() {
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
// 弱引用转强引用
String strongReference = weakReference.get();
}
虚引用(PhantomReference)
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
应用场景:
- 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:
- 虚引用必须和引用队列(ReferenceQueue)联合使用。
- 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
public void test() {
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
}
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
进一步理解软引用和弱引用
对于强引用,我们平时在编写代码时经常会用到。而对于其他三种类型的引用,使用得最多的就是软引用和弱引用,这 2 种既有相似之处又有区别。它们都是用来描述非必需对象的,但是被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在 JVM 进行垃圾回收时总会被回收。
在 SoftReference 类中,有三个方法,两个构造方法和一个 get 方法(WekReference 类似):
两个构造方法:
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
get 方法用来获取与软引用关联的对象的引用,如果该对象被回收了,则返回 null。
在使用软引用和弱引用的时候,我们可以显示地通过 System.gc() 来通知 JVM 进行垃圾回收,但是要注意的是,虽然发出了通知,JVM 不一定会立刻执行,也就是说这句是无法确保此时JVM一定会进行垃圾回收的。
如何利用软引用和弱引用解决OOM问题
下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:
用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题。在 Android 开发中对于大量图片下载会经常用到。
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
public void addBitmapToCache(String path) {
// 强引用的Bitmap对象
Bitmap bitmap = BitmapFactory.decodeFile(path);
// 软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
// 添加该对象到Map中使其缓存
imageCache.put(path, softBitmap);
}
public Bitmap getBitmapByPath(String path) {
// 从缓存中取软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
// 判断是否存在软引用
if (softBitmap == null) {
return null;
}
// 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
Bitmap bitmap = softBitmap.get();
return bitmap;
}
总结
Java 中 4 种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用
当垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object来标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。
通过表格来说明一下,如下:
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM 停止运行时终止 |
软引用 | 当内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 正常垃圾回收时 | 对象缓存 | 垃圾回收后终止 |
虚引用 | 正常垃圾回收时 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
Q.E.D.
Comments | 0 条评论