一般自动内存管理都需要解决的以下三个问题:
1、为新对象分配空间。
2、确定存活对象。
3、回收死亡对象所占用的空间。
其中后两个问题在之前的博文中都已经介绍过了,不了解的可以参考垃圾回收算法分析与JVM具体应用 、JVM中如何判断一个对象是否为可回收。
现在还有最后一个为新对象分配空间的问题,一起来看一下在JVM中是如何处理的吧!
首先我们依然基于分代划分的思想,将堆空间分为新生代、老年代,其中新生代一般又被分为一个eden区和两个Survivor区。
1、对象优先在Eden区分配
大多数情况下,对象肯定是优先分配在Eden区的,如果Eden区空间不足,就会触发一次新生代的回收(也可以叫做:Minor GC或YGC)。
TLAB
TLAB:本地线程分配缓冲,内存分配实际上被按照不同的线程划分在不同的内存之间进行,每个线程在Eden区中中有一块独享的小区域,这样做的好处是可以减少同步处理带来的性能消耗。
可以使用-XX:TLABSize设置大小。
2、大对象直接进入老年代
大对象一般指的是那种需要占用连续的内存空间的对象,比如很大的一个数组对象。
为什么大对象不优先在Eden区分配?
首先我们知道Eden区的对象都是默认被我们假设为“朝生夕死”的对象,在Eden区中的对象默认需要经历15次垃圾回收(动态年龄)才会被放入老年代,所以假设这个大对象不是一个短命鬼,那么我们就需要在内存中来回复制15次,这必然会降低垃圾回收的效率,所以干脆直接放入老年代,以避免大对象的频繁复制过程。
写代码时应该注意避免大对象的频繁产生
了解这个分配原则后,我们平时在写代码就应当尽量避免不必要的大对象产生,尤其是那种“朝生夕死”的大对象,因为这样的对象就会频繁的进入老年代,并且如果老年代的连续内存空间不足,就会频繁的触发FullGC,因为要为大对象整理出连续的内存空间。
同时大对象必然需要消耗更多的内存复制的开销。
使用-XX:PretenureSizeThreshold这个参数可以设置大对象的阈值,不过要注意这个参数只对Serial和ParNew两款新生代收集器有效。
分配演示
public class Test {
public static void main(String[] args) throws InterruptedException {
byte[] bytes = new byte[1024*1024*1];//分配1M内存
Thread.sleep(Integer.MAX_VALUE);//让程序休眠,观察内存情况
}
}
设置JVM参数,JDK1.8环境
-Xms20m(堆的初始大小)
-Xmx20m(堆的最大大小)
-XX:NewSize=10m(新生代的初始大小)
-XX:MaxNewSize=10m(新生代的最大大小)
我们可以通过jmap命令查看heap的分配情况
Eden区一共使用了5M,4M大约来自JDK本身启动时所需加载的对象所占用的内存空间。
设置-XX:PretenureSizeThreshold=1024(单位为byte),垃圾收集器为Serial,再看一下效果。
这时候1M的byte数组就被直接分配到了老年代中了。
3、长期存活的对象进入老年代
对象首先被分配到Eden区,当发生MinorGC后,如果对象仍然存活,那么就会被移动到Survivor区,此时对象的年龄就会+1岁,当到达指定年龄后对象仍然存活,这样的对象就属于长期存活的对象,那么就会被放入老年代中,这样做的好处当然是为了减少对象在新生代中来回复制带来的性能消耗。
使用-XX:MaxTenuringThreshold参数可以配置年龄的大小,其中parallel默认为15,CMS默认为6。
示例演示
public class Test {
public static void main(String[] args) {
byte[] b1 = new byte[1024 * 256];
byte[] b2 = new byte[1024 * 1024 * 1];
byte[] b3 = new byte[1024 * 1024 * 2];
byte[] b4 = new byte[1024 * 1024 * 2];
}
}
当使用默认年龄时,发生MinorGC后,有一部分对象进入Survivor区。
[GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 2 (max 2)
[PSYoungGen: 6350K->1016K(9216K)] 6350K->4476K(19456K), 0.0015667 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 3230K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff8299b8,0x00000000ffe00000)
from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffefe020,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 3460K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 33% used [0x00000000fec00000,0x00000000fef61010,0x00000000ff600000)
Metaspace used 3193K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
当设置-XX:MaxTenuringThreshold=0后,发现Survivor区没有存活对象了。
[GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 0 (max 0)
[PSYoungGen: 6350K->0K(9216K)] 6350K->4417K(19456K), 0.0017623 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 2214K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff829960,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4417K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 43% used [0x00000000fec00000,0x00000000ff050420,0x00000000ff600000)
Metaspace used 3181K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
4、对象动态年龄判断
在HotSpot虚拟机设计中,并不是完全要等对象年龄到达-XX:MaxTenuringThreshold设置的值以后才会被放入老年代,也有一种例如的情况,当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
5、栈上分配
几乎所有的对象都是分配在堆内存中,但是还有一种比较特殊的分配方式是分配在栈上,这是借助于逃逸分析来辅助实现的,逃逸分析中指出如果对象的作用域不会逃出方法或者线程之外,也就是无法通过其他途径访问到这个对象,那么就可以对这个对象采取一定程度的优化,这其中就包含了:栈上分配。
栈上分配的好处在于,对象可以随着栈的出栈过程被自然的销毁,节省了堆中垃圾回收所消耗的性能。
对象分配大致的流程图