- 浏览: 10340 次
- 性别:
- 来自: 深圳
最近访客 更多访客>>
文章分类
最新评论
-
chenzhou123520:
之前看《深入理解JVM》时了解过这些,但是一直没有太多实际的实 ...
(2)垃圾收集器与内存分配策略 -
makemyownlife:
分析得很详实
(1):JAVA内存区域与内存溢出异常
当要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
在上一节谈到的几个JAVA内存区域中,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭。每个栈桢分配多少内存,在类结构确定下来后就已知的,因此这几个区域的内存分配和回收都具备确定性,所以这几个区别不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟着回收了。
而JAVA堆和方法区而和上述几个区域不同,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。只有在程序处于运行期间时才能知道会创建多少个对象,这部分内存的分配和回收都是动态的,本节讨论的内存分配和回收是指这一部分内存。
如何判断对象已死?
主流的程序语言都是使用根搜索算法(GC Roots Tracing)判定对象是否存活
基本思路是:通过一系列名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索超过的路径称为引用链(Reference chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
下图A的各个对象有引用链有GC Roots相连,说明对象都存活,而图B的三个对象虽然都各自相连,但却没有与任何一个GC Roots相连,则可判定它们为可回收的对象。
在JAVA中,可作为GC Roots的对象包括下面几种:
1,虚拟机栈(栈桢中的本地变量表)中引用的对象。
2,方法区中的类静态属性引用的对象。
3,方法区中的常量引用的对象。
4,本地方法栈中JNI(即一般说的Native方法)的引用的对象。
JDK1.2后,JAVA对引用的概念进行了扩充,针引用分为以下四种:强度依次逐渐减弱
强引用:类似"Object obj = new Object()”这类的引用就是强引用,只要引用关系还存在,就不会回收被引用的对象。
软引用:用来描述一些还有用,但并非必需的对象,在即将发生内存溢出之前,会将这些对象列入回收范围,以进行第二
次回收。在JDK1.2后,提供了SoftReference类来实现软引用。
弱引用:用来描述非必要对象,比软引用更弱一些,这些对象只能生存到下一次垃圾收集之前。1.2后,使用
WeakReference来实现弱引用。
虚引用:是最弱的一种引用关系,一个对象是否有虚引用存在,完全不会对其生存时间构成影响,它的唯一目的就是希望
能在这个对象被收集器回收时收到一个系统通知。1.2后,提供了PhantomReference来实现虚引用。
关于finalize方法:
如果一个对象在GC Roots上没有与任何一个对象连接,但它有覆盖finalize方法,如果它在finalize方法中有重新连接上GC Roots对象,则不会被回收。
package com.chapter1; /** *此代码演示了两点: *1,对象可以在GC时通过finalize方法自我拯救 *2,这种自救机会只有一次,因为一个对象的finalize方法最多只会被系统自动调用一次 */ public class FinalizeEscapeGC { public static FinalizeEscapeGC save_hook = null; protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed"); FinalizeEscapeGC.save_hook = this; } public static void main(String[] args) throws Throwable{ save_hook = new FinalizeEscapeGC(); //对象第一次拯救自己 save_hook = null; System.gc(); //因为finalize方法优化级很低,暂停0.5秒,以等待它 Thread.sleep(500); if(save_hook != null){ System.out.println("yes , i still alive"); }else{ System.out.println("no , i am dead"); } //下面这段代码与上面完全相同,但这次拯救却失败了 save_hook = null; System.gc(); Thread.sleep(500); if(save_hook != null){ System.out.println("yes , i still alive"); }else{ System.out.println("no , i am dead"); } } }
运行这段代码的输出结果为:
finalize method executed yes , i still alive no , i am dead
finalize运行代码高昂,不确定性大,无法保证各个对象的调用顺序,它能做的所有工作,使用try_finally或其它方式都可以做的更好、更及时。不建议使用此方法。
回收方法区(也称为永久代):
在堆中,尤其是在新生代中,常规应用进行一次垃圾回收一般可以回收70~95%的空间,则方法区的垃圾回收效率远低于此。
方法区的垃圾回收主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收JAVA堆中的对象非常类似,当一个常量池中的常量没有被任何对象引用,则可以被回收。
判断一个类是否是无用的类,则条件要苛刻的多。需要同时满足下面3个条件才能算是“无用的类”:
1,该类的所有实例都已被回收,也就是JAVA堆中不存在该类的任何实例。
2,加载该类的ClassLoader已被回收。
3,该类的java.lang.Class对象没有在任何地方被引用。
满足上述三个条件的无用类就可以被回收。在大量使用反射、动态代理、CGLib等框架的场景,都需要虚拟机具备类卸载的功能,以保证方法区不会溢出。
垃圾回收算法:
1,标记-清除算法:
这是最基础的收集算法,分为“标记”和“清除”两个阶段,首先标记出要回收的对象,标记完成后,统一回收掉所有被标记的对象。它主要有两个缺点:一是效率问题,标记和清除的效率不高,二是空间问题,标记清除后产生大量不连续的内存碎片,如果碎片过多,可能会导致,当程序在以后运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2,复制算法:
将内存分为大小相等的两块,每次只使用一块,当一块用完,就将存活的对象复制到另一块去,然后将已使用过的对象一次清理掉,这样每次都是对一块内存进行回收。优点是实现简单,运行高效。缺点是可用内存缩小为原来的一半。
目前的商用虚拟机几乎都使用这种算法,研究表明,在新生代内存,一次垃圾回收能回收98%对象。因此不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden(英文意思:伊甸园)和两块较小的Survivor空间,每次分配内存只使用Eden和其中的一块Survivor。当回收时,将存活的对象都复制到另一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。即每次新生代可用内存空间为整个新生代内存容量的90%(80%+10%)。当作为复制的Survivor空间不够用时,需要依赖其它内存空间(如老年代)进行分配担保。
3,标记-整理算法:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动。然后清除掉边界以外的内存。
4,分代收集算法:
当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,在新生代,具体采用复制算法,在老年代,由于对象存活率高、没有额外空间担保,就必须使用“标记-清理”或“标记-整理”算法进行回收。
垃圾收集器
上面述说的算法是内存回收方法论,垃圾收集器就是内存回收的具体实现,JAVA虚拟机规范中对垃圾收集器应该如何实现没有任何规定,各厂家、不同版本的垃圾收集器都可能会有很大的差别。这里讨论的是基于sun的hotspot1.6update22版本的虚拟机。这个虚拟机包含的所有收集器如下:
图中的问号表示1.7版本即将推出的G1收集器。如果两个垃圾收集器之间存在连线。就说明它们可以搭配使用。
没有一种垃圾收集器是放之四海皆准,任何场景下都适用的。
Serial收集器
它是1.3版本之前新生代的唯一收集器,是一个单线程收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程(sun将此称之为Stop the world),直到它收集结束 。
此种收集器在client模式下的默认新生代收集器,简单高效,收集几十M甚至一两百M的新生代,停顿时间完成可以控制在几十毫秒左右。
ParNew收集器
ParNew是Serial收集器的多线程版本,除了使用多线程进行垃圾回收外,其余行为与Serial基本一样。
它是Server模式下虚拟机首选的新生代收集器
Parallel Scavenge收集器
这也是一个新生代,使用复制算法的并行多线程垃圾收集器,这个收集器的特点是:其目标是使用CPU运行时间达到一个可控的吞吐量。所谓吞吐量是指CPU运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),例如:CPU总运行时间为100分钟,其中处理垃圾回收占了1分钟,则吞吐量就是99%。
此收集器提供了两个参数用于精确控制吞吐量,分别是最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数及直接吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis参数允许的值是大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值,此时间变小,会使垃圾回收变得更频繁。可能使吞吐量下降。
-XX:GCTimeRatio参数的值是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认为99,就是最大允许1%的垃圾回收时间。
上面介绍的三个都是新生代垃圾回收器,下面再介绍三个老年代垃圾回收器
Serial Old收集器
这个收集器的示例图与Serial一样,它是Serial收集器的老年代版本,使用“标记-整理”算法,这个收集器的主要目的是在Client模式下的虚拟机使用。在Server模式下,它主要是作为CMS模式的后备预案,或与Parallel Scavenge收集器搭配使用。
Parallel Old收集器
Paralle Old是Paralle Scavenge收集器的老年代版本,使用“标记-整理“算法,这个收集器是1.6版本后提供,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS(Concurrent Mark Sweep)收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,使用的是“标记-清除”算法。互联网服务器尤其重视服务的响应速度,CMS收集器非常符合这类应用的需求。
它的运作过程分为四步:
1,初始标记
2,并发标记
3,重新标记
4,并发清除
其中初始标记和重新标记这两个步骤仍然需要“stop the world",初始标记只是标记一下GC ROOTS能直接关联到的对象,速度很快。并发标记就是进行 GC ROOTS TRACING的过程,而重新标记阶段是为了修正并发标记期间,因用户程序继续运作导致标记变动的那一部分对象的标记记录。这一阶段的停顿时间会稍比初始标记长一些,但远比并发标记时间短。
整个过程耗时最长的并发标记和并发清除过程中,收集线程都可以和用户线程一起工作。
CMS收集器是一款优秀的收集器,主要优点是并发收集、低停顿。但它有下面三个主要的缺点:
1:对CPU资源非常敏感,会占用一部线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
2,无法处理浮动垃圾,在并发清理阶段程序还在运行,可能产生新的垃圾(浮动垃圾),当这些浮动垃圾过大 ,CMS
运行期的预留内存无法容下这些浮动垃圾时,将出来“Concurrent Mode Failure“失败,这些虚拟机将启用Serial
Old来进行老年代垃圾回收。
3,最后一个缺点是,由于CMS采用“标记-清除“算法,垃圾收集结束后,会产生大量空间碎片。空间碎片过多,将会给大
对象分配带来很大麻烦。很可能由于无法找到足够大的连续空间来分配此大对象,导致触发一次FULL GC。虚拟机提供
了一个参数-XX:+UseCMSCompactAtFullCollection开关参数,表示在FULL GC后,会进行一次碎片整理。
G1收集器
G1收集器在1.7版本正式发布,关于G1垃圾收集器的新特性可参考这里
内存分配与回收策略
JAVA的内存自动管理可以归结为:对象的内存分配以及回收分配给对象的内存。上面用大量篇幅描述了内存回收,下面再谈谈内存分配。
从大方向说,就是在堆上分配,对象主要分配在新生代的Eden区上,当然也不是固定的,其细节取决于使用哪种垃圾收集器组织及其参数设置。
下面介绍几条最普遍的内存分配规则:
1,对象优先在Eden分配
在大多数情况下,对象在新生代Eden区中分配,当Eden区中没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
先看下面这段程序:
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails */ public class TestMinorGC { private static final int _1MB = 1024 * 1024; public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { TestMinorGC test = new TestMinorGC(); test.testAllocation(); } }
打印结果为:
[GC [DefNew: 6472K->140K(9216K), 0.0087933 secs] 6472K->6284K(19456K), 0.0088364 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] Heap def new generation total 9216K, used 4400K [0x03bb0000, 0x045b0000, 0x045b0000) eden space 8192K, 52% used [0x03bb0000, 0x03fd8fd8, 0x043b0000) from space 1024K, 13% used [0x044b0000, 0x044d3050, 0x045b0000) to space 1024K, 0% used [0x043b0000, 0x043b0000, 0x044b0000) tenured generation total 10240K, used 6144K [0x045b0000, 0x04fb0000, 0x04fb0000) the space 10240K, 60% used [0x045b0000, 0x04bb0030, 0x04bb0200, 0x04fb0000) compacting perm gen total 12288K, used 2105K [0x04fb0000, 0x05bb0000, 0x08fb0000) the space 12288K, 17% used [0x04fb0000, 0x051be728, 0x051be800, 0x05bb0000) No shared spaces configured.
分析:在运行时通过-Xms20M -Xmx20M和-Xmn10M这3个参数限制JAVA堆大小为20M,且不可扩展,其中10M分配给新生代,剩下10M给老年代,-XX:SurvivorRatio=8 这个参数决定了Eden与Survivor区的空间比例为8:1。在上面打印结果的第4,5行中可以清晰地看到“eden space 8192K,from space 1024K”的信息,新生代总内存为9216K。
在新生代分配完allocation1 ,allocation2,allocation3三个数组对象后,再分配allocation4时,由于此时前3个数据占用了新生代6M空间,再分配allocation4的4M时,新生代的内存空间已不够。此时虚拟机会对新生代进行一次Minor GC,GC期间虚拟机发现这三个数组无法放入Survivor空间,所以只好通过分配担保机制提前转移到老年代去。
这次Minor GC结束后,allocation4被顺利分配到Eden中。因此程序执行完后,Eden占用了4M,上面GC 日志的第3行对此有描述,老年代被占用了6M。上面日志的倒数第5行,有描述。
PS:Minor GC与Full GC有什么不同?
新生代GC(Minor GC):指发生在新生代的垃圾收集运作,因为JAVA对象大多是朝生夕灭,所以Minor GC非常频繁。
且速度较快。
老年代GC(Major/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随一次Miinor GC,Major GC一般比Minor GC慢十倍。
使用-XX:PretenureSizeThreShold参数将大对象直接放入老年区
所谓大对象是指,需要大量连续内存空间的JAVA对象。经常出现大对象容易导致内存还有不少空间就提前触发垃圾回收收集以获取足够的连续空间来安置它们。
虚拟机提供了一个-XX:PretenureSizeThreShold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝(因为新生代采用复制算法收集内存)
看以下代码:
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:PretenureSizeThreshold=3145728 -XX:+PrintGCDetails */ public class TestPretenureSizeThreShold { private static final int _1MB = 1024*1024; public static void testPretenureSizeThreShold(){ byte[] allocation1 = new byte[4 * _1MB]; } public static void main(String[] args) { testPretenureSizeThreShold(); } }
运行结果:
Heap def new generation total 9216K, used 492K [0x03b00000, 0x04500000, 0x04500000) eden space 8192K, 6% used [0x03b00000, 0x03b7b2a8, 0x04300000) from space 1024K, 0% used [0x04300000, 0x04300000, 0x04400000) to space 1024K, 0% used [0x04400000, 0x04400000, 0x04500000) tenured generation total 10240K, used 4096K [0x04500000, 0x04f00000, 0x04f00000) the space 10240K, 40% used [0x04500000, 0x04900010, 0x04900200, 0x04f00000) compacting perm gen total 12288K, used 2099K [0x04f00000, 0x05b00000, 0x08f00000) the space 12288K, 17% used [0x04f00000, 0x0510cd70, 0x0510ce00, 0x05b00000) No shared spaces configured.
可以看到在testPretenureSizeThreshold()方法后,我们看到Eden几乎没有被使用,而老年代的空间被使用了40%,这是因为PretenureSizeThreshold被设置为3M(就是3145728B,这个参数不能直接写MB),因此超过3M的对象都会直接在老年代中进行分配。
需注意的是:此参数只对serial和Parnew两款收集器有效。
长期存活的对象将进行老年代
虚拟机使用了分代收集的思想来管理内存,那么在回收内存就就必须识别哪些对象应该放入老年代,哪些应该放在新生代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次GC 后仍然存活,并能被Survivor容下的话,将被移动到Survivor空间中,并将对象年龄设为1,此后对象第在Survivor区中熬过一次GC,年龄就会加1,当它的年龄加到一定程序后(默认15岁)。就会晋升到老年代。这个值可以通过-XX:MaxTenuringThreshold来设置。
看下面两个例子:
例子1:
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:MaxTenuringThreshold=1 -XX:+PrintGCDetails -XX:+PrintTenuringDistribution */ public class TestTenuringThreShold { private static final int _1MB = 1024 * 1024; public static void testTenuringThreShold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB/4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } public static void main(String[] args) { testTenuringThreShold(); } }
打印结果:
[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 405624 bytes, 405624 total : 4680K->396K(9216K), 0.0343545 secs] 4680K->4492K(19456K), 0.0343994 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4492K->0K(9216K), 0.0010247 secs] 8588K->4492K(19456K), 0.0010613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4259K [0x03b60000, 0x04560000, 0x04560000) eden space 8192K, 52% used [0x03b60000, 0x03f88fd8, 0x04360000) from space 1024K, 0% used [0x04360000, 0x04360000, 0x04460000) to space 1024K, 0% used [0x04460000, 0x04460000, 0x04560000) tenured generation total 10240K, used 4492K [0x04560000, 0x04f60000, 0x04f60000) the space 10240K, 43% used [0x04560000, 0x049c3088, 0x049c3200, 0x04f60000) compacting perm gen total 12288K, used 2105K [0x04f60000, 0x05b60000, 0x08f60000) the space 12288K, 17% used [0x04f60000, 0x0516e6f0, 0x0516e800, 0x05b60000) No shared spaces configured.
例子2 :
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails -XX:+PrintTenuringDistribution */ public class TestTenuringThreShold { private static final int _1MB = 1024 * 1024; public static void testTenuringThreShold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB/4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } public static void main(String[] args) { testTenuringThreShold(); } }
打印结果:
[GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 405624 bytes, 405624 total : 4680K->396K(9216K), 0.0058936 secs] 4680K->4492K(19456K), 0.0059357 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 2: 405480 bytes, 405480 total : 4492K->395K(9216K), 0.0010474 secs] 8588K->4491K(19456K), 0.0010814 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] Heap def new generation total 9216K, used 4655K [0x03b10000, 0x04510000, 0x04510000) eden space 8192K, 52% used [0x03b10000, 0x03f38fd8, 0x04310000) from space 1024K, 38% used [0x04310000, 0x04372fe8, 0x04410000) to space 1024K, 0% used [0x04410000, 0x04410000, 0x04510000) tenured generation total 10240K, used 4096K [0x04510000, 0x04f10000, 0x04f10000) the space 10240K, 40% used [0x04510000, 0x04910010, 0x04910200, 0x04f10000) compacting perm gen total 12288K, used 2105K [0x04f10000, 0x05b10000, 0x08f10000) the space 12288K, 17% used [0x04f10000, 0x0511e6f0, 0x0511e800, 0x05b10000) No shared spaces configured.
两个例子的-XX:MaxTenuringThreshold参数的值设置不同,例1中为1次,例2中为15次。allocation1 为256K,survivor空间可容下,当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代survivor已使用空间变为0K。而在MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代的Survivor空间,占用了38%。
动态对象年龄判定
为了更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄到达MaxTenuringThreshold设定的值才晋升老年代,如果survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
看下例代码:
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails -XX:+PrintTenuringDistribution */ public class TestTenuringThreShold2 { private static final int _1MB = 1024 * 1024; public static void testTenuringThreShold2() { byte[] allocation1, allocation2, allocation3,allocation4; allocation1 = new byte[_1MB/4]; allocation2 = new byte[_1MB/4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { testTenuringThreShold2(); } }
打印结果:
[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 667800 bytes, 667800 total : 4936K->652K(9216K), 0.0059675 secs] 4936K->4748K(19456K), 0.0060051 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 4748K->0K(9216K), 0.0013297 secs] 8844K->4748K(19456K), 0.0013699 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4259K [0x03b80000, 0x04580000, 0x04580000) eden space 8192K, 52% used [0x03b80000, 0x03fa8fd8, 0x04380000) from space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000) to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000) tenured generation total 10240K, used 4748K [0x04580000, 0x04f80000, 0x04f80000) the space 10240K, 46% used [0x04580000, 0x04a23018, 0x04a23200, 0x04f80000) compacting perm gen total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000) the space 12288K, 17% used [0x04f80000, 0x0518e718, 0x0518e800, 0x05b80000) No shared spaces configured.
运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1,allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来已达到512KB,并且它们是同年的,满足同年对象达到survivor空间的一半规则。
空间分配担保
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC,如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行Minor GC,如果不允许,则也要改为进行一次Full GC。
下面看两个分别打开与关于HandlePromotionFailure参数的例子
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -XX:-HandlePromotionFailure */ public class TestHandlePromotion { private static final int _1MB = 1024 * 1024; public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; } public static void main(String[] args) { TestHandlePromotion test = new TestHandlePromotion(); test.testHandlePromotion(); } }
打印结果:
[GC [DefNew: 6472K->140K(9216K), 0.0140637 secs] 6472K->4236K(19456K), 0.0141047 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] [GC [DefNew: 6370K->6370K(9216K), 0.0000223 secs][Tenured: 4096K->4236K(10240K), 0.0239241 secs] 10466K->4236K(19456K), [Perm : 2079K->2079K(12288K)], 0.0240152 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] Heap def new generation total 9216K, used 2211K [0x03ac0000, 0x044c0000, 0x044c0000) eden space 8192K, 27% used [0x03ac0000, 0x03ce8fd8, 0x042c0000) from space 1024K, 0% used [0x043c0000, 0x043c0000, 0x044c0000) to space 1024K, 0% used [0x042c0000, 0x042c0000, 0x043c0000) tenured generation total 10240K, used 4236K [0x044c0000, 0x04ec0000, 0x04ec0000) the space 10240K, 41% used [0x044c0000, 0x048e3090, 0x048e3200, 0x04ec0000) compacting perm gen total 12288K, used 2105K [0x04ec0000, 0x05ac0000, 0x08ec0000) the space 12288K, 17% used [0x04ec0000, 0x050ce7f8, 0x050ce800, 0x05ac0000) No shared spaces configured.
上面这个是关闭HandlePromotionFailure参数的程序,程序运行到allocation4 = new byte[2 * _1MB]时,新生代的Eden已没有足够的空间放入allocation4,由于不允许分配担保,此时虚拟机发动一次FULL GC,新生代变为140K,整个内存占用的空间为4236K,即allocation1,allocation2占用的空间,allocation3被回收,这在GC日志的第一行可以看出。
程序运行完后,新生代被allocation7占用了27%,老年代被allocation2,allocation3占用了41%
package com.chapter1; /** * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -XX:+HandlePromotionFailure */ public class TestHandlePromotion { private static final int _1MB = 1024 * 1024; public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; } public static void main(String[] args) { TestHandlePromotion test = new TestHandlePromotion(); test.testHandlePromotion(); } }
打印结果:
[GC [DefNew: 6472K->140K(9216K), 0.0066974 secs] 6472K->4236K(19456K), 0.0067406 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC [DefNew: 6370K->139K(9216K), 0.0006466 secs] 10466K->4236K(19456K), 0.0006861 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 2351K [0x03b00000, 0x04500000, 0x04500000) eden space 8192K, 27% used [0x03b00000, 0x03d28fd8, 0x04300000) from space 1024K, 13% used [0x04300000, 0x04322fe0, 0x04400000) to space 1024K, 0% used [0x04400000, 0x04400000, 0x04500000) tenured generation total 10240K, used 4096K [0x04500000, 0x04f00000, 0x04f00000) the space 10240K, 40% used [0x04500000, 0x04900020, 0x04900200, 0x04f00000) compacting perm gen total 12288K, used 2105K [0x04f00000, 0x05b00000, 0x08f00000) the space 12288K, 17% used [0x04f00000, 0x0510e7f8, 0x0510e800, 0x05b00000) No shared spaces configured.
上面这个是允许分配担保的例子:
与第一个例子的区别是在第二行GC日志,allocation4,5,6在新生代都被回收,Eden由6370K->139K,原来新生代+老年代共占用了10466K,这时也优化到了4236K。最后堆的占用情况与第一个例子类似。
相关推荐
主要整理内容为:分析了垃圾收集的算法和JDK1.7中提供的7款垃圾收集器的特点以及运作原理。以及内存分配策略
了解和掌握Java的垃圾收集器与内存分配策略对于开发高性能、稳定的应用至关重要,这涉及到程序的运行效率、内存消耗和避免内存泄漏等问题。通过理解这些概念,开发者可以更好地理解和解决Java应用程序中的内存问题,...
JVM内存管理是Java虚拟机的核心机制之一,其主要包含对象的创建、内存分配、...通过对内存分配策略、对象生死判定、垃圾收集算法和垃圾收集器的理解与应用,可以更好地掌握JVM的内存管理,从而提升应用性能和稳定性。
《垃圾回收器与内存分配策略详解》 在Java编程中,理解垃圾回收(Garbage Collection,简称GC)机制和内存分配策略是至关重要的。GC的主要目的是自动管理内存,避免程序员手动进行繁琐且容易出错的内存释放工作。而...
《垃圾回收器与内存分配策略详解》 在Java编程中,理解垃圾回收(Garbage Collection,简称GC)机制和内存分配策略是至关重要的。GC的主要目的是自动管理内存,避免程序员手动进行繁琐且容易出错的内存释放工作。而...
Java垃圾收集器与内存分配策略是Java性能优化的重要组成部分。垃圾收集器的主要任务是自动管理Java应用程序的内存,确保程序运行过程中有效地回收不再使用的对象,从而避免内存泄漏。本文将详细讲解Java垃圾收集器的...
本篇将深入探讨两种重要的垃圾回收器——G1收集器和ZGC,以及Stop the World现象和内存分配策略。 首先,G1(Garbage-First)收集器是一种并行并发的垃圾回收器,旨在减少垃圾回收停顿时间。其特点是采用了区域...
本文详细探讨了JVM中的垃圾收集器和垃圾收集算法,以帮助开发者深入理解Java虚拟机的内部运作机制。 垃圾收集(GC,Garbage Collection)是JVM的一个重要功能,用于自动释放不再使用的对象所占用的内存空间,以防止...
3. **低优先级与紧急响应**:垃圾收集器线程在系统资源紧张时,可能会被触发以释放内存,但这并不总是按照预期进行,其执行时机是不确定的。 4. **不可强制执行**:虽然程序员可以通过调用`System.gc()`来建议执行...
- 无用的对象不一定会在垃圾收集器的每次运行中被回收,也可能在整个程序运行期间都保留着,直到程序结束,除非它们的内存被重新分配或由其他方式释放。 6. **垃圾收集策略**: - Java提供了多种垃圾收集策略,如...
至于Java垃圾收集器,它是自动管理内存的关键,减少了手动内存管理可能导致的错误,如C++中的内存泄漏。Java的垃圾收集策略始于20世纪60年代,并在Smalltalk和Eiffel等语言中应用。所有垃圾收集器的目标都是找出不再...
在实际应用中,我们还需要关注其他内存管理策略,如对象存活判断算法(如可达性分析)、内存分配策略(如TLAB,Thread Local Allocation Buffer)以及内存压缩(如CMS的压缩整理阶段)。深入理解这些细节有助于更好...
垃圾收集器的工作策略包括多种,如标记-清除、复制算法、标记-整理和分代收集等。这些策略根据JVM的实现和应用的需求,可以进行组合和优化,以达到最佳的内存管理和性能。例如,分代收集策略将堆分为新生代和老年代...
Java的垃圾收集器(GC)是Java编程语言的一个核心特性,它自动化地管理程序的内存分配和回收,显著减轻了程序员手动管理内存的负担。在Java中,内存管理主要是通过垃圾收集器来实现的,它负责检测并回收不再使用的...
在编程领域,垃圾收集器(Garbage Collector, GC)是一个重要的概念,特别是在内存管理中。GC 自动跟踪并回收不再使用的内存,防止内存泄漏。本文将深入探讨如何使用 C 语言编写一个简单的垃圾收集器,这是一项对于...
垃圾回收器和内存分配策略 在Java开发中,理解JVM(Java Virtual Machine)的内存管理,特别是垃圾回收器(Garbage Collector, GC)和内存分配策略,对于优化应用性能至关重要。这篇文章将探讨这些核心概念。 内存...
9. **Java虚拟机参数调整**:开发者可以通过JVM参数来调整内存分配策略,例如-Xms和-Xmx设置堆内存的初始大小和最大大小,-XX:NewRatio设置新生代和老年代的比例等。 10. **JVM内存诊断工具**:JVisualVM、jmap、...
垃圾收集算法中,复制算法是一种简单高效的策略,它将内存分为两等份,每次只使用一半,存活对象在回收时被复制到另一半,然后清空已使用的一半。在Java的新生代内存管理中,采用类似的分代策略,但内存划分不是精确...
3. **垃圾收集器选择与参数调优** 不同的JVM版本和应用场景,选择合适的垃圾收集器和调整其参数至关重要。例如,对于需要低延迟的应用,可能会选择G1或ZGC;对于大内存应用,可能选择CMS或Parallel Old GC。 ...