前面的两小节,我分享了一下JVM的垃圾回收算法和垃圾回收器,本节中,我们来看看JVM的内存分配到底是如何进行的,作为对前面两节内存回收的补充。
从前面的内存回收中我们了解到,Hotspot JVM中的垃圾收集器的设计思路都是基于分代回收的,尽管在G1收集器不再像任何(当前已使用的)其他收集器一样,明显的把JVM堆内存区域分为新生代、老年代、永久代,但是,在逻辑上依然存在着这样的概念。为什么会出现这样的情况呢?我想大概还是因为那两个假设。
那下面我主要来看看JVM堆区对象分配的一般规则:
- 1. 对象优先在Eden区分配
- 2. 大对象直接进入老年代(-XX:PretenureSizeThreshold=3145728 这个参数来定义多大的对象直接进入老年代)
- 3. 长期存活的对象将进入老年代(在JDK8中测试,-XX:MaxTenuringThreshold=1的阀值设定根本没用)
- 4. 动态对象年龄判定(虚拟机并不会永远地要求对象的年龄都必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄的所有对象的大小总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代)
- 5. 空间分配担保,逻辑如下(仅限6.0_24之前的JDK版本,6.0_24及之后的版本逻辑参考6)
- 5.1 在Minor GC之前,检查老年代最大连续可用空间是否大于新生代所有对象总空间,如果是,则确认Minor GC是正常的,否则继续
- 5.2 查看HandlePromotionFailure设置值是否允许担保失败,如果允许则继续,否则5.5;
- 5.3 检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果是则5.4,否则5.5;
- 5.4 尝试着进行一次Minor GC,尽管可能有风险;
- 5.5 进行full GC。
- 6. 只要老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minor GC,否则会进行full GC
说上面只是一般规则,主要因为这和JVM采用什么样的GC机制有着密切的关系。如,在G1回收器中,内存被分成了Region(当然逻辑上还是存在新、老、永久的概念),而不是CMS回收器中的新生代、老年代、永久代。另外在具体的运行过程中,JVM内部的内存分配算法远比这复杂的多得多。举一个我知道的例子,TLAB(Thread Local Allocation Buffer)就是为了优化在多线程环境下,避免在内存分配的过程中锁竞争而引起的线程频繁切换而导致内存分配性能受影响而采取的优化措施。具体的优化过程,可以自行百度“Java TLAB”或者“JMM”了解更为详细的信息。当然我在后续的研究过程中,还会继续深究并分享出来。
JVM的内存管理是JVM的核心之一,如何高效的管理和回收系统中宝贵的内存资源,是Java语言编写的应用程序得以稳定、高效运行的基石,而深入理解JVM是如何做这些工作的原理,并进行多方面的实践,又是Java程序员能够编写健壮、高效的代码的基石。尽管学习了这么多,但最终只有真正在工作中运用和实践,才能才敢真正说掌握了。
参考:《深入理解Java虚拟机》