一、概念
处于运行时数据区,进程唯一的(也就是一个JVM一个)。JVM创建的时候一起被创建,空间大小就确定了,也是JVM最大的一块内存。逻辑上连续、物理上可以不连续。几乎所有的对象和数组都分配在堆上。也是GC主要的区域。
大部分内存都共享,会分出来一部分,每个线程独有->TLAB。
二、设置堆内存大小与OOM
1、堆内存的细分(分代)
jdk7之前:新生区+养老区+永久区(方法区)
- Young Generation Space 新生区 Young/New
- 又分为Eden区和Survivor(0-from、1-to)区
- Tenure Generation Space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
jdk8之后:新生区+养老区+元空间(方法区)
- Young Generation Space 新生区 Young/New
- 又分为Eden区和Survivor(0-from、1-to)区
- Tenure Generation Space 养老区 Old/Tenure
- Meta Space 元空间 Perm
2、设置堆空间大小(年轻代+老年代)
- -Xms:设置堆内存的起始内存,等价于:-XX:InitialHeapSize -X 是jvm运行参数、ms是memory start
- -Xmx:设置堆内存的最大内存,等价于:-XX:MaxHeapSize -X 是jvm运行参数、mx是memory max
默认堆空间的大小:-Xms 物理内存的64分之一、-Xmx 物理内存的4分之一,建议设置一样的值,避免一直扩容。
查看进程的参数使用:jps+jstat -gc 进程ID。也可以使用运行参数 +XX PrintGCDetails
3、OOM举例
三、年轻代与老年带
存储在java声明周期的分为2类:
- 一类是生命周期较短的瞬时对象,创建和消亡都很迅速
- 一类是声明周期非常长,有些极端的情况能够与JVM的声明周期一直
内存的调试
- 默认-XX:NewRaito=2,表示新生代1份、老年代2份,新生代占整个堆的3分之一
- 如果修改成-XX:newRaito=4,表示新生代1份,老年代4份,新生代占整个堆的5分之一
- 默认情况下,Eden空间和另外两个Survivor比例是8:1:1,可以使用-XX:SurvivorRatio调整,但是实际分配过程中,是自适应分配的内存,并不是8:1:1的。
几乎所有的对象都在Eden区分配的,但是如果一个对象很大,可能直接进入到Survivor区。经过研究大部分对象都销毁在新生代了。可以使用-Xmn进行新生代内存调整(使用默认的就行了)。
四、图解对象分配过程
- new 对象在 Eden区
- Eden区满的时候,就需要进行Young/Minor GC(STW),YGC一起回收Eden和Survivor
- YGC之后,会回收一部分垃圾,然后幸存下来的对象,会进入到Survivor0区,对象的年龄计数器+1
- 第二次YG的时候,幸存下来的对象进入到Survivor1区,同时S0对象也会判断,幸存下来的一起进入到S1区,并且年龄+1
- 当一个对象年龄到达一定程度的时候(15阈值),就会进入到老年代
总结:
- 幸存者s0、s1区总结:复制之后有交换,谁空谁是TO
- 关于垃圾回收:频繁在新生区收集,很少在老年区收集,几乎不再永久区\元空间收集
对象分配的特殊情况:
- YGC后,Survivor区还放不下,直接放入到Old区
- Old区放不进去,进行FullGC,如果还放不下就OOM
- S0\S1区放不下,直接放入Old区
常用的调优工具
- JDK命令行:jinfo、jstat、jmap、javap
- Eclipse->Memory Analyzer Tool
- Jconsole
- VisualVm
- Jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
五、Minor GC、Major GC、Full GC(出现的情况要少一些)
- Minor GC(Young GC):
- Major GC:尽量避免(STW时间长)
- Full GC:尽量避免(STW时间长)
Hotpot VM,它里面的GC按照回收区域分为两大类:一种是部分GC,一种是整体堆GC
- 部分收集:不是完成的收集整个JAVA堆中的垃圾
- 新生代收集(Minor GC/Young GC):新生代GC
- 老年代收集(Major GC/Old GC):老年代GC
- 目前只有GMS GC会有单独收集老年代的行为
- Major GC 和Full GC混淆使用,需要分析回收的是老年代还是整体
- 混合收集:收集整个新生代和部分老年代的牢记
- 目前只有G1 GC会有这种行为
- 整体收集(Full GC):针对整个java堆和方法区进行的GC
YGC的触发机制
- 当年轻代中的Eden区空间不足的时候就会触发YGC,注意Survivor区满了不会触发
- 因为80%的对象都Eden区销毁,所以YGC很频繁
- YGC会触发STW,暂停其它用户线程,等垃圾回收结束后,用户线程在恢复工作
Major GC的触发机制(需要避免)
- 指发生在老年代的GC
- 出现了Major GC,经常会伴随至少一次YGC
- 也就是当老年代空间不足的时候,会触发一次YGC,如果还不足则触发Major GC
- Major GC 速度很慢,是YGC的10倍以上,所以STW的时间更长
- 如果Major GC之后,内存还是不足,则出现OOM
Full GC的触发机制(需要避免)
- 调用System.gc()的时候
- 老年代不足
- 方法区不足
- 通过YGC进入老年的平均大小大于老年代的可用内存
- Eden向TO区复制的时候,对象大小大于TO可用内存,在转存到老年代,对象大小大于老年代可用内存
六、堆空间分代思想
为了优化GC的性能,分代之后,就能根据不同的年龄进行回收处理。
七、内存分配策略
针对不同年龄段的对象分配原则如下:
- 有限分配到Eden
- 大对象直接分配到老年代,尽量避免程序成出现大对象
- 长期存储的对象分配到老年代
- 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小总和大于Survivor区的一半,年龄大于或等于该年龄的,直接进入到老年代,无需等到阈值
- 空间分配担保:-XX:HandlePromotionFailure
八、为对象分配内存:TLAB
TLAB(Thread Local Allocation Buffer):
- 堆区是线程共享的区域,任何线程都可以访问到堆中的共享数据
- 由于对象实例的创建在JVM非常频繁,因此在并发环境下堆内存是线程不安全的
- 为了避免多线程操作同一个地址,需要使用加锁等机制,进而影响分配速度
TLAB的定义
- 从内存模型,对Eden区域继续进行划分,JVM为每个线程分配了私有的缓存区域,它包含在Eden空间
- 多线程同时分配内存时,使用TLAB可以避免一些列的非线程安全问题,同时还能提示内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
- 空间仅占1%,使用-XX:UseTLAB参数来控制是否开启TLAB,默认是开启的,使用-XX:TLABWasteTargetPercent进行大小调试
- 分配失败后,JVM就会使用加锁机制,确保数据操作的原子性,直接使用Eden空间的内存
- 内存分配首选是TLAB
九、小结堆空间的参数设置
常用参数:
- -XX:+PrintFlagInitial:查看所有参数的默认初始值
- -XX:+PrintFlagFinal:查看所有参数的最终值
- -Xms:初始堆空间大小(默认物理内存的64分之一)
- -Xmm:最大堆空间大小(默认物理内存4分之一)
- -Xmn:设置新生代的大小(初始值及其最大值)
- -XX:NewRatio:配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间比例
- -XXMaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX:+PrintGC:打印GC简要信息
- -verbose:gc:打印GC简要信息
- -XX:PrintGCDetails:输出GC信息详细日志
- -XX:HandelPromotionFailure:是否设置空间分配担保
查看具体参数:
- jps查看进程
- jinfo - flag 参数名 进程ID
当Eden区内存设置过大,Survivor区就过小,那么就会导致对象很容易就进入到了老年区,YGC就失去了意义。而Eden区设置过小,会导致YGC频率过高,执行STW的次数就过高。
在发生YGC之前,JVM或检查老年代最大的连续可用空间是否大于新生代所有对象的总空间
- 如果大于,则此次YGC是安全的
- 如果小于,则JVM会检查-XX:HandelPromotionFailure是否设置担保失败
- 如果是true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象平均大小
- 如果大于,则尝试一次YGC,但是依然有风险的
- 如果小于,则进行一次Full GC
- 如果是false,则直接进行Full GC
- 如果是true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象平均大小
-XX:HandelPromotionFailure JDK7 已经失效了,就是永远是true
十、堆是分配对象的唯一选择吗
如果经历过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能优化成分配到栈上。分配到栈上,就不需要GC了。
TAOBAOVM,会把把声明周期长的对象,移除堆,GC不去回收。
逃逸分析:
- 如果将堆上的对象分配到栈上,需要使用逃逸分析手段
- 这是一种有效减少java程序中同步负载和内存对分配压力的跨函数全局数据流分析算法
- 如果一个对象,只在方法内部使用,那么没有发生逃逸,就可以分配到栈上
- 从JDK7开始,逃逸分析就已经开始了,使用-XX:+DoEscapeAnalysis参数
代码优化:
- 栈上分配:将堆分配的转换成栈上分配,对象没发生逃逸,就分配在栈上
- 同步省略:一个对象只有一个线程访问,那么就可以考虑不使用同步,逃逸分析也会分析出来,也叫锁消除
- 分离对象或标量替换:如果一个对象不需要联系的内存存储,也可以被访问到,那么可以不存储在堆上,可以存放在寄存器上,如果一个聚合量没有逃逸,那么可以考虑分解成标量,使用-XX:+EliminateAllocations 参数开启,默认是打开的
- 标量:无法在分解更小的数据单元(java中的基本类型)
- 聚合量:就是多个标量或者聚合量组成的
但是由于逃逸分析技术不成熟的原因,并没有广泛使用,所以对象还是分配到堆上。