JVM堆区

    • 堆(Heap)
      • 堆区的组成:新生代+老年代
      • 堆空间的大小设置
      • 创建对象的内存分配
      • 堆区的分代垃圾收集思想
      • 堆区产生的错误

堆(Heap)

Heap堆区,用于存放对象实例和数组的内存区域

​ Heap堆区,是JVM所管理的内存中最大的一块区域,被所有线程共享的一块内存区域。堆区中存放内存实例,“几乎”所有的对象实例以及数组都在这里分配内存。
​ 每一个JVM进程值存在一个堆区,它在JVM启动时被创建,JVM规范中规定堆区可以是物理上不连续的内存,但必须是逻辑上连续的内存。

  1. 堆区是线程共享的区域,同时也是JVM管理最大的内存区域。
  2. JVM规范中描述,所有的对象实例及数组都应该在运行时分配在堆上。而他们的引用会被保存在虚拟机栈中,当方法结束时,这些实例不会被立即清除,而是等待GC垃圾回收。
  3. 由于堆占用内存大,所以是GC垃圾回收的重点区域,因此堆区也被称作GC堆。

对象逃逸分析
​ Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
​ 从JDK 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

堆区的组成:新生代+老年代

​ 从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以JVM中的堆区旺旺进行分代划分,例如:新生代老年代。目的是更好地回收内存,或者更快地分配内存。

​ 堆区地组成分为新生代(Young Generation)、老年代(Old Generation)。新生代被分为伊甸区(Eden)和幸存区(from+to),幸存区又被分为Survivor 0(from)和Survior 1(to)。

​ 新生代和老年代的比例为1:2,伊甸区和s0、s1比例为8:1:1,不同区域存放对象的用途和方式不同:

  1. 伊甸区(Eden):存放大部分新创建对象。
  2. 幸存区(Survivor):存放Minor GC 之后,Eden区和幸存区(Servivor)本身没有被回收的对象。
  3. 老年代:存放Minor GC之后且年龄计算器达到15依然存活的对象、Major GC 和Full GC 之后仍然存活的对象。

堆空间的大小设置

堆空间的内存大小是可以修改的,默认情况下,初始堆内存为物理内存的1/64,最大为物理内存的1/4。

  • -Xms :设置初始堆内存,例如:-Xms64m
  • -Xmx :设置最大堆内存,例如:-Xmx64m
  • -Xmn :设置年轻代内存,例如: -Xmx32m

Heap堆区中的新生代、老年代的空间分配比例,通过java -xx:+PrintFlagsFinal -version命令查看:

上述输出结果分析:

InitialSurvivorRatio = 8

新生代Young(Eden/Survivor)空间的初始比例 = 8:代表Eden占新生代空间的80%

uintx NewRatio = 2

老年代Old/新生代Young的空间比例 = 2:代表老年代Old是新生代Young的2倍

因为新生代是由Eden + s0 + s1组成的,所以按照上述默认比例,如果Eden区内存大小是40M,那么两个Survivor区就是5M,整个新生代区就是50M,然后可以算出老年代Old区内存大小是100M,堆区总大小就是150M。

创建对象的内存分配

​ 创建一个新对象,在堆中的内存分配。

​ 大部分情况下,对象会在Eden区生成,当Eden区装填满的时候,会触发Young Garbage Collection,即YGC垃圾回收的时候,在Eden区实现清楚策略,没有被引用的对象则直接收回。

​ 依然存活的对象会被送到Survivor区。Survivor区分为s0和s1两块内存区域。每次YGC的时候,他们将存活的对象复制到未使用的Survivor空间(s0或s1),然后将当前正在使用的空间完全清除,交换两块空间的使用状态。每次交换时,对象的年龄会+1。

​ 如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象不可能永远呆在新生代,在JVM中一个对象从新生代晋升到老年代的阈值默认值为15,可以在Survivor区交换14次之后,晋升到老年代。

堆区的分代垃圾收集思想

​ 处于效率的缘故,JVM的垃圾收集器不会对三个区域(伊甸区、幸存区、老年代)进行收集,大部分时候都是回收新生代,HotSpot虚拟机将垃圾收集分为部分收集(Partial GC)和整堆收集(Full GC)。

部分收集

  1. 新生代收集YGC(Minor GC/Young GC):回收新生代区域,频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收,性能耗费比较少。例如:Seriall、ParNew 、ParallelScavenge等垃圾收集器都是新生代收集;
  2. 老年代收集器Old GC(Major GC/Old GC):回收老年代区域,例如Serial Old、CMS、Parallel Old等垃圾收集器都是老年代收集;
  3. 混合收集(Mixed GC):收集整个年轻代区域及部分老年代区域,目前只有G1收集器有。

整堆收集FGC(Full GC):回收整个Java堆区,默认堆空间使用到达80%(可调整)的时候会触发FGC。

​ GC组合垃圾回收只有YGC和Full GC,Old GC不可以单执行。原因是OldGC是STW机制+标记整理算法,相对耗时只能在关键时刻使用,因此只有Full GC 才能触发Old GC。

GC垃圾回收的影响

GC耗时太长、GC次数太多会影响进程的性能,导致进程响应变慢,或者无法响应。

YGC耗时:耗时在几十或着几百毫秒属于征程情况,用户几乎无感知,对程序影响比较少。耗时太长或者频繁,会导致服务器超时问题。

YGC次数:太频繁,会降低服务的整体性能。高并发服务时,影响会比较大。

FGC次数:越少越好。比较正常情况几个小时一次、或者几天一次。

FGC耗时:耗时很长会导致线程频繁被停止,使应用响应变慢,比较卡顿。

产生FGC的原因:

  1. 大对象:系统一次加载了过多数据到内存中,导致大对象进入老年代。
  2. 内存泄露:频繁创建了大量对象,但是无法被回收(比如IO流对象使用后未调用close方法释放资源),先引发FGC,最后导致OOM。
  3. 程序频繁生成一些长生命周期的对象,当这些对象呢的存活年龄超过分代奈年龄时便会进入老年代,最后引发FGC。
  4. 程序BUG导致动态生成了很多新类,使得Metaspace不断被占用,先引发FGC,最后导致OOM。
  5. JVM参数设置不合理:包括内存大小、新生代和老年代的大小、Eden区和Survivor区的大小、元空间大小、垃圾回收算法等等。

堆区产生的错误

堆区最容易处出现的就是OutOfMemoeyError错误,这种错误的表现形式会有以下两种:

  1. OutOfMemoeyError:GC Overhead Limit Exceeded:当JVM花太多时间执行回收,并且只能回收很少的堆空间时,就会发生此错误。
  2. OutOfMemoryError:Java heap space:假如在创建新的对象时,堆空间中的空间不足以存放新创建的对象,就会引发此错误。

这种情况与配置的最大内存有关,且受制于物理内存的大小。