Java一个重要的优势就是通过垃圾管理器GC (Garbage Collection)自动管理和回收内存,程序员无需通过调用方法来释放内存。也因此很好多的程序员可能会认为Java程序不会出现内存泄漏的问题,这种想法是不对的,当我们对内存使用不当的时候仍然可能会出现内存泄漏,并且问题相对与c++来说更隐秘,问题的根源排查起来也比较困难。不过,当我们了解了Java虚拟机内存区域,Java垃圾收集器之后,对于解决内存泄漏的问题也就不是什么困难的事情了。
相关阅读
在前一篇博客中已经已经详细分析了java内存运行时各个区域,其中程序计数器、虚拟机栈、本地方法栈随着线程而生,随着线程而灭亡,操作数栈中的栈帧随着方法的执行的有条不紊地进行着入栈和出栈的操作,每个栈帧中分配多少内存基本上在类结构确定下来时就已经确定了大小了,因此这几个区域的内存分配和回收都具备确定性,在这个几个区域中就不需要过多的考虑内存分配的问题了,因为随着线程的结束,内存在然而然地就已经被回收了。而java堆和方法区不一样,一个接口中多个实现类所需要的内存可能不一样,一个方法中多个分支需要的内存的大小也有可能是不同的,我们在程序的运行期才能确定哪些对象需要创建,创建所需内存是多大,这些内存都是动态分配的,垃圾回收器考虑的就是这部分的内存,接下来我们将围绕以下3个问题来展开描述java垃圾收集器是如何自动回收内存的:
1、哪些内存需要回收?
2、什么时候回收?
3、如何回收?
本片博客就围绕这第一个问题展开说明,其余连个问题将在后面的博客中一一详解;
首先,Java垃圾回收器的一个想要解决的问题是什么样的内存需要被回收呢?Java垃圾收集器认为,当一个对象再无被其它对象引用时可以认为这个对象可以回收了。那么垃圾收集器是怎么知道对象的死亡的还是存活的呢?目前,Java虚拟机有两种算法来确定哪些对象是无用的,需要被回收的。
1.引用计数法
该算法的思路是给每个对象都添加一个引用计算器,每当有其它对象引用时计数器就+1,当引用失效时计数器-1,任何时刻当该对象的引用数为0的时候,则判定这个对象不会再被使用了,可以将该对象回收了。这种算法实现起来很简单,效率也非常高,但是并没有被Java所采用,原因是这种算法很难解决对象相互引用的问题。看一下下面例子的代码:
public class Test { private Object instance = null;
private static final int _1MB = 1024 * 1024; /** 这个成员属性唯一的作用就是占用一点内存 */
private byte[] bigSize = new byte[2 * _1MB]; public static void main(String[] args)
{
Test A= new Test();
Test B = new Test();
A.instance = B;
B.instance = A;
A = null;
B = null; System.gc();
}
}
运行结果:
[GC (System.gc()) 9299K->720K(249344K), 0.0010947 secs]
[Full GC (System.gc()) 720K->627K(249344K), 0.0075042 secs]
上面的例子中A和B相互引用,它们的引用计数并不为0,但是执行System.gc()方法后仍然被回收了,这说明了Java垃圾收集器并不是使用引用计数法的。
2.可达性分析算法
可达性分析算法就是Java垃圾收集器判断‘垃圾’对象的算法。基本思路是通过一系列的”GC Roots“ 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路程叫做引用链,当一个对象没有任何引用链与”GC Roots“有链接时,那么可以判定这个对象是无用的对象。可以作为GC Roots对象的包括一下几种:
1)Java虚拟机栈中局部变量表引用的对象;
2)本地方法栈中JNI所引用的对象;
3)方法区中的静态变量;
4)方法区中常量引用的对象;
下图为GC Roots:
图中obj1 ~ obj7都能够直接或间接地与GC Roots有连接,因此他们是存活对象,不会被GC回收。obj8基本上可以被回收了,obj9和obj10虽然有相互引用,但是他们的引用链中并没有达到GC Roots,因此也会判定为非存活对象。
3.引用的四种状态
在jdk1.2之前,Java的引用类型是比较简单的,只有被引用和未引用两种状态,类型过于简单的话不利于垃圾收集器的回收,当已经将未被引用的对象都被回收之后内存仍然比较紧张时,垃圾收集器将无法确认被引用的对象是否需要回收,哪些对象可以被回收等。jdk1.2后Java对引用类型进行的拓展,包括:强引用、软引用、弱引用和虚引用四种情况,四者的引用强度依次递减。这样在虚拟机中内存使用不同的情况下,分别回收不容引用类型的对象。
1)强引用
在程序中我们直接new出来的对象的引用都属于强引用,比如:Object obj = new Object();只要强引用还在,就不会被GC回收。
2)软引用
描述部分部分有用但非必须的对象。在系统将要发生内存溢出的时候,虚拟机会尝试从这部分的引用类型中进行二次回收,如果二次回收后的内存仍不够用才会抛出内存溢出异常。Java中的类SoftReference表示软引用。
3)弱引用
描述被必须对象。被弱引用关联的对象只能生存到下一次回收之前,在垃圾收集器工作之后,无论内存是否够用,这类的对象都会被回收掉。Java中的类WeakReference表示弱引用。
4)虚引用
被虚引用关联的对象在被回收时会收到系统的通知。被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。
对于可达性分析算法,对于那些没有与GC Roots关联的对象并非是立即回收的,而是经历过两次标记后仍然没有与GC Roots关联上,此时才会回收该对象。对象在进行可达性分析后如果没有与GC Roots关联,则会进行第一次标记和第一次筛选,筛选条件为是否有必要执行finalize方法,比如如果finalize方法是否被覆盖,或是否已被执行一次。如果没有被覆盖或已被执行了,基本可以确认该对象会被回收了。否则,将这个对象放到F-Queue队列中。对F-Queue队列二次标记,如果在这次标记中对象成功关联上GC Roots,则该对象拯救了自己,将从”即将回收“的集合中移除,否则将会被回收。然而,对象只能拯救自己一次,第二次就会被回收了。
迎大家关注公众号: 【java解忧杂货铺】,里面会不定时发布一些技术干货博客;关注即可免费领取大量最新,最流行的技术教学视频: