您的位置:首页 > 编程语言 > Java开发

4 Java 内存回收及算法 — 引用及内存泄漏

2018-01-21 16:14 218 查看
本节要点

Java 引用

Java 内存泄漏的原因

垃圾回收机制的基本算法

堆内存的分代回收

内存管理小技巧

问题

1、JVM 在什么时候决定回收一个对象所占据的内存?

当一个 Java 对象失去引用时,JVM 的垃圾回收机制会自动清除对象,回收其所占用的内存空间。

也就是说,当 Java 对象被创建后,垃圾回收机制会实时的监控每个对象的运行状态,包括对象的空间申请、引用、被引用、赋值等。当监控到某个对象不再被引用变量所引用时,回收机制就会在回收时回收该对象所占用的空间。

2、JVM 会不会漏掉回收某些 Java 对象,由此造成内存泄漏?

不再使用的强引用没有置空时。

4.1 Java 引用的种类

Java 内存管理包括内存分配(创建 Java 对象时)和内存回收(回收 Java 对象时)。分配和回收任务都由 JVM 自动完成,因此也加重了 JVM 的工作,从而使 Java 程序运行较慢。

当通过关键字 new 创建对象时,即为 Java 对象申请内存空间,JVM 会在堆内存中为每个对象分配空间;当一个 Java 对象失去引用时,JVM 的垃圾回收机制会自动清除对象,回收其所占用的内存空间。

4.1.1 对象在内存中的状态

可以把 JVM 内存中对象引用理解成一种有向图,因为 Java 所有对象都是由一条一条线程创建出来的,因此可以把线程对象当成有向图的起始顶点。

例如,对于单线程,整个程序只有一条 main 线程,那么该图就是以 main 线程为顶点的有向图,main 顶点可达的对象都处于可达状态,不会被回收掉;如果某个对象在有向图中处于不可达状态,即该对象不再被引用,接下来会被垃圾回收机器所回收。

public static void main(String[] args){
Node n1 = new Node("第一个节点");
Node n2 = new Node("第二个节点");
Node n3 = new Node("第三个节点");

n1.next = n2;
n3 = n2;
n2 = null;
}




当一个对象在堆内存中运行时,对应有向图中的状态,可以把对象所处的状态分为如下3种:

1) 可达状态:有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象,程序可通过引用变量来调用该对象的属性和方法,那该对象就处于可达状态。

2) 可恢复状态:若程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象,系统的垃圾回收器准备回收该对象所占用的内存。

在回收该对象之前,系统会调用可恢复状态的对象的 finalize() 方法进行资源清理,如果 finalize() 方法重新让一个以上引用变量引用该对象,则该对象会再次变为可达状态;否则,将进入不可达状态。如:

public void test(){
String a = new String("aaa"); // ①
ea7e

a = new String("bbb"); // ②
}


当程序执行了 ② 代码后,字符串 “aaa” 对象处于可恢复状态,而字符串 “bbb” 对象处于可达状态。

3) 不可达状态:当对象的所有关联都被切断,且系统调用对象的 finalize() 方法依然没有使该对象变成可达状态,那该对象将永久性地失去引用,最后变成不可达状态。

采用有向图方式管理内存中的对象,可以方便的解决循环引用导致对象不能被回收的问题,如,有3个对象相互引用,A 引用 B,B 引用 C,C 又引用 A 对象,这样三者都没有失去引用,但从有向图的起始顶点(即进程根)不可到达它们,垃圾回收器就会回收它们。

采用有向图来管理内存中的对象具有精度高的优点,缺点是效率较低

4.1.2 四种引用类型

为了更好的管理对象引用,从 JDK 1.2 开始,Java 在 java.lang.ref 包下提供了3个类:SoftReference、PhantomReference、WeekReference,即软引用、虚引用和弱引用。

Java 对对象的引用有四种:强引用、软引用、虚引用和弱引用。

1. 强引用

程序创建一个对象,并把该对象赋给一个引用变量,该引用变量就是强引用

当对象被强引用变量所引用时,它处于可达状态,不会被回收。由于 JVM 肯定不会回收强引用所引用的 Java 对象,因此强引用是造成 Java 内存泄漏的主要原因之一

2. 软引用

软引用通过 SoftReference 类来实现,对于软引用对象,当系统内存空间足够时,它不会被系统回收;当系统内存空间不足时,系统将会回收该对象。

软引用通常用于对内存敏感的程序中,软引用是强引用很好的替代。

当需要大量创建某个类的新对象,而且有可能重新访问已创建老对象时,可以充分使用软引用来解决内存紧张的问题。

SoftReference<Person>[] people = new SoftReference[100000];
for (int i = 0; i < people.length; i++ ){
Person people = new Person("name"+i, i);
people[i] = new SoftReference<Person>(people);
}

System.out.println("回收前:"+people[0].get());
// 通知系统进行垃圾回收
System.gc();
System.out.println("回收后:"+people[0].get());


运行后输出:

回收前:Person[name=name0,age=0]
回收后:Person[name=name0,age=0]


上面程序创建了一个长度为 100 的 SoftReference 数组,当系统内存足够时,即使进行垃圾回收,垃圾回收器也不会回收这些 Person 对所占用的内存空间。

通过命令行指定最大内存为 2M 后(1M 启动不了 JVM),再执行 class 文件结果如下:

E:\JavaWeb_workspaces\dbBackup\src\com\zxk\test>javac -encoding utf-8 -d . SoftReferenceTest.java

E:\JavaWeb_workspaces\dbBackup\src\com\zxk\test>java -Xmx2m -Xms2m com/zxk/test/SoftReferenceTest
回收前:null
回收后:null


当使用 -Xmx2m 参数设置最大内存只有 2M 时,而创建一个长度为 100000 的数组会造成系统内存资源紧张,这种情况下,软引用所引用的 Java 对象将会被回收。

另外,最大堆内存不要设置为 1M,启动不了 JVM:



3. 弱引用

弱引用所引用对象的生存期更短,通过 WeakReference 类实现。对于只有弱引用的对象而言,当系统垃圾回收器运行时,不管系统内存是否足够,总会回收该对象所占用的内存。

public class WeakReferenceTest {
public static void main(String[] args){
String str = new String("疯狂Java讲义");
// 创建一个弱引用,让其引用到字符串
WeakReference<String> wr = new WeakReference<String>(str); // ①
// 断开强引用
str = null; // ②
System.out.println("回收前:"+wr.get()); // ③

System.gc();
System.out.println("回收后:"+wr.get());
}
}


执行结果:

回收前:疯狂Java讲义
回收后:null


程序中,创建了一个弱引用对象,并让该对象和 str 变量引用同一个对象,然后在 ② 代码中切断 str 对字符串的强引用,此时引用图如下:



此时该字符串只有一个弱引用对象引用它,程序依然可以通过这个弱引用对象来访问该字符串常量,所有③输出“回收前:疯狂Java讲义”,然后程序调用 gc(),强制垃圾回收,只有弱引用的对象被清理掉,输出为 null,表明该对象被清理了。

注意:该测试程序创建字符串对象时,不要使用 String str = “疯狂Java讲义”; ,这样将看不到效果,因为采用常量值定义字符串时,系统会缓存这个字符串常量(会使用强引用来引用它),而系统不会回收被缓存的字符串常量。

由于弱引用具有这种因为随时会被回收掉的不确定性,因此程序获取弱引用所引用的对象必须小心空指针异常。若程序需要使用该对象,应先判空,然后确定是否重新创建该对象。

// 取出弱引用所引用的对象
obj = wr.get();
if(obj == null){
// 重新创建对象,建议先使用强引用来引用它(考虑到回收时机的不确定性)
obj = recreateObj(); // 在使用前不会被回收
// 加上弱引用
wr = new WeakReference(obj);
}
... // 操作 obj

// 切断强引用,以便回收
obj = null;


与 WeakReference 功能类似的还有 WeakHashMap(与HashMap的用法类似)。实际中很少会直接使用 WeakReference 来引用某个 Java 对象,因为这种时候系统内存往往不会特别紧张。当程序有大量的对象需要使用弱引用来引用时,可以考虑使用 WeakHashMap 来保存这些对象。

在垃圾回收器运行之前,WeakHashMap 的功能与普通 HashMap 功能完全相似,但垃圾回收器运行时,WeakHashMap 中所有的 key - value 对都会被清空,除非某些 key 还有强引用在引用它们。

4. 虚引用

虚引用通过 PhantomReference 类实现,该引用对对象本身没有太大影响,完全类似于没有引用。如果一个对象只有一个虚引用,那和没有引用的效果大致相同。

虚引用的主要用于跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用对象是否即将被回收。虚引用不能单独使用,单独使用没有太大的意义,必须和引用队列(ReferenceQueue)联合使用。

引用队列用于保存被回收后对象的引用。系统回收被引用的对象后,会把被回收对象对应的引用添加到关联的引用队列中,这使得可以在对象被回收之前采取行动。

最后,使用这些特殊的引用类,就不能保留对对象的强引用,否则,就会失去这些类所提供的任何好处。

4.2 Java 的内存泄漏

程序在运行过程中,系统会不断的分配内存空间,那些不再使用的内存空间应该及时回收掉,保证系统可以再次使用这些内存,若存在无用的内存没有被回收,那就是内存泄漏

对于 C++ 程序,若程序员忘了回收无用对象,即产生内存泄漏;

对于 Java 程序,若程序中有一些对象处于可达状态,即使不会再被引用,它们所占用的内存空间也不会被回收,它们所占用的空间也会产生内存泄漏。下图显示了内存泄漏示意图:



如 ArrayList 中 remove(int index)方法代码中的 elementData[–size] = null; 这句就是为了避免内存泄漏的代码,置空以便垃圾回收器回收。



参考资料:

疯狂Java:突破程序员基本功的16课-Java 的内存回收
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐