JVM - 堆

# JVM - 堆

JDK版本:1.8

# 1、堆的核心概述

堆内存针对于JVM进程是唯一的,也就是一个进程只有一个JVM,一个进程下会存在多个线程,这些线程共享同一个堆空间,其中还可以被划分为线程私有的缓冲区(Thread Local Allocation Buffer TLAB)。堆空间是Java内存管理的核心区域。

Java堆区在JVM启动时被创建,其空间大小同时也会被确定。堆空间是JVM管理的最大一块内存空间,同时堆内存空间也是可配置的。

Java虚拟机规范中规定,对可以处于物理上连续不断的内存空间中,但是逻辑上它应该被视为连续的。所有的对象实例以及数组都应当在运行时分配在堆空间上。The heap is the run-time data area from which memory for all class instances and arrays is allocated。数组和对象可能永远都不会存储在栈上,在栈帧中保存的是地址引用,这个地址引用指向对象或者数组在堆空间中的位置。在一个方法执行结束后,堆中的对象不会马上被移除,只有在进行垃圾回收的时候才会被移除。堆是GC(Garbage Collection)执行垃圾回收的重点区域。

JVM - 堆_第1张图片

# 1、JDK7堆空间内部结构

Java 7及之前堆的内存逻辑上分为三个部分:

  • 新生区(Young Generation Space):其中新生区又被细分为Eden区和Survivor区。
  • 养老区(Tenure generation space):老年代。
  • 永久区(Permanent Space)

JVM - 堆_第2张图片

# 3、JDK8堆空间内部结构

Java 8及以后堆的内存逻辑上分为三个部分:

  • 新生区(Young Generation Space):其中新生区又被细分为Eden区和Survivor区。
  • 养老区(Tenure generation space):老年代。
  • 元空间(Meta Space)

JVM - 堆_第3张图片


# 2、设置堆内存大小与OOM

# 1、堆空间大小设置

JVM堆区用于存储Java对象实例,在JVM启动时堆空间大小就会被确定下来,可以通过-Xmx-Xms选项进行配置堆空间大小。

  • -Xms:表示堆区的起始内存,等价于-XX:InitialHeapSize
  • -Xmx:表示堆区的最大内存,等价于-XX:MaxHeapSize

一旦堆区的内存大小超过-Xmx所指定的最大内存时,将会抛出OutOfMemoryError错误。通常情况下会将-Xms-Xmx两个选项配置成相同的参数值,这样做能够在JVM进行垃圾回收清理完毕堆区后不需要重新分隔计算堆区的大小从而达到提升性能的目的。

在默认情况下:

  • -Xms:物理内存大小 / 64。
  • -Xmx:物理内存大小 / 4。

# 3、年轻代与老年代

JVM堆中存储的Java对象可以被划分为两类:

  • 生命周期较短的瞬时对象,这些对象的创建和消亡都非常迅速。
  • 生命周期非常长的对象,在某些极端的情况下能够与JVM的生命周期保持一致。

JVM堆空间被细分为年轻代(Young Gen)和老年代(Old Gen)。其中年轻代又被划分为Eden区、Survivor 0区和Surivior 1区,其中Survivor 0区和Surivior 1区由于会进行交换,在宏观上是一种相对而言的区域,所以又被称为from区和to区。

JVM - 堆_第4张图片

# 1、配置新生代与老年代空间占比

新生代与老年代的空间占比在实际开发中一般不会进行调整。

首先看一下新生代中的Eden区和Survivor 0区和Survivor 1区之间的空间占比。

Hotspot虚拟机中,Eden区和Survivor 0区和Survivor 1区之间的空间占比为:8:1:1。可以通过选项-xx:SurvivorRatio选项调整新生代中的空间占比,如:-xx:SurvivorRatio=8JVM中几乎所有的Java对象都是在Eden区被new出来的。新生代最大内存可通过-Xmn选项进行配置,一般这个参数使用默认值即可。

新生代与老年代在堆中的空间占比。

Hotspot虚拟机中,新生代与老年代的空间占比为1:2,这个比例可以通过-XX:NewRatio选项进行配置。默认-XX:NewRatio=2代表新生代的空间占比为1,老年代空间占比为2,新生代占整个堆空间的1 / 3。可以修改配置如:-XX:NewRatio=4代表新生代空间占比为1,老年代占比为4,新生代占整个堆空间的1 / 5。


# 4、对象分配过程

为新对象分配内存是一件非常严谨和复杂的任务,不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

JVM - 堆_第5张图片

  • 1、Javanew关键字创建的新对象先放在Eden区。此区域有大小限制。
  • 2、当Eden区的空间被填满时,应用程序又需要创建新的Java对象,JVM的垃圾回收器将堆Eden区进行垃圾回收(Minor GC),将Eden区中不再被其它对象所引用的对象进行销毁,销毁后再将新创建的Java对象放到Eden区。
  • 3、然后将Eden区中剩余对象移动至Survivor 0区。
  • 4、如果再次触发垃圾回收,此时上次幸存下来存放在Survivor 0区中的对象,如果没有进行垃圾回收,则会将其复制到Survivor 1区。
  • 5、如果再次触发垃圾回收,此时会将其重新复制到Survivor 0区,重复步骤4、5。反复经过15次之后,还存在的对象将会移动到老年区。重复步骤4、5的次数可以通过选型-Xx:MaxTenuringThreshold=N进行配置,默认是15次。
  • 6、在老年代,如果老年代中内存不足时,将触发垃圾回收(Major GC)进行养老区内存清理。若养老区经过Major GC之后,依然无法进行对象的保存,就会抛出OutOfMemory异常。

针对Survivor 0区和Survivor 1区之间的复制交换,通常都是将复制之后为空的一个移动到Survivor 1区,即Survivor 0区总是会存放对象的,这也是为什么说Survivor 0Survivor 1区是相对而言的,谁空谁是to区。

针对垃圾回收,频繁在新生代进行垃圾收集,很少在老年代进行。


# 5、Minor GCMajor GCFull GC

JVM的针对堆空间的垃圾回收大部分都是回收的新生代,针对Hotspot VM的实现,GC按照回收区范围又分为两大中类型

  • 部分收集(Partial GC)
    • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
      • 目前只有CMSGC会有单独收集老年代的行为。
      • 很多时候Major GC会和Full GC混淆使用,需要具体辨别是老年代垃圾回收还是整堆回收。
    • 混合收集(Mixed GC):对整合新生代以及部分老年代进行垃圾收集。
      • 只有G1 GC会有这种行为。
  • 整堆收集(Full GC):针对整个Java堆和方法区的垃圾收集。

# 6、分代式GC触发机制

# 1、年轻代GC(Minor GC)触发机制

当年轻代空间不足时,就会触发Minor GC,这里指的年轻代是Eden区,Survivor区空间不足不会引发GC。每次Minor GC都会清理年轻代内存。因为Java对象大多都具备朝生夕灭的特性,所以在JVMMinor GC是非常频繁的,垃圾回收速度也比较快。Minor GC会引发STW,当JVM堆堆中的新生代进行Minor GC时会暂停其它用户线程,等待垃圾回收结束用户线程才会被恢复。

# 2、老年代GC(Major GC / Full GC)触发机制

Major GCFull GC指发生在老年代的GC,当对象从老年代消失时,就发生了Major GCFull GC

Major GC经常会伴随至少一次的Minor GC,但是这种情况并非绝对。在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程。也就是说当老年代空间不足时,通常情况下会先进行一次Minor GC,如果进行Minor GC之后老年代空间还是不足则会触发Major GC对老年代进行垃圾回收。

Major GC的速度一般会比Minor GC慢10倍以上,Major GC引发的STW时间则会比Minor GCSTW时间更长。如果在Major GC之后堆空间内存还是不足则会抛出OutOfMemory异常。

# 3、整堆回收Full GC触发机制

触发整堆垃圾回收的场景有五种:

  • 显示调用System.gc()时,系统将会建议JVM执行Full GC,但是不一定会执行。
  • 老年代空间不足。
  • 方法区空间不足。
  • 通过Minor GC后进入老年代的对象平均内存大小大于老年代当前可用的内存。
  • Eden区、Survivor Space 0(From Space)区向Survivor Space 1(To Space)区复制时,对象大小大于To Space区可用内存则把该对象转存到老年代中,且当前老年代可用内存大小小于该对象大小。

注意:Full GC是开发或者调优中要尽最大努力避免的垃圾回收机制,Full GC非常慢。


# 7、堆空间分代思想

Java中不同对象的生命周期是不同的,其中70% ~ 99%的都是临时对象。Java堆空间分配为新生代和老年代,其中新生代分为伊甸园区(Eden)、幸存者0区(Survivor 0 / from)、幸存者1区(Survivor 1 / to),其中幸存者区空间大小相同,其中to区总为空。而老年代中存放新生代中经过多次GC后依然存活的Java对象。

JVM对堆空间进行分代的意义就是为了优化GC的性能,可以理解为分治思想。如果没有分代,所有Java对象都在一起,触发GC之后需要寻找出没有使用的Java对象,这样的话会进行全堆扫描,而Java对象又有很大一部分是临时对象,所以将特定的对象放在一块特定的区域进行专治,针对不同代的Java对象使用不同的垃圾回收机制进行垃圾回收处理,将会对JVM触发GC时的性能有很大的优化帮助。同时将堆空间分代之后,也便于对象堆内存的分配。

JVM - 堆_第6张图片


# 8、堆内存分配策略

Java中新创建的对象,JVM会在Eden区中创建该对象,经历过一次Minor GC之后仍然存活并且能够被Survivor 0区容纳,该对象将会被移动到Survivor 0区中并将该对象的年龄设置为1。该对象每在Survivor区中熬过一次Minor GC其年龄就会增加1岁,当该对象的年龄到达一定的程度之后(Hotspot默认为15岁,不同的JVM会有不同的默认值)该对象就会被移动到老年代。

针对不同年龄段的Java对象JVM的堆内存分配策略如下:

  • 新创建的对象优先分配到Eden区。
  • 大对象直接分配到老年区(开发者应尽量避免程序中出现过多的大对象)。
  • 长期存活的对象分配到老年代。
  • 动态对象年龄判断:如果在Survivor区中相同年龄的所有对象大小之和大于Survivor区空间的一半,年龄大于等于该年龄的对象可直接分配到老年代,无需等待这些对象的年龄到达MaxTenuringThreshold选项中设置的阈值。
  • Java对象堆空间分配担保:-XX:HandlePromotionFailure

# 9、线程私有分配缓冲区 - TLAB

# 1、Thread Local Allocation Buffer

JVM内存模型的角度出发,对堆内存中的Eden区继续进行划分,JVM为每个线程都分配了一个私有缓存区域,它包含在Eden空间内。在多线程同时在堆空间中为Java对象分配堆内存时,使用TLAB来避免一系列的非线程安全问题,同时还能够提升堆内存分配的吞吐量,这种内存分配方式称之为快速分配策略

Open JDK衍生的JVM都提供了TLAB的设计:

JVM - 堆_第7张图片

在应用程序中,开发者可通过-XX:UseTLAB选项设置是否开启TLAB空间。默认情况下TLABEden空间的内存非常小,仅仅占整合Eden区空间的1%。开发者可以通过-XX:TLABWasteTargetPercent选项配置TLAB空间所占用Eden空间的百分比大小。

设置开启TLAB之后,在创建新的Java对象时,首先会尝试TLAB中进行堆内存分配,JVM也确实将TLAB作为分配堆内存的首先。一旦该对象在TLAB空间中分配堆内存失败,JVM会立即尝试通过使用加锁机制确保操作数据的原子性进而直接在Eden空间中为该对象分配堆内存。


# 10、JVM堆空间参数选项

部分参数说明:

参数选项 说明
-XX:+PrintFlagsInitial 查看JVM所有的参数选项的默认初始值
-XX:+PrintFlagsFinal 查看JVM所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms 堆空间最小内存(默认为物理内存的1 / 64
-Xmx 堆空间最大内存(默认为物理内存的1 / 4
-XX:NewRatio 新生代与老年代在堆结构的占比(默认为1:2
-XX:SurvivorRatio 新生代中EdenS0 / S1空间的比例(默认为8:1:1
-XX:MaxTenuringThreshold 新生代垃圾对象的最大年龄
-XX:+PrintGCDetails 输出详细的GC处理日志
-Xx:+PrintGC / -verbose:gc 打印GC简要信息
-XX:HandlePromotionFalilure 是否设置空间分配担保

触发Minor GC时,在正真执行Minor GC之前,JVM虚拟机会检查老年代中最大可用的连续空间是否大于新生代所有对象的总空间。

  • 如果此时老年代中最大可用空间大于新生代中所有对象的总空间,则此次Minor GC是安全的。
  • 如果小于新生代中所有对象总空间,JVM虚拟机首先会查看-XX:HandlePromotionFalilure选项是否配置允许空间分配失败担保。
    • 如果-XX:HandlePromotionFalilure=trueJVM会继续检查老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小。
      • 如果大于,JVM会尝试进行一次Minor GC,但这次Minor GC仍然是有风险的。
      • 如果小于,JVM会直接进行一次Full GC
    • 如果-XX:HandlePromotionFalilure=trueJVM会直接进行一次Full GC

JDK6 Update 24之后,-XX:HandlePromotionFalilure选项不会再影响虚拟机的空间分配担保策略,虽然Open JDK中仍然还是定义了-XX:HandlePromotionFalilure选项,但是在代码层面已经完全没有使用这个参数选项了。JDK6 Update 24之后的规则变更为:只要老年代的最大连续可用空间大于新生代对象总大小或者历次晋升到老年代的对象的平均大小JVM就会进行Minor GC,否则将进行Full GC

Oracle JDK Document Addreshttps://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html


# 11、堆空间是分配对象的唯一选择吗?

《深入理解Java虚拟机》3版第45页抬头的段落尾写道:

​ 随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大、栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上页渐渐变得不是那么绝对了。

有一种特殊情况,如果经过逃逸分析(Escape Analysis)后发现一个对象并没有逃逸出方法的话,那么该对象就可能被优化成栈上分配,这样就无需在堆上进行内存分配,JVM也就无需针对此类临时对象进行垃圾回收,这也是最常见的堆外存储技术。

基于Open JDK深度定制的TaboBaoVM,其中创新的GCIH(Garbage Collection Invisible Heap)技术实现off-heap,将生命周期较长的Java对象从Heap中移动至Heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

你可能感兴趣的:(Java,JVM,java,开发语言,后端)