堆在JVM启动时被创建,大小也随之确定,是JVM最大、最重要的一块内存空间。堆在物理上不连续,而在逻辑上连续。虽然堆是每个线程共享,但也可以划分线程私有的缓冲区
。
几乎所有的对象实例和数组都分配在堆中,少部分分配在栈中。对象和数组可能永远不会存储在栈上,栈帧中仅保存引用,而这引用指向堆中的位置。在栈帧结束后,堆中的对象不会马上被移除,仅在垃圾回收时才被移除。
虚拟机的方法区和堆空间,是每个线程共用的,其他数据区为每个线程私有,私有数据区颗粒度较细,难有优化的空间。而共用的数据区,内存分配时能得到较多的内存,所以该数据区是GC时的重点。
新生代和老年代
在Java7以前,堆空间在逻辑上划分为:新生区+老年区+永久代
;在Java8及之后分为:新生区+老年区+元空间
永久代和元空间,是方法区的具体实现,方法区又叫non-heap,所以永久代和元空间实际上不归属于堆,只是逻辑划分而已。
存储在JVM的Java对象可以被划分成两类,一类是生面周期比较短的瞬时对象,这些类的创建和消亡都非常迅速;另一类对象的生命周期却非常长,在某些极端情况下,还能够与JVM的生命周期保持一直,如Runtime
类
为什么要进行内存分带?大量数据表明,不同对象的生命周期不同,70%~90%的对象时临时对象,对对象进行分代可以更有目的的进行GC,优化GC性能。
新生代
新生代可以划分为Eden
、Survivor0
和Survivor1
,Survivor0和Survivor1有时也叫作from、to。
- Eden:对象(几乎所有)最开始创建的位置。
- Survivor0和Survivor1:Eden区没有被GC清除的对象会被放到S0和S1中,S0和S1大小一致,只有一个空间用来存储对象,另一个区用来全量GC。
绝大部分的Java对象都是在新生代销毁,IBM研究表明,新生代80%的对象都是朝生夕死。
老年代
堆空间除了新生代就是老年代啦。。
对象在堆中的分配过程
1、对象最开始先被分配到Eden区,Eden区满了之后会进行Young GC,存活下来的对象会被移动到Survivor区
。每个对象都有一个年龄计数器
,在Eden区为0,被移动到S区则变为1。S区满不会触发YGC
。
2、存活在S区的对象,在经过第二次GC之后能存活下来的,年龄计数器会继续+1。当年龄计数器达到15后,对应的对象会被移动(promotion)至老年区
。该值可以通过-XX:MaxTenuringThreshold=15进行设置。
3、当对象占用的空间太大,YGC后任然超过了Eden区的空间时,会直接将该对象分配到老年代。老年代放不下则进行Full GC,GC后任然放不下则OOM。存活在Eden区的对象在YGC后,S区放不下,则会直接分配到老年代。
与堆内存设置相关的参数
-Xms
,表示堆的起始内存(新生代和老年代)
全称为-XX:InitialHeapSize
X是JVM的运行参数,ms是memory start。默认单位为字节,有k、m、g
-Xmx
,表示堆的最大内存
全称为-XX:MaxHeapSize
默认的堆空间大小:
-Xms = 计算机内存大小 /64
-Xmx = 计算机内存大小 /4
一旦超过了-Xmx所制定的最大内存时,将会抛出OutOfMemoryError
。通常会将两个参数设置成相同的值,让垃圾回收完后,不需要重新分隔计算堆区的大小,进而提高性能。
可以通过以下程序查看JVM的堆大小:
// -Xms 堆的起始值,s0,s1区只算其中的一个
long initalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024
// -Xmx 堆的最大值
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024
关闭自适应的内存分配策略
-XX:-UseAdaptiveSizePolicy:-为关闭,+为开启,默认开启
配置新生代和老年代在对结构的占比
- 默认
-XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆的1/3;可以修改为-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5 - 在HotSpot中,Eden和S0、S1的默认比例为8:1:1,可以通过
-XX:SurvivorRatio
=8调整比例
配置新生代的最大内存大小
-Xmn
,一般默认即可,有设置就以这个参数为准。
与堆相关的GC
JVM在GC时,并不是整体进行垃圾收集,而是频繁收集新生代,较少收集老年代,几乎不在永久代/元空间收集。
-
Minor GC,只对新生代进行垃圾收集,当Eden区内存不足时会触发,Survivor区满不会GC。
-
Major GC ,只对老年代进行垃圾收集
目前只有CMS GC会有单独收集老年代的行为
很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
Major GC经常会伴随Minor GC,也就是在老年代空间不足时,会尝试先触发Minor GC,之后空间还不足,则触发Major GC。Major GC的速度比Minor GC慢十倍以上,STW时间也更长。
尽量避免
-
Mixed GC,收集新生代和部分老年代的垃圾,只出现于G1 GC
-
Full GC,收集整个Java堆和方法区的垃圾
引起Full GC的几种情况: 调用System.gc()时,系统建议执行Full GC,但是不必然执行;老年代空间不足;方法区空间不足;Minor GC后进入老年代的平均大小大于老年代的可用内存;老年代放不下从Eden区过来的大对象;
尽量避免
堆内存分配策略
- 优先分配到Eden区
- 大对象直接分配到老年代,避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断,
如果Survivor区中相同年龄的所有对象大小总和大于S区空间的一半,年龄大于等于的对象可以直接进入老年代,无需等到MaxTenuringThreshold要求的年龄
- 空间分配担保,-XX:HandlePromotionFailure
堆内存的线程私有空间:TLAB
TLAB:ThreadLocal Allocation Buffer
堆区时线程共享的区域,任何线程都可以访问到堆区中共享数据。由于对象实例创建在JVM中非常频繁,因此在并发环境下,从堆区划分内存空间是线程不安全的。为了避免多个线程操作同一地址,需要加锁等机制,加锁影响分配速度。因此有必要为每个线程单独分配一块区域存放数据,该区域存在于Eden区中,这种策略也叫快速分配策略
。
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实将TLAB作为内存分配的首选
。可以通过-XX:UseTLAB
设置是否开启,默认开启。默认情况下,TLAB空间非常小,只占Eden区的1%,可以通过-XX:TLABWasteTargetPercent
进行设置占用的百分比。
一旦对象在TLAB分配失败时,JVM会尝试加锁确保数据操作的原子性,从而直接在Eden区分配。