java技术体系中所提倡的自动内存管理最后可以归结为自动化解决两个问题:给对象分配内存以及回收分配给对象的内存,关于回收内存,上一篇已经说了很多JVM内部的垃圾收集器体系以及他运行的垃圾收集算法,现在我们在探讨一下给对象分配内存的时候需要注意的事项。
对象的内存分配,大的讲,就是在java heap上分配,对象主要分配在新生代的Eden空间上,如果启用了本地线程缓冲分配,那么将按照线程优先在TLAB上分配,少数情况也可能直接分配在老年代。但是分配的原则不是百分之百固定的,还要取决你采用的那种垃圾收集器的二组合,以及虚拟机中内存设置的相关参数。
1:对象优先在Eden上分配
大多数情况下,对象在新生代Eden区域中进行分配,档Eden区域中没有足够的空间进行分配的时候,发起一次Minor GC
注意提到的Minor GC和Full GC
Minor GC:这就是新生代的GC,指的是新生的垃圾收集动作,由于java对象大多具有朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
Full GC:指的是发生在老年代的GC,又被称作Major GC,出现了Major GC,经常会伴随这至少一次的Minor GC,但这也不是绝对的,一般来说Major GC的速度会比Minor GC慢十倍以上。
2:大对象直接进入老年代
所谓的大对象是指大量需要连续java内存空间的java对象,最典型的大对象就是那种很长的字符串或者数组。大对象对于虚拟机分配来说是个很坏的消息,但是也有更坏的,就是那种朝生夕死的大对象,经常会出现大对象容易导致内存还有不少空间的时候就提前出发垃圾收集机制以获取足够的连续空间来“安置”他们。
虚拟机提供了一个参数,上一篇也讲到过,就是-XX:PretenureSizeThreshold参数,让大于这个参数的对象直接进入老年代分配,这样的目的就是避免在Eden区以及两个Survivor区之间发生大量的内存复制,这是因为新生代采用的复制算法,复制大量的生存对象会耗费很多资源。
3:长期存活的对象将进入老年代
虚拟机既然采用分代收集的思想来管理内存,那内存回收的时候就必须能识别那些对象放在新生代,那些对象放在老年代,为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并且经过一次Minor GC后仍然存活,并且能被Survivor接纳,年龄就加一岁,当他的年龄增加到一定程度的时候,默认是15,就会晋升到老年代,这个值可以通过虚拟机的参数-XX:MaxTenuringThreshold来进行设置。
4:动态对象年龄判定
为了能更好的适应不同程序的内存情况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小综合大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无语等到MaxTenuringThreshold中要求的年龄。
5:空间分配担保:
这点就给借钱时候的保人类似,如果到时间你换不上,那么保人要先把钱换上。
言归正传,在发生Minor GC的时候,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC,如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC,如果不允许,则也要进行一次Full GC.
由于新生代采用复制收集算法,但是为了保证内存的利用率,只是用其中一块Survivor空间作为轮换备份,因此当出现大量对象在Minor GC后任然存活的情况下,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代,与生活中贷款很类似,老年代要做这样的担保,前提是老年代本身还可以容纳这些对象,但是实际中经过Minor GC之后新生代到底会有多少对象存活下来,这个是不晓得的,所以只好去之前每一次回收晋升到老年代对象大小的一个平均值作为经验值,与老年代剩余的空间大小做比较,决定是否进行Full GC来让老年代腾出更多的空间。