引言
作为Java或Android程序员,大多时候我们不需要关系对象在内存中是如何分配,也不用担心对象占用的内存何时被系统回收。如果在开发过程中完全依赖系统帮助我们进行内存管理,那么后果将不堪设想。内存溢出,内存泄露,频繁GC等这些常见内存问题都是未对内存进行合理分配导致的,因此掌握JAVA内存分配策略是至关重要的。最近在读神作《深入理解Java虚拟机》,因此写下本文记录自己的读书心得。
走出误区
我记得大学读书的时候,老师是这样教我们的:JAVA语言中是使用引用计数算法来判断对象是否存活。每个对象都有一个引用计数器,当被引用一次,引用计数器的值增加1;当引用失效时,引用计数器的值减一。这种观念陪伴了我很久,直到某次面试的时候才知道这是错误的。然而JAVA语言并没有选择引用计数算法来判断对象是否存活,最主要的原因是它很难解决对象之间的相互循环引用的问题。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
//这个成员属性的意义就是占一点内存,以便能在GC日志中看清楚是否被回收过
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
// TODO Auto-generated method stub
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//如果这里发生GC,那么objA和objB是否能被回收?
System.gc();
}
}
如果虚拟机是采用引用计数算法的话,那么objA和objB对象是无法回收的,因为它们各自持有对方的引用。然而通过查看GC日志,可以发现objA和objB对象可以被虚拟机回收。这也说明虚拟机并不是采用引用计数算法来判断对象是否存活。
根搜索算法
JAVA是采用根搜索算法来判断对象是否存活的。该算法的基本思路就是通过一系列名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则此对象是不可用的。
如上图所示,虽然OBJ4和OBJ5有关联,但是他们到GC Root不可达,所以他们将会被判断为可回收对象。 在Java语言中,可作为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2.方法区中的类静态属性引用的对象。
3.方法区中的常量引用的对象。
4.本地方法栈中JNI(即一般说的Native方法)的引用对象。
如果对以上四种类型不太明白,在下篇文章中介绍Java内存具体是如何进行分配的。
引用的四种类型
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用的强度逐渐减弱。
1.强引用是最经常打交道的引用,通过new出来的引用都是强引用,只要强引用存在,GC宁可抛出outOfMemory,也不会回收掉被引用的对象。
2.软引用用于描述一些还有用,但是非必须的对象。对于软引用关联着的对象,在系统将要发生溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果此次回收还未有足够的内存,才会抛出内存溢出异常。
3.弱引用也是用来描述非必须的对象,但是它的强度比软引用更弱一些,被关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当时内存是否足够,都会回收掉只被弱引用关联的对象。
4.虚引用是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。设置虚引用的唯一目的是希望在这个对象被GC回收时收到一个系统通过。