java虚拟机内存分配与垃圾回收
主要讨论java 堆(Heap)的分配回收.
在学过C++的应该都知道,new一个对象时都会是堆区给对象分配一块内存空间,java一样新生成的对象都会在堆上分配空间。C++是需要开发人员自己管理内存,在需要的时候申请,不需要的时候要去释放。java则都是交给虚拟机来处理,不需要关心这些事情。
新生代老年代
因为当前垃圾收集器都是采用分代收集,所以java堆中把堆分为:新生代和老年代。新生代和老年代的空间大小比值默认为1:2(可以通过–XX:NewRatio
参数来设定)。一般来说新分配的对象都是在新生代中,新生代中的对象都具有朝生夕灭的特点。新生代又划分为三个区域:Eden、From Survivor、To Survivor。一般来说Eden区域是比较大的,两个Survivor区域大小一样,Hotspot虚拟机默认Eden和Survivor的空间比例为8:1,这个大小比例可以通过参数-XX:SurvivorRatio
来配置。新生代的回收都是采用复制算法,每次都是把Eden和Survivor中还存活的对象复制到另外的Survivor空间里,也就是新生代的利用率为90%左右。如果还存活的对象超过10%怎么办?Survivor空间不够用的时候会借助老年代的内存进行分配担保,即这些对象直接进入老年代,担保机制后面详细讲。
新生代和老年代有什么关系呢?新生代几乎是所有对象出生的地方,新生代的对象会有部分到后来进入老年代。上面提到了如果GC时Survivor空间不够用对象会直接进入老年代,另外就是新生代的对象在熬过一次次的GC之后还存活,会进入老年代空间。每次新生代中发生GC时,存活下来的对象的年龄会+1,当对象的年龄到达某个阈值(Hotspot默认为15,通过-XX:MaxTenuringThreshold
可以设置),这些对象就进入到老年代空间。另外对于较大的对象(即需要大量连续的内存空间)直接进入老年代。
新生代和老年代共同组成了java堆。
Minor GC和Full GC
Minor GC(也称为YGC (Young Generation Collection))是发生在新生代的GC动作,新生代大部分对象在回收时都会被回收掉,这里一般都是采用复制算法。Minor GC很频繁,速度也很快。
老年代的收集叫Major GC,一般是由YGC触发的(不一定的哦),有的地方也把它叫Full GC,我还是倾向于把这两个区分的。老年代的对象都是从新生代中一次次的熬过来的,对象不容易死掉。因此通常Major GC的频率比较低。而且Major GC的一次时间是很长的。老年代的收集是采用”标记-清除”算法实现。
垃圾收集器有很多的实现,当前在Hotspot中老年代一般是用CMS作为收集器,CMS收集器会有四个阶段,1)初始标记;2)并发标记;3)重新标记;4)并发清除。这里有一个问题,CMS采用标记清除,一定会有内存碎片,内存碎片过多会造成一些较大对象无法释放,那什么时候会整理(compact)对象呢?正常的CMS 执行GC,能够看到四个阶段的日志,但是在Full GC时,就只有一条日志,可以认为执行Full GC时,CMS退化成简单的标记整理算法了,在这个时候碎片的问题就解决了。或者可以认为,CMS执行GC的时候,会定期整理(Compact)对象。虚拟机是有参数-XX:+UseCMSCompactAtFullCollection
开关参数,决定在Full GC时是否要Compact。
另外是为什么一般老年代的收集会比较耗时呢?这个其实是多方面造成的,个人认为有这些原因。1).新生代空间一般来说比老年代空间小;2)新生代对象特性是朝生夕灭,每次GC时存活数量较少,因此复制对象会少很多;3)新生代和老年代收集算法不一样,老年代的收集会有多个阶段,而且偶尔老年代会有Compact过程,这个是很耗时的啊。
System.gc()和finalize()
这两个方法在实际中我们很少用到,在调用System.gc()时,只是告诉jvm,要执行一次GC,但是jvm不一定立即就执行gc。
在准备释放对象所占用的内存时,首先会调用其finalize()方法,并且在下一次gc发生时,才会真正回收内存空间。
内存分配策略
其实前面已经讲到堆空间的分区稍微提到了一些。一般来说,新的对象都会在Eden区分配空间,一些大对象直接进入老年代,这个对象的大小可以通过-XX:PretenureSizeThreshold
参数设置,这么做主要是为了避免在新生代gc时大对象来回复制,以及在新生代进入老年代时的大量复制。
简单分析gc日志
/**
* 运行参数为:
-Xms20m
-Xmx20m
-Xmn10m
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
*/
public static void main(String[] args) {
doTest();
}
public static void doTest(){
Integer M = new Integer(1024 * 1024 * 1);
byte[] bytes = new byte[1 * M]; //申请1M空间
bytes = null;
System.gc();
}
运行的时候,打印出来的日志如下:
[GC [PSYoungGen: 2023K->464K(9216K)] 2023K->464K(19456K), 0.0030790 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 464K->0K(9216K)] [ParOldGen: 0K->288K(10240K)] 464K->288K(19456K) [PSPermGen: 2640K->2639K(21504K)], 0.0123810 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
关于这个日志解析从网上找了张图片:
其实仔细看Full GC的那行日志会有一个有意思的发现,[PSYoungGen: 464K->0K(9216K)],young区空间大小由464K变成了0,然后old区由0变成了288K,到这里应该明白发生了什么事。Full GC的时候,一般会尽量清空新生代。不过有个疑问是,为什么对象移动到old区之后所占空间变小了?这个知道的可以告诉我答案。
好啦,讲到这里就大致讲完了,有问题可以讨论。感谢涛哥和顾博的指点。
Cite:
[1]深入理解Java虚拟机 (周志明)