八、运行时内存篇——堆
八、运行时内存篇——堆
1、核心概述
- 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域。
- Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确定了。是 JVM 管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
- 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 堆,是 GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
(1)对象都分配在堆上?
是的。特殊情况“栈上分配”,发生 JVM 性能优化时,将对象分配在栈上,因为栈不会被 GC,并且栈执行效率快。
(2)所有的线程都共享堆?
所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
2、堆的内部结构
堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
Young Generation Space 新生区,又细分为 Eden 区和 Survivor 区(S0 和 S1)
Tenure Generation Space 养老区
Meta Space 元空间
年轻代与老年代
1、 存储在 JVM 中的 Java 对象可以被划分为两类;
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致。
2、 Java 堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen);
3、 其中年轻代又可以划分为 Eden 空间、Survivora 空间和 Survivor1 空间(也叫做 from 区、to 区);
4、 几乎所有的 Java 对象都是在 Eden 区被 new 出来的大对象直接在老年代中创建;
5、 绝大部分的 Java 对象的销毁都在新生代进行了;
3、如何设置堆内存大小
1、 Java 堆区用于存储 Java 对象实例,那么堆的大小在 JVM 启动时就已经设定好了,大家可以通过选项“-Xms”和”-Xmx”来进行设置;
- -Xms 用于表示堆区的起始内存,等价手-XX:InitialHeapsize
- -Xmx 用于表示堆区的最大内存,等价于-XX:MaxHeapsize
2、 一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出 OutOfMemoryError 异常;
3、 通常会将-Xms 和-Xmx 两个参数配置相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小(根据回收效率来判断是否需要重新计算堆区大小),从而提高性能;
- heap 默认最大值计算方式:如果物理内存少于 192M,那么 heap 最大值为物理内存的一半。如果物理内存大于等于 1G,那么 heap 的最大值为物理内存的 1/4
- heap 默认最小值计算方式:最少不得少于 8M,如果物理内存大于等于 1G,heap 默认最小值为物理内存的 1/64,即 1024/64=16M。最小堆内存在 jvm 启动的时候就会被初始化。
4、 堆空间大小在实际开发中一般设置为 2GB 并不是设置的越大越好,可能会影响其他应用,比如 ES;
(1)如何设置新生代与老年代比例?
一般新生代占 1/3,老年代占 2/3。
1、 -XX:NewRatio 配置新生代与老年代在堆结构的占比;
默认-XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
可以修改-XX:NewRatio=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5 2、 可以使用-Xmn 设置新生代最大内存大小,这个参数一般使用默认值就可以了;
(2)如何设置 Eden、幸存者区比例?
1、 在 HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例是 8:1:1;
2、 当然开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例比如-XX:SurvivorRatio=8;
(3)参数设置总结
(4)初始堆大小和最大堆大小一样,问这样有什么好处?
通常会将 -Xms 和-Xmx 两个参数配置相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
(5)什么是空间分配担保策略
空间分配担保策略 -XX:HandlePromotionFailure
在发生 MinorGC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
1、 如果大于,则此次 MinorGC 是安全的;
2、 如果小于,则虚拟机会查看-XX:HandlePromotionFailure 设置值是否允许担保失败;
3、 如果 HandlepromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次 MinorGc,但这次 MinorGC 依然是有风险的;如果小于,则改为进行一次 FullGC;
4、对象分配金句
- 针对幸存者s0,s1 区的总结:复制之后有交换,谁空谁是 to 区
- 关于垃圾回收:
频繁在新生区收集
很少在养老区收集
几乎不在永久区/元空间收集
(1)过程剖析
如果对象在 Eden 出生并经过第一次 MinorGC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 survivor 空间中,并将对象年龄设为 1。对象在 survivor 区中每经过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁,其实每个 JVM、每个 GC 都有所不同)时,就会被晋升到老年代中。
1、 new 的对象先放 Eden 区此区有大小限制;
2、 当 Eden 区的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对 Eden 区进行垃圾回收(MinorGC/YGC),将 Eden 区中的不再被其他对象所引用的对象进行销毁再加载新的对象放到 Eden 区;
3、 然后将 Eden 区中的剩余对象移动到 s0 区;
4、 如果再次触发垃圾回收,Eden 区和 s0 区的存活对象会放到 s1 区;
5、 如果再次经历垃圾回收,Eden 区和 s1 区的存活对象会放到 s0 区如此反复;
6、 啥时候能去养老区呢?可以设置次数默认是 15 次可以设置参数:-XX:MaxTenuringThreshold=15 设置对象普升老年代的年龄阀值;
7、 在养老区,相对悠闲当养老区内存不足时,再次触发 GC:MajorGC,进行养老区的内存清理;
8、 若养老区执行了 MajorGC 之后发现依然无法进行对象的保存,就会产生 OOM 异常,java.lang.OutOfMemoryError:Javaheapspace;
(2)内存分配原则
针对不同年龄段的对象分配原则如下所示:
- 优先分配到 Eden
- 大对象直接分配到老年代。尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断。如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
- 空间分配担保 -XX:HandlePromotionFailure
5、解释 MinorGC、MajorGC、FullGC
JVM 在进行 GC 时,并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:
- 一种是部分收集(Partial GC),不是完整收集整个]ava 堆的垃圾收集。其中又分为:
⑴ 新生代收集(Minor GC / Young GC):只是新生代(Eden\s0,s1)的垃圾收集
⑵ 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。注意,很多时候 MajorGC 会和 Full GC 混滑使用,需要具体分辨是老年代回收还是整堆回收。
⑶ 混合收集(MixedGc):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为 - 一种是整堆(新生代、老年代、方法区)收集(Full GC),收集整个 java 堆和方法区的垃圾收集。
(1)MinorGC 触发机制
- 当 Eden 区空间不足时,就会触发 Minor Gc。 Survivor 满不会触发 GC。
- 因为 Java 对象大多都具备朝生夕的特性,所以 Minor Gc 非常频繁,一般回收速度也比较快。
- Minor GC 会引发 STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
(2)MajorGC 触发机制
指发生在老年代的 GC,对象从老年代消失时,我们说发生了 Major GC 或 Full GC。
出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。
在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发 Major GC
Major GC 的速度一般会比 Minor GC 慢 10 倍以上,STW 的时间更长。
如果 Major GC 后,内存还不足,就报 OOM 了。
(3)FullGC 触发机制
触发 Full GC 执行的情况有如下五种:
1、 老年代空间不足;
2、 方法区空间不足;
3、 通过 MinorGC 后进入老年代的平均大小大于老年代的可用内存;
4、 由 Eden 区、s0(From 区)区向 s1(to 区)复制时,对象大小大于 To 区可用内存,则把该对象转到老年代,且老年代的可用内存小于该对象大小;
5、 调用 system.gc()时,系统建议执行 FullGC,但是不必然执行;
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些。
6、OOM 如何解决
1、 要解决 OOM 异常或 heapspace 的异常,一般的手段是首先通过内存映像分析工具(如 EclipseMemoryAnalyzer)对 dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(MemoryOverflow);
2、 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GCRoots 的引用链于是就能找到泄漏对象是通过怎样的路径与 GCRoots 相关联并导致垃圾收集器无法自动回收它们的掌握了泄漏对象的类型信息,以及 GCRoots 引用链的信息,就可以比较准确地定位出泄漏代码的位置;
3、 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗;
7、堆空间分代思想
为什么需要把 Java 堆分代?不分代就不能正常工作了吗?
不分代也可以工作。分代的唯一理由就是优化 GC 性能:
- 如果没有分代,那所有的对象都在一块。GC 的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当 GC 的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
- 会频繁触发 STW,暂停用户线程。
8、快速分配策略:TLAB
为什么有 TLAB(Thread Local Allocation Buffer)?什么是快速分配策略?
1、 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;
2、 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的;
3、 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度所以,多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略;
默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,当然我们可以通过选项“-XX:TLABwasteTargetPercent”设置 TLAB 空间所占用 Eden 空间的百分比大小。