本博文对应《深入理解java虚拟机》第三章第三部分。
内存管理无非就是两件事——分配内存和回收内存,之前已经有讲述了垃圾回收机制,但是其实没有讲述一个对象在内存里是怎么分配的。
优先在Eden区
在没有读这本书之前,我一直以为是所有的对象都会直接分配到新生代的Eden区里。实际上是绝大部分的对象会被分配到这里,当这里没有足够的空间的时候,虚拟机就会触发一次Minor GC(次要垃圾回收)。示例代码如下所示:
1 | public class testAllocation { |
这段代码很简单,就是先后分配了2MB的空间给3个对象,然后最后一个对象分配到了4MB的空间。
然后编译执行这段代码:
1 | javac testAllocation.java |
-Xms,初始的Heap的大小。这里分配了20M-Xmx,最大Heap的大小。一般和Xms一样,也是20M,这样堆就不可以扩展了-Xmn,新生代的大小,这里分配了10M,同样意味着老年代也是10M-XX:+PrintGCDetails,显示详细的日志信息。-XX:SurvivorRatio=8,新生代中,Eden与一个survivor空间的比例。而新生代有两个survivor。所以它们之间的比例是8:1:1,也就是其实的Eden区域是8M,新生代的可用空间是9M
代码中可以看到,分配a1,a2,a3这三个共计6M的空间是没有问题的,但是最后一个a4会发现当时的可用空间只剩下3M了,但是a4这个对象需要4M,所以会发生一次Minor GC,但是显然a123这三个对象无法被回收,而这三个对象每个都比一个survivor空间要大(2M>1M),所以它们只能被放到老年代。然后Eden空间就被清空了,就能够放得下a4了。实际运行结果如图所示:

- 新生代可用空间,从
6M多一点的空间变成了266K(因为三个对象被移走了),总共是9216K(新生代的可用空间,为eden+from的总量) - GC之前堆的容量(
6651K,和上面那条里的6651K是一样的),GC之后的堆的容量(这里我认为有问题)和堆的总容量 - 新生代共计
9M可用空间,最后用了4M(用来存放a4) - 新生代的三个区域的总空间和所用空间,可以看到确实是
8:1:1
大对象直接入老年代
java虚拟机最讨厌什么?短命的大对象。大对象需要连续的空间,而短命大对象又只需要很短时间的连续空间,好不容易费力清理出空间,才住一小会就退宿了,差不多就是这种感觉。
大对象对于虚拟机来说比较好的办法是直接放到老年代,因为新生代的survivor区域一般来说很小,不如直接放到老年代比较划算。示例代码如下:
1 | public class testPretenureSizeThreshold { |
代码来说非常简单,创建一个4M大小的字节数组。然后编译并且运行:
1 | javac testPretenureSizeThreshold.java |
PS:Pretenure的意思是青春期(就是之前一直说的老年代…),且这里只能用字节数,不能直接写成3M

可以看到红框内 新生代几乎是没有使用空间,而老年代刚好4M。
长期存活进入老年代
之前提到对象是在新生代的Eden空间中的,经过第一次的Minor GC之后,如果对象存活,就会被安置在survivor空间中,并且年龄加一。之后每次经过一次Minor GC,年龄就加一,到指定年龄之后(默认是15)就会被放到老年代里,你可以通过-XX:MaxTenuringThreshold=15来修改这个值。示例略过。
显然如果要手动指定这个会显得有点死板,所以其实还有另外一种机制,当survivor空间里相同年龄的对象所占空间超过survivor的一半的时候,就会把年龄大于或者等于该年龄的对象全部搬动到老年代里面。
空间分配担保
之前有讲到Minor GC的是把新生代中from survivor区域和Eden区域中的对象做一次垃圾回收然后放到to survivor区域中,但是to survivor区域并不能保证一定能够存放下(毕竟这个区域默认只占10%),之前讲到的是需要老年代来帮助执行,用的就是这个机制。
这个分成几种情况来进行讨论,首先是老年代的空间足够放得下Eden+from survivor区域的所有对象,那放心大胆进行操作好了不会出现任何问题。第二种情况是老年代放不下,这个时候可能会出现问题,但是其实出现问题概率不大,所以你可以通过设置HandlePromotionFailure来设置是否允许担保失败。建议还是打开,因为基本上很少出现某一次Minor GC突然有很多对象存活下来导致连老年代都放不下从而触发full gc的情况的。
这个在JDK6之后发生了变化,HandlePromotionFailure已经没有用了。