`
lewis122
  • 浏览: 231741 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

JVM学习笔记系列(三)

    博客分类:
  • JVM
 
阅读更多

在Sun Hotspot的JVM内存管理白皮书中描述GC的概念是指垃圾收集器,而不是垃圾回收。例如在做GC调优的时候,选择的不是一种垃圾回收方式,你选择的是一种垃圾的收集器。垃圾收集器的不同在于,它不仅仅是做垃圾回收,他还决定了内存是如何分配的。也就是在选择GC的时候,因为他决定的内存分配,所以也就决定了应用的处理速度。

JVM - 内存分配

JVM在分配堆内存的时候,大多数情况下都是在JVM的堆上分配内存,具体在堆上是如何分配的这个就取决于具体的实现。各种GC在内存分配的策略上会稍有不同。也有可能直接在栈上分配内存(这个是Sun JDK7里面的优化方向),希望不必要在堆上分配的小对象,转为直接在栈上进行分配,好处就是只要栈帧退出了,内存就会被回收了。如果在堆中分配内存,栈帧退出是没用的。因为栈帧退出只能是引用没有了。因此Sun JDK7 的优化方向是将少数不是特别大的对象,直接放在栈上分配内存。据说会提高不少的性能,Oracle JRockit就着这样实现的,Sun JDK6只能做到将局部变量(非对象)在栈上面分配。

还有另外一种情况就是直接在JVM堆外进行内存分配,即在物理内存或虚拟内存上进行分配的,通常是不推荐这样做的。还有就是JNI模块,会在堆外进行分配,但是也会进行自动内存管理。堆外内存遇到OOM后是比较麻烦的。例如Oracle的连接方式,Oracle连接是使用oci进行连接,这样就会直接消耗堆外内存。JVM自身在做GC的时候,是需要消耗堆外的内存的,因此再给JVM堆分配内存的时候,分配堆的内存越大,就意味着可用的物理内存就越小,但是做GC的时候有需要消耗堆外内存,因此千万不要给堆分配过大的内存。例如4G内存的系统给JVM分配了3G,此时会变得非常危险。因为物理内存只剩下1G,这样在做GC的时候物理内存不够用了,应用就会就会直接挂了。现在还没有测试出来GC到底需要多少堆外内存。

JVM - 内存回收

JVM除了做内存以外还需要做内存的回收,GC要做的就是将已经“死掉”的对象占用的内存回收掉。Sun Hotspot认为没有引用的对象就表示对象已死。就意味着只要有引用对象就是活的。通常来讲内存泄露就是这样造成的,内存泄露就是这部分(引用)内存本来就是应该释放掉的,但是在代码里面一直拿着这个引用,导致这部分内存就释放不了,这种情况就会造成内存泄露。

Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟随着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

堆中几乎存放着Java中所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象有哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。

引用计数算法
以前在判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
客观地说,引用计数算法(Reference Counting)的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软的COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言以及在游戏脚本领域中被广泛应用的Squirrel中都使用了引用计数算法进行内存管理。但是,Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。
举个简单的例子,请看下面的代码中的testGC()方法:对象objA和objB都有字段instance,赋值令objA.instance = objB及objB.instance = objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

/**
 * testGC()方法执行后,objA和objB会不会被GC呢?
 * @author jianshi.dlw
 */
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 testGC() {  
		ReferenceCountingGC objA = new ReferenceCountingGC();  
		ReferenceCountingGC objB = new ReferenceCountingGC();  
		objA.instance = objB;  
		objB.instance = objA;  
 
		objA = null;  
		objB = null;  
 
		// 假设在这行发生GC,那么objA和objB是否能被回收?  
		System.gc();  
	}
}

运行结果:

    [Full GC (System) [Tenured: 0K->210K(10240K), 0.0149142 
    secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 
    0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]  
    Heap  
    def new generation   total 9216K, used 82K 
    [0x00000000055e0000, 0x0000000005fe0000, 0x0000000005fe0000)  
     Eden space 8192K,   1% used [0x00000000055e0000, 
    0x00000000055f4850, 0x0000000005de0000)  
     from space 1024K,   0% used [0x0000000005de0000, 
    0x0000000005de0000, 0x0000000005ee0000)  
     to   space 1024K,   0% used [0x0000000005ee0000, 
    0x0000000005ee0000, 0x0000000005fe0000)  
    tenured generation   total 10240K, used 210K 
    [0x0000000005fe0000, 0x00000000069e0000, 0x00000000069e0000)  
      the space 10240K,   2% used [0x0000000005fe0000, 
    0x0000000006014a18, 0x0000000006014c00, 0x00000000069e0000)  
    compacting perm gen  total 21248K, used 3016K 
    [0x00000000069e0000, 0x0000000007ea0000, 0x000000000bde0000)  
      the space 21248K,  14% used [0x00000000069e0000, 
    0x0000000006cd2398, 0x0000000006cd2400, 0x0000000007ea0000)  
    No shared spaces configured. 

从运行结果中可以清楚地看到GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

根搜索算法
在主流的商用程序语言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定对象是否存活的。这个算法的基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

在Java语言里,可作为GC Roots的对象包括下面几种:

 

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)的引用的对象。

 

引用

无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

 

  • 强引用 :强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。如果对象真的没有引用了,就会被GC回收。

Sun Hotspot GC会对后面的三种引用类型做不同的处理。

 

  • 软引用 :软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。也就是说只有在内存不够使用的情况下一定会被GC掉,长期不用的时候也会被GC。通常比较适合用来做缓存。比如说一个缓存我们也不清楚到底分配多大内存算是适合,那么可以做一个软引用,这样的话等到内存不够的时候就会被GC掉,也不会造成OOM。如果使用强引用做缓存的话那么就必须要控制大小。
  • 弱引用 :弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。弱引用在GC的时候一定会被GC掉的。
  • 虚引用 也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。如果想知道这个对象什么时候从JVM堆中释放掉,可以使用虚幻引用得到这个通知。

在根搜索算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那它就真的离死不远了。从下面的代码中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。

/**
 * 此代码演示了两点:
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * @author jianshi.dlw
 */
public class FinalizeEscapeGC {

	public static FinalizeEscapeGC SAVE_HOOK = null;

	public void isAlive() {
		System.out.println("yes, i am still alive :)");
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize mehtod executed!");
		FinalizeEscapeGC.SAVE_HOOK = this;
	}

	public static void main(String[] args) throws Throwable {
		SAVE_HOOK = new FinalizeEscapeGC();

		// 对象第一次成功拯救自己
		SAVE_HOOK = null;
		System.gc();
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("no, i am dead :(");
		}

		// 下面这段代码与上面的完全相同,但是这次自救却失败了
		SAVE_HOOK = null;
		System.gc();
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("no, i am dead :(");
		}
	}
}

运行结果:

    finalize mehtod executed!  
    yes, i am still alive :)  
    no, i am dead :( 

从上面的运行结果可以看到,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。

另外一个值得注意的地方就是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

 

回收方法区
很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看类的加载和卸载信息。
在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics