Come on ! Java对象内存分配与回收策略

简介: Come on ! Java对象内存分配与回收策略

Java所承诺的自动内存管理主要是针对对象内存的回收和对象内存的分配。


在Java虚拟机的五块内存空间中,程序计数器、Java虚拟机栈、本地方法栈内存的分配和回收都具有确定性。一般都在编译阶段就能确定下来需要分配的内存大小,并且由于都是线程私有,因此它们的内存空间都随着线程的创建而创建,线程的结束而回收。也就是说这三个区域的内存分配和回收都具有确定性。


而Java虚拟机中的方法区因为是用来存储类信息、常量、静态变量等,这些数据的变动性较小,因此不是Java内存管理重点关注的区域。


而对于堆,所有线程共享,所有的对象都需要在堆中创建和回收。虽然每个对象的大小在类加载的时候就能确定,但对象的数量只有在程序运行期间才能确定,因此堆中的内存分配具有较大的不确定性。此外,对象的生命周期长短不一,因此需要针对不同生命周期的对象采用不同的内存回收算法,增加了内存回收的复杂性。


综上,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配)。对象主要分配在新生代的Eden区,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。


下面描述的是在使用Serial/Serial Old收集器下(ParNew/Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。

【1】对象优先在Eden区中分配

目前主流的垃圾收集器都会采用分代回收算法,因此将堆内存分为新生代和老年代。


在新生代中为了防止内存碎片问题,因此垃圾收集器一般都采用“复制”算法。因此,堆内存的新生代被进一步分为:Eden区+Survior1区+Survior2区。也有说法为Eden+From Survior+ To Survior。


每次创建对象时,首先会在Eden区中分配;

若Eden区空间不足,尝试在Survior1区中分配,仍然不足则发生MinorGC。

若Eden区+Survior1区剩余内存太少,导致对象无法放入该区域时,就会启用“分配担保”,将当前Eden区+Survior1区中的对象转移到Survivor(或老年代)中,然后再将新对象存入Eden区。

新生代内存分配时,将内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中一块Survior。新生代采用垃圾收集算法为复制算法,在回收时,将Eden和Survior中还存活的对象一次性地复制到另外一块Survior空间上,最后清理掉Eden和刚才用过的Survior空间。


【2】大对象直接进入老年代


所谓“大对象”就是指一个需要占用大量连续存储空间的对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息,更坏的消息则是遇到一群“朝生夕死”的“短命大对象”,写程序的时候应当避免。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。


当发现一个大对象在Eden区+Survior1区中存不下的时候就需要分配担保机制把当前Eden区+Survior1区的所有对象都复制到老年代中区。


我们知道,一个大对象能够存入Eden区+Survior1区中的概率比较小,发生分配担保机制的概率比较大,而分配担保需要涉及到大量的复制,就会造成效率低下。


因此,对于大对象我们直接把他放到老年代中去,从而就能避免大量的复制操作。


那么,什么样的对象才是“大对象”呢?


通过-XX:PretrnureSizeThreshold参数设置大对象。字节大小超过该参数的对象被认为是“大对象”,直接进入老年代。这样做的目的是避免在Eden区及两个Survior区之间发生大量的内存复制(新生代采用复制算法收集内存)。


需要注意的是,PretrnureSizeThreshold该参数只对Serial(串行收集器)和ParNew收集器有效。Parallel Scavenge收集器不认识这个参数,Parallel Scavenge 收集器一般不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew+CMS收集器组合。


【3】生命周期较长的对象进入老年代


老年代用于存储生命周期较长的对象,那么如何判断一个对象的年龄呢?


新生代中每个对象都有一个年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活并且能被Survior容纳的话,将被移动到Survior空间中,并且对象年龄设为1。对象在Survior区中每“熬过”一次MinorGC,年龄就增加1岁。当它的年龄增加到一定程度(默认为15岁),将会被晋升到老年代中。


使用-XXMaxTenuringThreshold设置新生代的最大年龄。设置该参数后,只要超过该参数的新生代对象都会被转移到老年代中去。


【4】动态对象年龄判定


为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果在Survior空间中相同年龄所有对象大小的总和大于Survior空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。


【5】分配担保策略

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象空间。如果这个条件成立,那么minor GC可以确保是安全的。


如果上述条件不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那这时要更改为一次Full GC。通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保


这个过程就是分配担保。那么冒险是冒了什么风险?


前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survior空间来作为轮换备份。因此当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survior无法容纳的对象直接进入老年代。


而老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。


取平均值进行比较其实仍然是一种动态概率的手段。也就是说,如果某次MinorGC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(HandlePromotion Failure)。如果出现了担保失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。


这个规则在JDK6 Update24之后变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则将进行Full GC。



【6】总结


① 分配担保是老年代为新生代作担保。

② 新生代中使用“复制”算法实现垃圾回收,老年代中使用“标记-清除”或“标记-整理”算法实现垃圾回收。

只有使用“复制”算法的区域才需要分配担保,因此新生代需要分配担保,而老年代不需要分配担保。


目录
相关文章
|
15天前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
5天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
18 4
|
19天前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
39 17
|
18天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
19天前
|
消息中间件 监控 算法
Java性能优化:策略与实践
【10月更文挑战第21】Java性能优化:策略与实践
|
18天前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
16 0
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
360 0
|
19天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
41 1
|
24天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
28天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。