前言
在开始介绍内存分配策略之前,先啰嗦一下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的设计者为了方便程序员们阅读,将各个收集器的日志做了格式统一。
gc日志开头的
[GC
和[Full GC
代表这次垃圾收集的类型,需要注意的是,它并不是用来区分是新生代的gc还是老年代gc的,如果是Full GC,只能说明这次gc是发生了STW(Stop-The-World)的;-
接下来会看到
[PSYoungGen
,[ParOldGen
,[Metaspace
表示gc发生的区域,当然这里的区域名与垃圾收集器是相关的:- 如果是Serial收集器,那么新生代名称为
Default New Generation
,gc日志显示[DefNew
; - 如果是ParNew收集器,新生代的名称变成
Parallel New Gneration
,gc日志显示[ParNew
; - 如果是Parallel Scavenge收集器,gc日志则显示
[PSYoungGen
- 如果是Serial收集器,那么新生代名称为
区域名称后紧跟着
8192K->8192K(9216K)
,它的意思是在gc前该内存区域已使用的量 -> gc后该内存区域的使用量(该内存区域总容量)。在方括号后面紧跟着12288K->16392K(19456K)
,它表示gc heap已使用的量 -> gc后heap的使用量(heap的总容量)。-
在各个区域的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中,通过JVM参数
-Xms20M -Xmx20M -Xmn10M
限制了heap的大小为20M并且不可扩展,young区的大小为10M,剩下的10M给old区,在main方法中,创建byte1到btye4四个对象,一共10M,我们看一下会发生什么?从gc日志可以看出:
在分配bytes1到bytes3后,eden区没有额外的空间,再创建bytes4的时候,此时eden区内存不够,触发Minitor GC,本次gc结束后,yong区的6441k变成872k,但是由于bytes1到bytes3对象都是存活的,所以总得内存量其实并没有减少;
在发生Monitor GC的过程中,由于Survivor空间只有1M,不足以放下bytes1到bytes3的任何一个对象,此时,通过分配担保机制,会提前进入old区;
这次GC结束后,bytes4被顺利分配在eden区,此时,eden区占用6M,Survivor空闲,old区被占用4M。
2. 大对象直接进入老年代
注:大对象定义:所谓的大对象,其实就是指需要大量的连续内存空间的对象,比如长度很长的数组。
对于JVM来说,需要分配大对象是一个坏消息,如果程序中经常出现大对象就容易导致gc的提前触发。当然,JVM提供参数-XX:PretenureSizeThreshold
,一旦对象所需内存大小大于该参数配置的阈值,直接在old区为其分配内存空间。当然,这样做的目的一则是为了避免gc提前出发,二则是为了避免在eden区和Survivor区发生大量的内存拷贝。接下来还是以一个简单的例子验证该规则。
在案例2中,通过参数
-XX:PretenureSizeThreshold=4194304
设置阈值为4M,一旦待分配对象大小超过4M,直接在old区进行内存分配。在main方法中,要创建一个大小为5M的byte数组,我们来看下gc日志,看看这个对象是不是直接在old区分配内存的。从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中,通过
-XX:MaxTenuringThreshold=1
设置对象晋升到old区的年龄阈值为1,那么,gc结束后,bytes1和bytes2均会进入old区,我们看一下gc日志看看是不是这样。从gc日志可以看出,经过两次Monitor GC,bytes1和bytes2均进入old区,bytes3在eden区。
4. 动态年龄判定
虽然JVM要求对象年龄必须要达到-XX:MaxTenuringThreshold
设置的阈值才能晋升到old区,但是,为了更好的适应不同的内存使用情况,JVM增加了一个新的晋升到old区的条件:如果在Survivor区中相同年龄的对象所占内存空间大于Survivor区的一半,不小于该年龄的对象可以直接进入old区,不需要达到-XX:MaxTenuringThreshold
设置的阈值。
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确认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。