主要讨论 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 虚拟机 (周志明)
来源: http://www.92to.com/bangong/2016/12-07/14002082.html