4 Java 内存回收及算法 — 引用及内存泄漏
2018-01-21 16:14
218 查看
本节要点
Java 引用
Java 内存泄漏的原因
垃圾回收机制的基本算法
堆内存的分代回收
内存管理小技巧
当一个 Java 对象失去引用时,JVM 的垃圾回收机制会自动清除对象,回收其所占用的内存空间。
也就是说,当 Java 对象被创建后,垃圾回收机制会实时的监控每个对象的运行状态,包括对象的空间申请、引用、被引用、赋值等。当监控到某个对象不再被引用变量所引用时,回收机制就会在回收时回收该对象所占用的空间。
2、JVM 会不会漏掉回收某些 Java 对象,由此造成内存泄漏?
不再使用的强引用没有置空时。
当通过关键字 new 创建对象时,即为 Java 对象申请内存空间,JVM 会在堆内存中为每个对象分配空间;当一个 Java 对象失去引用时,JVM 的垃圾回收机制会自动清除对象,回收其所占用的内存空间。
例如,对于单线程,整个程序只有一条 main 线程,那么该图就是以 main 线程为顶点的有向图,main 顶点可达的对象都处于可达状态,不会被回收掉;如果某个对象在有向图中处于不可达状态,即该对象不再被引用,接下来会被垃圾回收机器所回收。
当一个对象在堆内存中运行时,对应有向图中的状态,可以把对象所处的状态分为如下3种:
1) 可达状态:有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象,程序可通过引用变量来调用该对象的属性和方法,那该对象就处于可达状态。
2) 可恢复状态:若程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象,系统的垃圾回收器准备回收该对象所占用的内存。
在回收该对象之前,系统会调用可恢复状态的对象的 finalize() 方法进行资源清理,如果 finalize() 方法重新让一个以上引用变量引用该对象,则该对象会再次变为可达状态;否则,将进入不可达状态。如:
当程序执行了 ② 代码后,字符串 “aaa” 对象处于可恢复状态,而字符串 “bbb” 对象处于可达状态。
3) 不可达状态:当对象的所有关联都被切断,且系统调用对象的 finalize() 方法依然没有使该对象变成可达状态,那该对象将永久性地失去引用,最后变成不可达状态。
采用有向图方式管理内存中的对象,可以方便的解决循环引用导致对象不能被回收的问题,如,有3个对象相互引用,A 引用 B,B 引用 C,C 又引用 A 对象,这样三者都没有失去引用,但从有向图的起始顶点(即进程根)不可到达它们,垃圾回收器就会回收它们。
采用有向图来管理内存中的对象具有精度高的优点,缺点是效率较低。
Java 对对象的引用有四种:强引用、软引用、虚引用和弱引用。
1. 强引用
程序创建一个对象,并把该对象赋给一个引用变量,该引用变量就是强引用。
当对象被强引用变量所引用时,它处于可达状态,不会被回收。由于 JVM 肯定不会回收强引用所引用的 Java 对象,因此强引用是造成 Java 内存泄漏的主要原因之一。
2. 软引用
软引用通过 SoftReference 类来实现,对于软引用对象,当系统内存空间足够时,它不会被系统回收;当系统内存空间不足时,系统将会回收该对象。
软引用通常用于对内存敏感的程序中,软引用是强引用很好的替代。
当需要大量创建某个类的新对象,而且有可能重新访问已创建老对象时,可以充分使用软引用来解决内存紧张的问题。
运行后输出:
上面程序创建了一个长度为 100 的 SoftReference 数组,当系统内存足够时,即使进行垃圾回收,垃圾回收器也不会回收这些 Person 对所占用的内存空间。
通过命令行指定最大内存为 2M 后(1M 启动不了 JVM),再执行 class 文件结果如下:
当使用 -Xmx2m 参数设置最大内存只有 2M 时,而创建一个长度为 100000 的数组会造成系统内存资源紧张,这种情况下,软引用所引用的 Java 对象将会被回收。
另外,最大堆内存不要设置为 1M,启动不了 JVM:
3. 弱引用
弱引用所引用对象的生存期更短,通过 WeakReference 类实现。对于只有弱引用的对象而言,当系统垃圾回收器运行时,不管系统内存是否足够,总会回收该对象所占用的内存。
执行结果:
程序中,创建了一个弱引用对象,并让该对象和 str 变量引用同一个对象,然后在 ② 代码中切断 str 对字符串的强引用,此时引用图如下:
此时该字符串只有一个弱引用对象引用它,程序依然可以通过这个弱引用对象来访问该字符串常量,所有③输出“回收前:疯狂Java讲义”,然后程序调用 gc(),强制垃圾回收,只有弱引用的对象被清理掉,输出为 null,表明该对象被清理了。
注意:该测试程序创建字符串对象时,不要使用 String str = “疯狂Java讲义”; ,这样将看不到效果,因为采用常量值定义字符串时,系统会缓存这个字符串常量(会使用强引用来引用它),而系统不会回收被缓存的字符串常量。
由于弱引用具有这种因为随时会被回收掉的不确定性,因此程序获取弱引用所引用的对象必须小心空指针异常。若程序需要使用该对象,应先判空,然后确定是否重新创建该对象。
与 WeakReference 功能类似的还有 WeakHashMap(与HashMap的用法类似)。实际中很少会直接使用 WeakReference 来引用某个 Java 对象,因为这种时候系统内存往往不会特别紧张。当程序有大量的对象需要使用弱引用来引用时,可以考虑使用 WeakHashMap 来保存这些对象。
在垃圾回收器运行之前,WeakHashMap 的功能与普通 HashMap 功能完全相似,但垃圾回收器运行时,WeakHashMap 中所有的 key - value 对都会被清空,除非某些 key 还有强引用在引用它们。
4. 虚引用
虚引用通过 PhantomReference 类实现,该引用对对象本身没有太大影响,完全类似于没有引用。如果一个对象只有一个虚引用,那和没有引用的效果大致相同。
虚引用的主要用于跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用对象是否即将被回收。虚引用不能单独使用,单独使用没有太大的意义,必须和引用队列(ReferenceQueue)联合使用。
引用队列用于保存被回收后对象的引用。系统回收被引用的对象后,会把被回收对象对应的引用添加到关联的引用队列中,这使得可以在对象被回收之前采取行动。
最后,使用这些特殊的引用类,就不能保留对对象的强引用,否则,就会失去这些类所提供的任何好处。
对于 C++ 程序,若程序员忘了回收无用对象,即产生内存泄漏;
对于 Java 程序,若程序中有一些对象处于可达状态,即使不会再被引用,它们所占用的内存空间也不会被回收,它们所占用的空间也会产生内存泄漏。下图显示了内存泄漏示意图:
如 ArrayList 中 remove(int index)方法代码中的 elementData[–size] = null; 这句就是为了避免内存泄漏的代码,置空以便垃圾回收器回收。
参考资料:
疯狂Java:突破程序员基本功的16课-Java 的内存回收
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 的内存回收
相关文章推荐
- [jvm解析系列][四]Java的垃圾回收(二)垃圾收集算法,内存分配和回收策略
- 安卓内存泄漏学习(-)--内存泄漏介绍与回收算法简介
- JVM入门,java环境基础+理解内存+垃圾判定、回收算法
- Java的内存回收——Java的内存泄漏
- Java内存回收算法
- Java虚拟机垃圾回收(一) 基础:回收哪些内存/对象 引用计数算法 可达性分析算法 finalize()方法 HotSpot实现分析
- Java虚拟机垃圾回收(一) 基础:回收哪些内存/对象 引用计数算法 可达性分析算法 finalize()方法 HotSpot实现分析
- java 内存回收和回收机制的算法
- Java内存回收机制全解(算法+内存分配)
- 牛客网Java刷题知识点之垃圾回收算法过程、哪些内存需要回收、被标记需要清除对象的自我救赎、对象将根据存活的时间被分为:年轻代、年老代(Old Generation)、永久代、垃圾回收器的分类
- Java内存回收(2)——垃圾回收算法
- java内存回收----引用
- Java的垃圾收集算法、垃圾收集器以及内存分配与回收策略
- java内存回收算法(分代收集)
- Java的垃圾回收机制,GC,和变量的创建和删除的关系(即变量的作用域,for循环中的某个变量,出了for循环再引用就会报错了,因为该变量已经被从内存中删掉了)
- Java之JVM垃圾回收 内存结构以及垃圾回收算法
- java 内存回收机制和算法(只有跳转链接)
- Java内存回收算法
- javaVM 判断对象实例何时回收 用的可达性分析算法,而非引用计数算法
- 面向对象_引用类型_内存分析_垃圾回收JAVA028-033