在Java虚拟机(JVM)中,内存管理是一个关键的环节,它涉及到对象的创建、内存的分配与回收等多个方面。前两篇文章中,我们已经介绍了内存区域划分和对象创建的过程。本文将重点探讨JVM中的内存分配机制。
1 空闲地址维护
在 JVM 的内存管理中,空闲地址的维护是通过一系列策略共同实现的,包括指针碰撞、空闲列表、CAS(Compare-And-Swap)操作以及TLAB(Thread-Local Allocation Buffer)。
指针碰撞是用于单线程环境下的一种快速内存分配策略,它通过移动一个指针来找到空闲内存区域,从而为新对象分配空间。但在多线程环境中,由于多个线程可能同时尝试更新这个指针,就需要一种同步机制来保证操作的原子性。这时,CAS操作就显得尤为重要,它是一种无锁的原子操作,可以确保在多线程竞争下,内存分配的安全性和原子性。
空闲列表是另一种适用于多线程环境的内存分配策略,它维护了一个记录所有空闲内存块的列表。通过CAS操作,线程可以从空闲列表中安全地选择和分配内存块,避免了多线程同时访问同一内存块时的竞态条件。
TLAB是一种线程私有的内存分配策略,它为每个线程提供了一块独立的内存区域,用于对象的分配。这样,大多数内存分配可以在线程内部完成,减少了线程之间的同步开销。当TLAB中的内存耗尽时,线程会尝试从全局空闲列表中分配新的内存块,这个过程同样可以利用CAS操作来保证线程安全。
综合来看,指针碰撞、空闲列表、CAS操作和TLAB共同构成了JVM内存分配的核心机制,它们相互配合,确保了在不同线程环境下内存分配的效率和安全性。通过这种精细的内存管理策略,JVM能够支持高并发的应用程序,同时保持内存使用的高效性和稳定性。
2 内存分配流程
JVM中对象内存分配的流程大致会经历栈分配判断—Eden区分配判断—Old区分配判断等,如下图:
3 对象分配规则
JVM在内存分配时遵循一定的规则,以优化内存使用和垃圾回收性能。
3.1 栈上分配
对于生命周期较短的临时对象,JVM可能会选择在栈上分配,以避免在堆上分配和后续的垃圾回收开销。
JVM会通过逃逸分析来确定对象不会被外部访问,如果不会则可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,减轻了垃圾回收的压力。(关于逃逸分析回在后续分享JIT编译优化时进行介绍)
3.2 对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
3.3 大对象直接进入老年代
为了避免对象在新生代和Survivor区之间频繁复制,大对象(如大数组)会直接在老年代分配。
JVM参数 -XX:PretenureSizeThreshold
可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代。比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC,即当对象大小超过1MB则视作大对象进入老年代。这个参数只在 Serial 和ParNew两个收集器下有效。
3.4 老年代动态年龄判断机制
JVM根据一批对象在Survivor区中的占比,动态这批对象中可能是长期存活的对象年龄,以使其尽早进入老年代。
JVM参数 -XX:TargetSurvivorRatio
可以设置Survivor区域的目标使用率,超过设置的使用率会触发老年代动态年龄判断,大于Survivor区域里满足这个使用率的年龄最大值的对象,就可以直接进入老年代了。默认是50%。例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。
3.5 长期存活对象进入老年代
对于在新生代中存活时间较长的对象,JVM会根据其年龄和大小,决定是否将其提升到老年代。
每个对象的对象头Mark Word中会记录当前对象的分代年龄(可参考上一章【JVM】03. 内存管理(二):对象创建),即对象每经过第一次 Minor GC,分代年龄就加1,当分代年龄增加到阈值(默认为15,CMS收集器默认6,不同的垃圾收集器会略微有点不同),对象就会晋升到老年代。对象分代年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
3.6 空间分配担保机制
在进行Full GC之前,JVM会检查老年代的剩余空间是否足够容纳新生代中所有对象,这个过程称为空间分配担保(SATB)。
- 年轻代每次Minor GC之前JVM都会计算下老年代剩余可用空间
- 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会看是否设置了
-XX:-HandlePromotionFailure
参数(jdk1.8默认设置了这个参数) - 如果有这个参数,就会看看老年代的可用内存大小,是否小于之前每一次Minor GC后进入老年代的对象的平均大小,如果小于就会进行一次Full GC,否则进行Minor GC
- 如果没有设置这个参数,那么就会直接触发一次Full GC,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
- 如果Minor GC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发Full GC,Full GC完之后如果还是没有空间放Minor GC之后的存活对象,则也会发生“OOM”
4 总结
在本篇博文中,我们深入探讨了JVM内存分配的相关知识,包括空闲地址的维护、内存分配的流程以及对象分配的规则。特别地,对象分配规则对于JVM性能调优至关重要,因为它们直接影响到对象在堆内存中的生命周期,以及垃圾回收的行为。
尤其是对象进入老年代的规则,对于减少Full GC的发生非常关键。Full GC会暂停应用程序的执行,以清理整个堆内存,这可能导致性能瓶颈。通过合理设置对象晋升老年代的条件,可以减少Full GC的频率,从而提高应用程序的响应速度和吞吐量。
在后续的博文中,我们将继续深入JVM内存管理的高级话题,包括垃圾回收算法、内存泄漏分析和调优案例等。通过这些内容,我希望能够为读者提供更多的知识和工具,以更好地理解和优化Java应用程序的内存使用。