JVM堆区
- 堆(Heap)
- 堆区的组成:新生代+老年代
- 堆空间的大小设置
- 创建对象的内存分配
- 堆区的分代垃圾收集思想
- 堆区产生的错误
堆(Heap)
Heap堆区,用于存放对象实例和数组的内存区域
Heap堆区,是JVM所管理的内存中最大的一块区域,被所有线程共享的一块内存区域。堆区中存放内存实例,“几乎”所有的对象实例以及数组都在这里分配内存。
每一个JVM进程值存在一个堆区,它在JVM启动时被创建,JVM规范中规定堆区可以是物理上不连续的内存,但必须是逻辑上连续的内存。
- 堆区是线程共享的区域,同时也是JVM管理最大的内存区域。
- JVM规范中描述,所有的对象实例及数组都应该在运行时分配在堆上。而他们的引用会被保存在虚拟机栈中,当方法结束时,这些实例不会被立即清除,而是等待GC垃圾回收。
- 由于堆占用内存大,所以是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,不同区域存放对象的用途和方式不同:
- 伊甸区(Eden):存放大部分新创建对象。
- 幸存区(Survivor):存放Minor GC 之后,Eden区和幸存区(Servivor)本身没有被回收的对象。
- 老年代:存放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)。
部分收集:
- 新生代收集YGC(Minor GC/Young GC):回收新生代区域,频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收,性能耗费比较少。例如:Seriall、ParNew 、ParallelScavenge等垃圾收集器都是新生代收集;
- 老年代收集器Old GC(Major GC/Old GC):回收老年代区域,例如Serial Old、CMS、Parallel Old等垃圾收集器都是老年代收集;
- 混合收集(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的原因:
- 大对象:系统一次加载了过多数据到内存中,导致大对象进入老年代。
- 内存泄露:频繁创建了大量对象,但是无法被回收(比如IO流对象使用后未调用close方法释放资源),先引发FGC,最后导致OOM。
- 程序频繁生成一些长生命周期的对象,当这些对象呢的存活年龄超过分代奈年龄时便会进入老年代,最后引发FGC。
- 程序BUG导致动态生成了很多新类,使得Metaspace不断被占用,先引发FGC,最后导致OOM。
- JVM参数设置不合理:包括内存大小、新生代和老年代的大小、Eden区和Survivor区的大小、元空间大小、垃圾回收算法等等。
堆区产生的错误
堆区最容易处出现的就是OutOfMemoeyError错误,这种错误的表现形式会有以下两种:
- OutOfMemoeyError:GC Overhead Limit Exceeded:当JVM花太多时间执行回收,并且只能回收很少的堆空间时,就会发生此错误。
- OutOfMemoryError:Java heap space:假如在创建新的对象时,堆空间中的空间不足以存放新创建的对象,就会引发此错误。
这种情况与配置的最大内存有关,且受制于物理内存的大小。