一、堆的内存结构和管理
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术使得并非所有对象实例都在堆上分配)。 如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
1、TLAB分配
JVM在内存新生代Eden Space中开辟了一小块区域,由线程私有,称作TLAB(Thread-local allocation buffer),默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
补充:为什么不在堆上分配
我们知道堆是由所有线程共享的,既然如此那它就是竞争资源,对于竞争资源,必须采取必要的同步,所以当使用new关键字在堆上分配对象时,是需要锁的。既然有锁,就必定存在锁带来的开销,而且由于是对整个堆加锁,相对而言锁的粒度还是比较大的,当对象频繁分配时,不免影响效率。
所以对于某些特殊情况,可以采取避免在堆上分配对象的办法,以提高对象创建和销毁的效率。
2、逃逸分析与栈上分配
JVM在Server模式下的逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。Java在Java SE 6u23以及以后的版本中支持并默认开启了逃逸分析的选项。
细节可以参考文章:http://blog.csdn.net/yangzl2008/article/details/43202969
3、Java对象分配的过程
编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.
如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.
重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5.
执行一次Young GC(minor collection)。
经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。
4、新生代和旧生代
如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。结构图如下所示:
堆的总大小可以通过-Xmx和-Xms来控制。
新生代 Young Generation:新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例;
S0 Suvivor (From) Space :存在时间较长,经过垃圾回收没有被清除的实例,就从Eden 搬到了S0;
S1 Survivor (To) Space:同理,存在时间更长的实例,就从S0 搬到了S1;
旧生代 Old Generation/Tenured:存在时间更长的实例,对象多次回收没被清除,就从S1 搬到了tenured。
参见:
【Java 内存区域和GC机制】http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html
【JVM结构、GC工作机制详解】http://blog.csdn.net/tonytfjing/article/details/44278233
【java虚拟机的垃圾回收机制】http://blog.csdn.net/csh624366188/article/details/8042649
5、内存模型
参见:http://www.cnblogs.com/smyhvae/p/4748392.htm