内存分配策略

前言

在开始介绍内存分配策略之前,先啰嗦一下gc日志相关内容,要知道会读gc日志是处理java虚拟机内存问题的一项基本技能。接下来以一段gc日志为例,详细介绍下日志相关内容:

[GC (Allocation Failure) --[PSYoungGen: 8192K->8192K(9216K)] 12288K->16392K(19456K), 0.0038111 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 8192K->2731K(9216K)] [ParOldGen: 8200K->8193K(10240K)] 16392K->10924K(19456K), [Metaspace: 3334K->3334K(1056768K)], 0.0056151 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

:其实gc日志的格式是跟垃圾收集器有关的,不同的收集器,它们的格式可能都是不一样的,但是JVM的设计者为了方便程序员们阅读,将各个收集器的日志做了格式统一。

  1. gc日志开头的[GC[Full GC代表这次垃圾收集的类型,需要注意的是,它并不是用来区分是新生代的gc还是老年代gc的,如果是Full GC,只能说明这次gc是发生了STW(Stop-The-World)的;

  2. 接下来会看到[PSYoungGen[ParOldGen[Metaspace表示gc发生的区域,当然这里的区域名与垃圾收集器是相关的:

    • 如果是Serial收集器,那么新生代名称为Default New Generation,gc日志显示[DefNew
    • 如果是ParNew收集器,新生代的名称变成Parallel New Gneration,gc日志显示[ParNew
    • 如果是Parallel Scavenge收集器,gc日志则显示[PSYoungGen
  3. 区域名称后紧跟着8192K->8192K(9216K),它的意思是在gc前该内存区域已使用的量 -> gc后该内存区域的使用量(该内存区域总容量)。在方括号后面紧跟着12288K->16392K(19456K),它表示gc heap已使用的量 -> gc后heap的使用量(heap的总容量)。

  4. 在各个区域的gc相关内存变化之后,会给出该内存区域gc所用时间,有的收集器会给出具体的gc耗时时间数据,比如[Times: user=0.02 sys=0.00, real=0.01 secs],可以看到,该时间数据包括3个时间:

    • user:用户态消耗的cpu时间;
    • sys:内核态消耗的cpu时间;
    • real:操作从开始到结束所经过的实际时间

:需要注意的是,real时间包括各种非运算的等待耗时,比如等待磁盘I/O,但是cpu时间是不包括这些耗时的。可能熟悉gc日志的同学可能会问,既然real的时间包括等待时间,user和sys不包括等待时间,那为什么好多时候user或者sys的时间会超过real呢?我们现在绝大多数服务器都是多cpu或者多核的,当多个线程操作时,user和sys会叠加这些cpu时间,所以看到user或者sys的时间超过real是很正常的。

啰里吧嗦介绍完gc日志后,接下来我们就可以进入正题,看一下JVM对象内存的分配策略。

内存分配策略

对象内存的分配,简单点儿说,就是在heap上分配内存(JIT编译可能间接在栈上分配),对象首先会在Eden区分配,当然,如果启动了本地线程分配缓存,则优先在线程的TLAB上分配,同时,也会有少数情况会在old区分配,具体的分配细节跟垃圾收集器以及JVM内存相关参数相关。接下来就根据实例具体分析一下相关分配策略。

1. 对象首先在Eden区分配

在大多数情况下,对象都优先在eden区分配内存,当eden区内存空间不够时,JVM会发起一次Monitor GC(对young区的gc)

案例1

在案例1中,通过JVM参数-Xms20M -Xmx20M -Xmn10M限制了heap的大小为20M并且不可扩展,young区的大小为10M,剩下的10M给old区,在main方法中,创建byte1到btye4四个对象,一共10M,我们看一下会发生什么?
案例1.gc日志

从gc日志可以看出:

  1. 在分配bytes1到bytes3后,eden区没有额外的空间,再创建bytes4的时候,此时eden区内存不够,触发Minitor GC,本次gc结束后,yong区的6441k变成872k,但是由于bytes1到bytes3对象都是存活的,所以总得内存量其实并没有减少;

  2. 在发生Monitor GC的过程中,由于Survivor空间只有1M,不足以放下bytes1到bytes3的任何一个对象,此时,通过分配担保机制,会提前进入old区;

  3. 这次GC结束后,bytes4被顺利分配在eden区,此时,eden区占用6M,Survivor空闲,old区被占用4M。

2. 大对象直接进入老年代

:大对象定义:所谓的大对象,其实就是指需要大量的连续内存空间的对象,比如长度很长的数组。

对于JVM来说,需要分配大对象是一个坏消息,如果程序中经常出现大对象就容易导致gc的提前触发。当然,JVM提供参数-XX:PretenureSizeThreshold,一旦对象所需内存大小大于该参数配置的阈值,直接在old区为其分配内存空间。当然,这样做的目的一则是为了避免gc提前出发,二则是为了避免在eden区和Survivor区发生大量的内存拷贝。接下来还是以一个简单的例子验证该规则。

案例2

在案例2中,通过参数-XX:PretenureSizeThreshold=4194304设置阈值为4M,一旦待分配对象大小超过4M,直接在old区进行内存分配。在main方法中,要创建一个大小为5M的byte数组,我们来看下gc日志,看看这个对象是不是直接在old区分配内存的。
案例2.gc日志

从gc日志标红的地方可以看出,byte4确实直接被放在了old区。

:需要注意的是,-XX:PretenureSizeThreshold只对ParNew和Serial垃圾收集器有效,如果你需要使用该参数的话,可以使用ParNew + CMS。

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

JVM采用分代收集来管理内存,为了在gc的时候能够确认哪些对象要放在young区,哪些对象放在old区,JVM为给每一个对象都定义了一个年龄计数器,如果对象在eden区被分配内存并且经过第一次Monitor GC后还存活,此时该对象会被移动到Survivor区,对象的年龄被设置成为1,该对象在Survivor区中每经过一次Monitor GC还不被回收,年龄就加1,当它的年龄增加达到一定的值时(默认值是15),对象就会被晋升到old区。当然,JVM提供参数-XX:MaxTenuringThreshold来设置对象晋升到old区的年龄阈值。

案例3

在案例3中,通过-XX:MaxTenuringThreshold=1设置对象晋升到old区的年龄阈值为1,那么,gc结束后,bytes1和bytes2均会进入old区,我们看一下gc日志看看是不是这样。
案例3.gc日志

从gc日志可以看出,经过两次Monitor GC,bytes1和bytes2均进入old区,bytes3在eden区。

4. 动态年龄判定

虽然JVM要求对象年龄必须要达到-XX:MaxTenuringThreshold设置的阈值才能晋升到old区,但是,为了更好的适应不同的内存使用情况,JVM增加了一个新的晋升到old区的条件:如果在Survivor区中相同年龄的对象所占内存空间大于Survivor区的一半,不小于该年龄的对象可以直接进入old区,不需要达到-XX:MaxTenuringThreshold设置的阈值。

案例4

gc日志:
案例4.gc日志

从gc日志可以看出,经过两次Monitor GC,由于bytes1所占用内存空间大于Survivor区的一半,bytes1和bytes2均进入old区。

5. 空间分配担保

在发生Monitor GC之前,JVM会检查old区的最大可用连续空间是否大于young区所有对象总空间,如果条件成立,那么Monitor GC一定是安全的,进行一次Monitor GC,如果不成立:

  • 在jdk6 update 24之前JVM则会读取参数-XX:-HandlePromotionFailure值判断是否允许担保失败,如果允许,检查old区的最大可用连续空间是否大于晋升到old区对象的平均大小,如果大于,再进行一次Monitor GC,如果小于或者不允许担保失败,则进行一次Full GC;

  • 在jdk6 update 24之后,虽然JVM还定义参数-XX:-HandlePromotionFailure,但是已经不会再使用它,规则变为只要old区最大可用连续空间大于晋升到old区的对象的平均大小,就进行一次Monitor GC,否则,进行一次Full GC,JVM源码也可以验证此规则:

    JVM源码

看到这里大家估计还是有点懵,还是不理解为什么要空间分配担保,接下来就解释下为什么需要空间分配担保。

为什么需要空间分配担保?

注:空间分配担保其实就是JVM确认old区是否可以容纳Monitor GC后晋升到old区的对象们。

由于young区的gc算法是复制收集算法,为了内存的使用率,JVM只使用其中的一个Survivor取作为中间转换空间,当出现大量对象在Monitor GC后还存活的,此时,Survivor区无法容纳的对象直接进入old区,在进入old区之前JVM一定要确认old区是否有足够的剩余空间可以容纳这些对象。由于在回收之前并不知道有多少对象要进入old区,JVM设计者认为可以取每一次Monitor GC晋升到old区的对象容量的平均大小为经验值,将该经验值与old区剩余空间比较,来决定是否要进行一次Full GC让old区释放出更多的空间。但是,取平均值毕竟是一种动态概率手段,如果某一次Monitor GC后存活对象陡增,远远高于平均值,此时还是会担保失败,一旦出现担保失败,JVM会发起一次Full GC。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容