前言
上回说道自动内存管理机制,那么没用的对象该怎么处理呢?内存又该怎么分配?为了应对这些场景,这回我们引入垃圾收集器和内存分配策略。
基本概念
-
垃圾收集器(Garbage Collection,GC)
这并不是一门java语言的伴生产物,实际上比java语言早半个多世纪问世。1960年MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。我们在上回就知道了程序计数器,虚拟机栈,本地方法栈这三个区域随着线程而生,随着线程而灭,不需要考虑回收问题,线程结束,内存就跟着回收了。但是java堆和方法区就不一样了,每个实例需要的内存可能不一样,一个方法的多个分支需要的内存也有可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,分配内存和回收都是动态的。因此,垃圾收集器关注的就是这部分内存。
-
如何确定对象是否该回收
引用计数算法
给对象添加一个引用计数器,引用一次则加一,引用失效则减一,当引用为0的对象则表示不能再被使用。
这是个不错的算法,如微软的COM技术,使用ActionScript3的FlashPlayer、python语言和游戏领域的Squirrel都采用这个算法进行内存管理,然而主流的java虚拟机并没有采用引用计数算法管理内存,主要是很难处理对象之间相互循环引用的问题。-
可达性算法
以一个GC Roots的对象为起点,从这些节点向下探索,探索走过的路径为引用链,当一个对象到GC Roots没有任何引用链时,证明这个对象是不可用的。
不可达对象也并非一定会被回收,要真正宣布对象死亡,至少需要经历两次标记过程:如果对象进行可达性分析发现没有与GC Roots相连接,那么,它会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。对象如果没有覆盖该方法,或者改方法已经被虚拟机调用过,则虚拟机认为没有必要执行。对象将会进入一个叫F-Queue的队列中。GC将会对队列中的对象第二次小规模标记,如果对象不通过finalize()方法自救的话,对象将会被回收。当然这种方式拯救对象是不推荐的,运行代价高昂,不确定性大,使用try-finally方式可能做得更好。
JDK中对Object.finalize()方法的解释是
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。
我们可以理解为当显示的调用System.gc()时,java虚拟机会调用一次finalize()方法,由于这个方法是protected类型的所以优先级比较低,我们需要通过sleep方法来等待。
当第二次调用System.gc()时,虚拟机是不会再次执行finalize方法。
以下是关于finalize()方法自救的案例。
public class FinalizaEscapeGC {
public static FinalizaEscapeGC SAVE_HOKE = null;
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
super.finalize();
System.out.println("finalize method execute ! ");
FinalizaEscapeGC.SAVE_HOKE = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOKE = new FinalizaEscapeGC();
SAVE_HOKE = null;
System.gc();
Thread.sleep(500);
if(SAVE_HOKE != null){
System.out.println("yes,i am still alive !");
}else{
System.out.println("no,i am dead !");
}
SAVE_HOKE = null;
System.gc();
Thread.sleep(500);
if(SAVE_HOKE != null){
System.out.println("yes,i am still alive !");
}else{
System.out.println("no,i am dead !");
}
}
}
- 回收方法区
其实 java虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾收集,因此很多人认为方法区(永久代)是没有垃圾收集的。方法区进行垃圾收集效率十分低,在堆中新生代一次垃圾收集一般可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此。
判断一个类是否是无用类,我们需要满足一下三点:
a. 该类所有实例都已经被回收
b. 该类的ClassLoader已经被回收
c. 该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 垃圾收集算法
-
标记-清除算法
最基础的收集算法。先标记需要回收的对象,然后统一回收所有标记的对象。
缺点:效率不高,标记清除后会产生大量的内存碎片,碎片太多可能会导致需要分配较大对象时无法找到足够的连续内存而不得不触发下一次标记-清除算法。
-
复制算法
针对上一种算法,解决效率问题,就有了复制算法。将内存按容量分成两块,每次只使用一块。当一块内存用完了,就将存过的对象复制到另一块上面,然后将使用过的内存块一次清理掉。
缺点:内存缩小一半,代价太高。
-
标记-清理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低,更关键的是浪费50%空间。因此老年代一般不能直接选用这种算法。
标记-整理算法和标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让存活对象都向一端移动,然后清理掉边端以外的内存。
- 分代收集算法
当前商业虚拟机的垃圾收集都采用这个算法。将内存划分为几块,一般吧java堆分为新生代和老年代,根据每个年代的特点采用最适合的收集算法。
在新生代中,每次垃圾收集时都有大批对象死亡,只有少量存活,那就选用复制算法。老年代必须使用标记-清理或者标记-整理算法来回收。
实例
- HotSpot的算法实现
1.Stop The World 枚举根节点
可达性算法以GC Roots 节点寻找引用链需要耗费很多时间,可达性分析对于执行时间的敏感体现在了GC停顿上。分析工作必须在一个能确保一致性的快照中进行,即时间冻结在某个点。这就是Stop The World ,即使在几乎不会停顿的CMS收集器中,枚举根节点时也是必须停顿的。
- 安全点。
OopMap帮助HotSpot快速完成GC Roots枚举。
长时间执行产生safepoint,体现在指令序列复用,如方法调用,循环跳转,异常跳转等。 - 安全区域
-垃圾收集器
-
Serial 收集器
最基本、发展最悠久的收集器,曾经虚拟机新生代收集的唯一选择。它是一个单线程收集器。
有Stop The World的不良体验。“你妈妈在给你打扫房间的时候,肯定会让你老老实实地在椅子上或者房间外待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?”这个理由还是很合理的。
-
ParNew收集器
其实就是Serial收集器的多线程版本。它能做到一边打扫,一边让你扔纸屑。
-
Parallel Scavenge收集器
这是一个使用复制算法的新生代收集器,也是并行的多线程收集器,它关注的是吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
Serial Old 收集器
Serial收集器的老年代版本,也是个单线程收集器,使用的是标记-整理算法。Parallel Old
老年代的Parallel Scavenge
6.CMS收集器
Concurrent Mark Sweep 收集器是一种以获取最短回收停顿时间为目标的收集器。
基于标记-清除算法实现的,运行过程较前面几种收集器来说复杂了点,分为四个步骤即初始标记、并发标记、重新标记、并发清除。其中初始标记、重新标记两个步骤仍然需要Stop The World .初始标记标记下GC Roots能直接关联的对象,速度很快。并发标记是进行GC Roots Tracing过程,重新标记修正并发标记期间因用户程序运作而导致标记产生变动的那一部分对象的标记记录,耗时远小于并发标记时间。
CMS收集器的缺点:
a.对CPU资源敏感,会降低总吞吐量
b.无法处理浮动垃圾
c.基于标记-清除算法的弊端
7.G1收集器
这是收集器技术发展的最前沿成果之一,是为取代CMS收集器而生的。特点有:并行和并发、分代收集、空间整合、可预测的停顿。G1收集器大致分四个步骤:初始标记、并发标记、最终标记、筛选回收。
- 内存分配与回收策略
- 对象优先在新生代Eden区分配。
- 大对象直接进入老年代。
大对象指:很长的字符串,数组等。在开发的时候应该尽量避免那种短命的大对象。 - 长期存活的对象将进入老年代。
- 实战
-
开启eclipseGC日志
其中:-XX:+PrintGCDetails为收集日志参数,-Xms20M -Xmx20M -Xmn10m 表示限制java堆20M内存,其中分配给新生代10M,老年代剩下的10M。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1
-
理解java堆的结构
java堆分新生代和老年代,新生代由一个Eden和两个Survivor组成,默认比例为8比1,即我们分配给新生代10M内存,Eden为8M,Survivor各占1M。两个Survivor用from,to 区分。
-
理解gc日志
33.125,100.667表示的是gc发生的时间,这个是虚拟机启动以来的秒数
[GC 和 [Full GC 表示垃圾收集的停顿类型,并不是用来区分老年代和新生代。如果是Full GC 表示发生过stop the world。
[DefNew,[Tenured ,[Perm 表示GC发生的区域。
方括号里面的3324K->152K(3712K) 表示GC前该内存已使用容量->GC后该内存已使用容量(该内存总容量),方括号外面的3324K->152K(11904K)表示GC前java堆已使用容量->GC后java堆已使用容量(该内存总容量),后面的0.0031680 secs 表示该内存区域GC所占用的时间,单位是秒。更详细一点的是[Times:user=0.01 sys=0.00,real=0.02 secs] 表示用户、操作内核、操作从开始到结束的墙钟时间。 案例
a. 对象优先存入Eden区
/*
*
* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
*
*/
public class WatchGcDemo1 {
private static final int _1MB = 1024*1024;
public static void testAllocation(){
byte[] a1,a2,a3,a4;
a1 = new byte[2*_1MB];
a2 = new byte[2*_1MB];
a3 = new byte[2*_1MB];
a4 = new byte[4*_1MB];
}
public static void main(String[] args) {
testAllocation();
}
}
控制台打印
[GC [DefNew: 6487K->150K(9216K), 0.0047964 secs] 6487K->6294K(19456K), 0.0048239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4574K [0x330f0000, 0x33af0000, 0x33af0000)
eden space 8192K, 54% used [0x330f0000, 0x33541fa8, 0x338f0000)
from space 1024K, 14% used [0x339f0000, 0x33a15b68, 0x33af0000)
to space 1024K, 0% used [0x338f0000, 0x338f0000, 0x339f0000)
tenured generation total 10240K, used 6144K [0x33af0000, 0x344f0000, 0x344f0000)
the space 10240K, 60% used [0x33af0000, 0x340f0030, 0x340f0200, 0x344f0000)
compacting perm gen total 12288K, used 375K [0x344f0000, 0x350f0000, 0x384f0000)
the space 12288K, 3% used [0x344f0000, 0x3454dd90, 0x3454de00, 0x350f0000)
ro space 10240K, 55% used [0x384f0000, 0x38a70f00, 0x38a71000, 0x38ef0000)
rw space 12288K, 55% used [0x38ef0000, 0x395942f0, 0x39594400, 0x39af0000)
程序中给a4赋值的时候发生了一次新生代GC(Minor GC), [DefNew: 6487K->150K(9216K), 0.0047964 secs] 6487K->6294K(19456K), 0.0048239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
,新生代从6487K变成150K,而堆内存6487K到6294K,几乎没有变化(虚拟机没有找到可回收对象)。
GC发生原因是由于a4需要分配4M内存时,发现Eden占6M,剩余的内存不足以分配,因此发生了Minor GC。GC期间虚拟机发现三个2M内存的对象无法放入Survivor区(Survivor区只有1M内存空间),只好通过分配担保机制提前转移到老年代中去了。
这次GC结束后,a4成功的放入到了Eden中,因此,程序结束后内存占用情况是:Eden 4M,Survivor空闲,老年代占用6M。
b. 大对象直接进入老年代
首先我们设置运行参数-XX:PretenureSizeThreshold=3145728,这样超过3M的对象会直接进入老年代。PretenureSizeThreshold只对ParNew和Serial两种收集器生效。
示例代码
private static final int _1MB = 1024*1024;
/**
* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureThreshold(){
byte[] a = new byte[3*_1MB];
}
运行结果
Heap
def new generation total 9216K, used 507K [0x330f0000, 0x33af0000, 0x33af0000)
eden space 8192K, 6% used [0x330f0000, 0x3316ef08, 0x338f0000)
from space 1024K, 0% used [0x338f0000, 0x338f0000, 0x339f0000)
to space 1024K, 0% used [0x339f0000, 0x339f0000, 0x33af0000)
tenured generation total 10240K, used 3072K [0x33af0000, 0x344f0000, 0x344f0000)
the space 10240K, 30% used [0x33af0000, 0x33df0010, 0x33df0200, 0x344f0000)
compacting perm gen total 12288K, used 375K [0x344f0000, 0x350f0000, 0x384f0000)
the space 12288K, 3% used [0x344f0000, 0x3454de70, 0x3454e000, 0x350f0000)
ro space 10240K, 55% used [0x384f0000, 0x38a70f00, 0x38a71000, 0x38ef0000)
rw space 12288K, 55% used [0x38ef0000, 0x395942f0, 0x39594400, 0x39af0000)
我们不难发现def new generation 新生代几乎没有变化,tenured generation老年代使用30%,就是我们的a对象。
c. 长期存活的对象将进入老年代
哪些对象该存放在新生代,哪些对象又该存放在老年代,虚拟机是通过一个年龄(Age)计数器来判断。如果对象在Eden出生并经历了一次Minor GC 后,并且可以背Survivor容纳的话,将会被移入到Survivor区,对象年龄设为1。以后对象每熬过一次Minor GC年龄会增加1岁,当增加到15岁(默认的),会被晋升到老年代。对象晋级年龄的阈值通过-XX:MaxTenuringThreshold=1设置。
示例代码
/**
* 长期存活的对象将进入老年代
* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
*/
public static void testTenuringThreshold(){
byte[] a1,a2,a3;
a1 = new byte[_1MB/4];
a2 = new byte[4*_1MB];
a3 = new byte[4*_1MB];
a3 = null;
a3 = new byte[4*_1MB];
}
运行结果
[GC [DefNew: 4695K->406K(9216K), 0.0038531 secs] 4695K->4502K(19456K), 0.0038787 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
[GC [DefNew: 4666K->0K(9216K), 0.0007054 secs] 8762K->4502K(19456K), 0.0007278 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x330f0000, 0x33af0000, 0x33af0000)
eden space 8192K, 52% used [0x330f0000, 0x33518fe0, 0x338f0000)
from space 1024K, 0% used [0x338f0000, 0x338f0088, 0x339f0000)
to space 1024K, 0% used [0x339f0000, 0x339f0000, 0x33af0000)
tenured generation total 10240K, used 4502K [0x33af0000, 0x344f0000, 0x344f0000)
the space 10240K, 43% used [0x33af0000, 0x33f55ab8, 0x33f55c00, 0x344f0000)
compacting perm gen total 12288K, used 375K [0x344f0000, 0x350f0000, 0x384f0000)
the space 12288K, 3% used [0x344f0000, 0x3454df60, 0x3454e000, 0x350f0000)
ro space 10240K, 55% used [0x384f0000, 0x38a70f00, 0x38a71000, 0x38ef0000)
rw space 12288K, 55% used [0x38ef0000, 0x395942f0, 0x39594400, 0x39af0000)
不难看出,发生了两次Minor GC ,当第一次发生GC时对象a1,256K被移入到了Survivor区(1M空间),这个时候年龄为1,而我们设定的阈值为1,当第二次GC的时候,a1达到2岁,就被移入到了老年代。from,to中为0,如果我们设置阈值为3时,结果如下
[GC [DefNew: 4695K->406K(9216K), 0.0032536 secs] 4695K->4502K(19456K), 0.0032839 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
[GC [DefNew: 4666K->406K(9216K), 0.0007343 secs] 8762K->4502K(19456K), 0.0007595 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4666K [0x330f0000, 0x33af0000, 0x33af0000)
eden space 8192K, 52% used [0x330f0000, 0x33518fe0, 0x338f0000)
from space 1024K, 39% used [0x338f0000, 0x33955b30, 0x339f0000)
to space 1024K, 0% used [0x339f0000, 0x339f0000, 0x33af0000)
tenured generation total 10240K, used 4096K [0x33af0000, 0x344f0000, 0x344f0000)
the space 10240K, 40% used [0x33af0000, 0x33ef0010, 0x33ef0200, 0x344f0000)
compacting perm gen total 12288K, used 375K [0x344f0000, 0x350f0000, 0x384f0000)
the space 12288K, 3% used [0x344f0000, 0x3454df60, 0x3454e000, 0x350f0000)
ro space 10240K, 55% used [0x384f0000, 0x38a70f00, 0x38a71000, 0x38ef0000)
rw space 12288K, 55% used [0x38ef0000, 0x395942f0, 0x39594400, 0x39af0000)
from space 1024K, 39% ,证明对象a1还在Survivor区。
d. 动态对象年龄判定
为了更好的适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须到到阈值才能晋升,如果Survivor空间中相同年龄的对象大小总和大于Survivor空间的一半,年纪大于或等于该年龄的对象直接进入老年代,无需打到阈值中要求的年龄。示例代码如下
/**
* 动态对象年龄判定
* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
*/
public static void testTenuringThreshold2(){
byte[] a1,a2,a3,a4;
a1 = new byte[_1MB/4];
a2 = new byte[_1MB/4];
a3 = new byte[4*_1MB];
a4 = new byte[4*_1MB];
a4 = null;
a4 = new byte[4*_1MB];
}
运行结果
[GC [DefNew: 4951K->662K(9216K), 0.0034462 secs] 4951K->4758K(19456K), 0.0034710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 4922K->0K(9216K), 0.0010763 secs] 9018K->4758K(19456K), 0.0011015 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x330f0000, 0x33af0000, 0x33af0000)
eden space 8192K, 52% used [0x330f0000, 0x33518fe0, 0x338f0000)
from space 1024K, 0% used [0x338f0000, 0x338f0088, 0x339f0000)
to space 1024K, 0% used [0x339f0000, 0x339f0000, 0x33af0000)
tenured generation total 10240K, used 4758K [0x33af0000, 0x344f0000, 0x344f0000)
the space 10240K, 46% used [0x33af0000, 0x33f95ac8, 0x33f95c00, 0x344f0000)
compacting perm gen total 12288K, used 376K [0x344f0000, 0x350f0000, 0x384f0000)
the space 12288K, 3% used [0x344f0000, 0x3454e060, 0x3454e200, 0x350f0000)
ro space 10240K, 55% used [0x384f0000, 0x38a70f00, 0x38a71000, 0x38ef0000)
rw space 12288K, 55% used [0x38ef0000, 0x395942f0, 0x39594400, 0x39af0000)
e. 空间分配担保
在发送Minor GC前,虚拟机会先检查老年代最大 可用的连续空间是否大于新生代所有对象的总空间。如果成立,那么Minor GC可以确保是安全的,否则,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC,尽管是有风险的,如果小于或者HandlePromotionFailure设置不允许冒险,那么这时改为进行一次Full GC。
示例代码
/**
* 空间分配担保
* VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
*
*/
public static void testHandlePromotion(){
byte[] a1,a2,a3,a4,a5,a6,a7,a8;
a1 = new byte[2*_1MB];
a2 = new byte[2*_1MB];
a3 = new byte[2*_1MB];
a1 = null;
a4 = new byte[2*_1MB];
a5 = new byte[2*_1MB];
a6 = new byte[2*_1MB];
a4 = null;
a5 = null;
a6 = null;
a7 = new byte[2*_1MB];
}
运行结果
[GC [DefNew: 6487K->150K(9216K), 0.0029545 secs] 6487K->4246K(19456K), 0.0030091 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 6544K->150K(9216K), 0.0005309 secs] 10640K->4246K(19456K), 0.0005594 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 2362K [0x330f0000, 0x33af0000, 0x33af0000)
eden space 8192K, 27% used [0x330f0000, 0x33318fe0, 0x338f0000)
from space 1024K, 14% used [0x338f0000, 0x33915b20, 0x339f0000)
to space 1024K, 0% used [0x339f0000, 0x339f0000, 0x33af0000)
tenured generation total 10240K, used 4096K [0x33af0000, 0x344f0000, 0x344f0000)
the space 10240K, 40% used [0x33af0000, 0x33ef0020, 0x33ef0200, 0x344f0000)
compacting perm gen total 12288K, used 376K [0x344f0000, 0x350f0000, 0x384f0000)
the space 12288K, 3% used [0x344f0000, 0x3454e1d8, 0x3454e200, 0x350f0000)
ro space 10240K, 55% used [0x384f0000, 0x38a70f00, 0x38a71000, 0x38ef0000)
rw space 12288K, 55% used [0x38ef0000, 0x395942f0, 0x39594400, 0x39af0000)
Warning: The flag -HandlePromotionFailure has been EOL'd as of 6.0_24 and will be ignored