6. 堆
1. 概述
➢ 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
➢ Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
➢ 堆内存的大小是可以调节的。
➢《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
➢ 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(ThreadLocal Allocation Buffer, TLAB)。
➢ 默认堆空间大小:
初始内存大小: 物理电脑内存大小/64
最大内存大小: 物理电脑内存大小/4
2. 年轻代和老年代
存储在JVM中的Java对象可以被划分为两类:
➢ 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
➢ 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步细分的话, 可以划分为年轻代(YoungGen) 和老年代(OldGen)
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间 (有时也叫做from区、to区)
在这里需要注意,系统虽然默认是8:1:1,但是 JVM 默认情况下还有一个自适应比例, 我们要使用-XX:-UserAdaptiveSizePolicy
关闭这个自适应规则后,才会使 JVM 默认的8:1:1.(第二个-
代表关闭;+
代表使用)
3. 内存分配
为新对象分配内存是-一件 非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后,是否会在内存空间中停生内存碎片。
- new的对象先放 Eden(伊甸园区) 。此区有大小限制。
- 当 Eden(伊甸园区) 的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对 Eden(伊甸园区) 进行垃圾回收(
Minor GC 回收机制
), 将 Eden(伊甸园区) 中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到 Eden(伊甸园区)
3.然后将 Eden(伊甸园区) 中的剩余对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6.啥时候能去老年代呢?可以设置次数。默认是15次。 当Survior区中的对象的 age 计数器
已经达到了15时,此时该对象就会 Promotion 晋升
到 Old generation 老年代
。
7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC
,进行养老区的内存清理。
8.若养老区执行了Major GC
之后发现依然无法进行对象的保存,就会产生0OM异常
●可以设置参数: -XX: MaxTenuringThreshold=进行设置。
总结:当出现 Eden(伊甸园区)满的时候,就会触发 Minor GC 回收机制(或者说YGC),会将 Eden(伊甸园区) 和 Survior区(幸存者区)一起回收。若 Survior区(幸存者区)满了之后,它不会自动触发Minor GC 回收机制,它只能由Eden(伊甸园区)满时被动触发.
4. 对象分配过程:TLAB(Thread Local Allocation Buffer),即线程本地分配缓存区
为什么要有TLAB?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,那么在并发情况下,为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
- 从内存模型而不是垃圾收集的角度,我们对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。在多线程情况下进行分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。例如阿里的jvm
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,因为默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,所以可能出现内存不足,对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。但JVM确实是将TLAB作为内存分配的首选。
- 在程序中, 开发人员可以通过选项
“-XX:UseTLAB"
设置是否开启TLAB空间。当然我们可以通过选项“-XX: TLABWasteTargetPercent"
设置TLAB空间所占用Eden空间的百分比大小。
5. 堆空间常用参数
/**
* 测试堆空间常用的jvm参数:
* -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
* -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
* 具体查看某个参数的指令: jps:查看当前运行中的进程
* jinfo -flag SurvivorRatio 进程id
*
* -Xms:初始堆空间内存 (默认为物理内存的1/64)
* -Xmx:最大堆空间内存(默认为物理内存的1/4)
* -Xmn:设置新生代的大小。(初始值及最大值)
* -XX:NewRatio:配置新生代与老年代在堆结构的占比
* -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
* -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
* -XX:+PrintGCDetails:输出详细的GC处理日志
* 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
* -XX:HandlePromotionFailure:是否设置空间分配担保
*
* @author mxc
* @create 2020 17:18
*/
6.Minor GC、 Major GC、 Full GC的区别
JVM在进行GC时,并非每次都对上面**三个内存(新生代、老年代;方法区)**区域一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
-
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
-
➢新生代收集(Minor GC / Young GC) :只是新生代(Eden,S0,S1)的垃圾收集
-
➢老年代收集 (Major GC / Old GC) :只是老年代的垃圾收集。
-
目前,只有CMS GC会有单独收集老年代的行为。
-
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
-
-
➢混合收集 (Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为
-
-
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
7. 老年代回收机制
在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
-
如果大于,则此次Minor GC是安全的
-
如果小于,则虚拟机会查看
-XX: HandlePromotionFailure
设置值是否允许担保失败。- ➢如果HandlePromotionFailure=true, 那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
- 如果大于,则尝试进行一”次Minor GC,但这次Minor GC依然是有风险的;
- 如果小于,则改为进行一次Full GC。
- ➢如果HandlePromotionFailure=false, 则改为进行一次Full GC。
在JDK6 Update24之后(或者说JDK7),HandlePromotionFailure参 数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。
JDK6 Update24(或者说JDK7)之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。