转载

憨人笔记之JVM--内存分配和回收策略

话不多说,肝就完了。

今天聊聊对象内存分配和回收策略。

在之前的笔记中,已经讲到过内存结构,对象在内存中的分配,以及垃圾回收算法及垃圾收集器的相关知识。本篇笔记记录一下对象的如何在内存中进行分配以及如何从内存中回收。

在Java中,其相对于C语言来说,它不用人为的去操作对象内存的释放。直接由虚拟机自动解决了。而归根结底,虚拟机解决的是两个问题:给对象分配内存、回收分配给对象的内存。

在聊到虚拟机内存结构的时候,与对象关系最为密切的就是堆空间,任何对象的创建都是在堆空间去分配内存的。而且对于新创建的对象,主要分配在新生代的Eden区上。如果启动本地线程分配缓冲(TLAB),那么将会按照线程优先在TLAB上分配。对于大对象,也可能直接被分配在老年代中。

具体如何分配,其规则并不是百分百固定的,主要取决于垃圾收集器的组合以及虚拟机与内存相关的参数配置。

新生对象优先分配至Eden区

在学习堆内存的时候,有说到整个对内存空间会按照对象的年龄进行分代,分为新生代和老年代。这是主要关注的区域(当然还有元数据区)。

对于新生 对象,会优先分配在Eden区 。可以这么理解, 新生代的Eden区是大部分对象生命周期的开始 。回顾一下在堆内存章节所学习到的对象在新生代的流传过程。新生代分为Eden区和Survivor区(Survivor From和Survivor To)。

  1. 对象首先会被创建在Eden区,当Eden区内存不足以为新对象分配内存时,会触发一次MinorGC。此时,会将Eden区中还存活的对象转移复制到Survivor区(其中一个Survivor区),同时会将对象头中的对象年龄标记加1。
  2. 在每一次触发MinorGC的时,都会将Eden区和Survivor区中还存活的对象复制转移到另外一个Survivor区,对象年龄加1。(Survivor区分为两块,在内存分配的时候,始终只会使用到其中一块区域)。
  3. 当对象的年龄达到15(默认15)的时,就会被复制转移到老年代中。

需要注意的是,在新生代中,存在一种叫做 分配担保机制 。什么叫做分配担保机制呢。官方点的描述就是,当触发MinorGC时,存活的对象会转移到另外一个Survivor区,当这个区域无法保存还存活的对象,那么就会触发分配担保机制。直接将对象复制到老年代中。通俗点说,银行放贷,你作为担保人(老年代),当实际还款人(新生代)换不起贷款的时候(对象所需的内存空间),将直接由担保人来进行还款(老年代的空间用来存对象)。对,大概就是这么个意思。

大对象直接进入老年代

虽然前面说到,新生的对象都会分配在Eden区,但是实际上新生代和老年代的内存比大致为1:2,可以模糊点理解,就是说老年代的空间大小是新生代空间大小的两倍。

什么样的对象又会被定义为大对象呢,通常是指需要大量连续内存来存储的对象。例如超长的字符串和数组。但是有一点要明确的是,老年代如果被大量大对象所占据,会很容易触发Full GC,所以,在实际编码过程中,应当避免大对象的出现。从而降低垃圾收集的几率。

在虚拟机中,存在一个**-XX:PretenureSizeThreshold**参数,让大于这个设置值的对象直接分配至老年代,这样就能够避免新生代中频繁的发生在Eden区和Survivor区进行对象的内存复制。

长期存活的对象进入老年代

在说到新生代时,MinorGC会触发对象的复制(在Eden区和Survivor区)。每一次复制都会将对象的年龄加1。当对象的年龄到达一定的阀值时。就会被移动到老年代中。在虚拟机中,可以通过**-XX:MaxTenuringThreshold**来设置这个阀值。

以上就是关于虚拟机在内存分配方面的举措: 新生对象分配在Eden区、大对象直接进入老年代、长期存活的对象进入老年代

动态判断对象年龄

在虚拟机中,并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold指定的年龄阀值才进入到老年代。当Survivor空间中,年龄相同的对象总内存大小超过了Survivor空间的一半的时候,就会被复制转移到老年代,而无需达到MaxTenuringThreshold所指定的年龄。怎么理解呢。假设当前Survivor空间为100K,MaxTenuringThreshold设置的值为15,存在两个相同年龄的对象A、B,两个对象的总大小达到了60K,年龄均为5,那么这两个对象的总大小已经超过了Survivor区的一半,则均会被直接送入到老年代中。

所以,动态判断对象年龄并不是说对象非得到了指定的年龄值才会被移动到老年代,而是会根据内存实际情况,动态的去判断对象是否应该移动到老年代。

空间分配担保

在前面说到新对象创建在Eden区时,提到过分配担保机制(在发生MinorGC时,若Survivor区不足以来保存对象,则会将对象分配到老年代)。虚拟机在发生MinorGC之前,会先检查老年代的最大可用连续内存空间是否大于新生代所有对象的总空间大小。如果成立,那么MinorGC是可以保证安全的。如果不成立,则会校验HandlePromotionFailure参数设置的值是否允许担失败。如果允许,则会继续检查老年代最大可用连续内存是否大于之前晋升到老年代对象的平均大小,若大于,则尝试进行一次MinorGC,若小于,或者说HandlePromotionFailure设置不允许担保失败,此时则进行一次FullGC。

在新生代中,采用的是复制算法,而复制算法每次都会空置其中一个Survivor区用来MinorGC时进行轮换。当大量的对象在MinorGC之后还继续存活,则会被送入到老年代。而老年代要容纳新生代中转移过来的对象,前提是必须老年代有足够的连续内存来容纳这些对象。但是在内存回收之前,会有多少对象存活下来是个未知数,所以,每一次都会取上一次从新生代中晋升到老年代中的对象大小的平均值作为经验值来与老年代剩余的空间进行比较,从而决定是否进行FullGC来进行垃圾回收来腾出空间。

后面会单独记录通过实际代码和GC日志分析来更好的理解上面的内容。

不怕路歹行不怕大雨淋,心上一字敢 面对我的梦,甘愿来作憨人。 --<憨人>

原文  https://juejin.im/post/5ebab33c6fb9a043780fe828
正文到此结束
Loading...