深入理解JVM—堆(Heap)

1、堆(Heap)概述

       Java堆(Java Heap)是JVM所管理的最大的一块内存空间。Java堆是被所有线程共享的一块内存区域,在JVM启动的时候创建堆的唯一目的就是存放对象实例的,Java中几乎所有的对象实例和数组都在对上分配内存。Java堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存亦不需要保证是连续的。

Heap 用来存储数组与new 关键字创建的对象实例,例如:

Student stu = new Student();  

       首先JVM 会在Heap 中分配出Student 对象存储的内存区域,并将地址返回,然后再在JVM Stack 中的局部表量表中创建Student 对象的引用用来存放Heap 分配的Student 地址。之后就可以在程序中使用栈中的引用对象来访问堆中的数组或对象。当使用完对象后,我们不必显式的管理堆内存释放工作,堆内存的释放会由GC(垃圾收集器)自动完成。


图1 对象的分配和访问

       Java堆是垃圾回收器工作的主要区域,因此堆也被称为“GC堆”(Garbage Collected Heap)。从垃圾回收的角度来看,由于现在的收集器基本采用分代收集算法,所以Java堆可以细分为:新生代(Young/New Generation)和老年代(Old/Tenured Generation 老年代)。新生对象放置在新生代中,新生代由Eden空间Survivor From空间Survivor To空间 组成;老年代用于存放程序中经过指定次数垃圾回收后还存活的对象。


图2 堆内存
(1)Young/New Generation 新生代

       程序中新建的对象都将分配到新生代中,新生代又由Eden(伊甸园)与两块Survivor(幸存者) Space 构成。Eden 与Survivor Space 的空间大小比例默认为8:1,即当Young/New Generation 区域的空间大小总数为10M 时,Eden 的空间大小为8M,两块Survivor Space 则各分配1M,这个比例可以通过-XX:SurvivorRatio 参数来修改。Young/New Generation的大小则可以通过-Xmn参数来指定。

Eden:刚刚新建的对象将会被放置到Eden 中,这个名称寓意着对象们可以在其中快乐自由的生活。
Survivor 空间:幸存者区域是新生代与老年代的缓冲区域,两块幸存者区域在逻辑上分别为s0 与s1。

       当触发Minor GC 后将Eden区中仍然存活的对象移动到S0中去(From Eden To s0)。这样Eden 就会被清空可以分配给新的对象。当再一次触发Minor GC后,S0和Eden 中存活的对象被移动到S1中(From s0 To s1),S0即被清空。在同一时刻, 只有Eden和一个Survivor Space同时被操作。所以s0与s1两块Survivor 区同时会至少有一个为空闲的,这点从下面的图中可以看出。
       当每次对象从Eden 复制到Survivor Space 或者从Survivor Space 之间复制,计数器会自动增加其值。 默认情况下如果复制发生超过16次,JVM 就会停止复制并把他们移到老年代中去。如果一个对象不能在Eden中被创建,它会直接被创建在老年代中。

● 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,通常很多的对象都活不过一次GC,所以Minor GC 非常频繁,一般回收速度也比较快。

● Minor GC 清理过程(图中红色区域为垃圾)


图3 Minor GC发生之前

图4 Minor GC发生之后

注意
1. 图中的"From" 与"To" 只是逻辑关系而不是Survivor 空间的名称,也就是说谁装着对象谁就是"From"。
2. 一个对象在幸存者区被移动/复制的次数决定了它是否会被移动到堆中。

(2)old/Tenured Generation 老年代

       老年代用于存放程序中经过几次垃圾回收后还存活的对象,例如缓存的对象等,老年代所占用的内存大小即为-Xmx 与-Xmn 两个参数之差。
       堆是JVM 中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new 对象的开销是比较大的,鉴于这样的原因,Hotspot JVM 为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB(Thread Local Allocation Buffer),其大小由JVM 根据运行的情况计算而得,在TLAB 上分配对象时不需要加锁,因此JVM 在给线程的对象分配内存时会尽量的在TLAB 上分配,在这种情况下JVM 中分配对象内存的性能和C 基本是一样高效的但如果对象过大的话则仍然是直接使用堆空间分配,TLAB 仅作用于新生代的Eden,因此在编写Java 程序时,通常多个小的对象比大的对象分配起来更加高效,但这种方法同时也带来了两个问题,一是空间的浪费,二是对象内存的回收上仍然没法做到像Stack 那么高效,同时也会增加回收时的资源的消耗,可通过在启动参数上增加 -XX:+PrintTLAB来查看TLAB 这块的使用情况。

● 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,通常会伴随至少一次Minor GC(但也并非绝对,在ParallelScavenge 收集器的收集策略里则可选择直接进行Major GC)。Major GC 的速度一般会比Minor GC 慢10倍以上。

       虚拟机给每个对象定义了一个对象年龄(age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

(3)JVM中给对象分配内存的过程

通过上的学习我们可以大致总结出一个对象被创建后,内存申请过程如下:
1. JVM 会试图为相关Java 对象在Eden区 中初始化一块内存区域。
2. 当Eden 空间足够时,内存申请结束。否则进入第三步。
3. JVM 试图释放在Eden 中所有不活跃的对象(执行1或更高级的垃圾回收), 释放后若Eden 空间仍然不足以放入新对象,则试图将部分Eden 中活跃对象放入Survivor 区。
4. Survivor 区被用来作为新生代与老年代的缓冲区域,当老年代空间足够时,Survivor 区的对象会被移到老年代,否则会被保留在Survivor 区。
5. 当老年代空间不够时,JVM 会在老年代进行0级的完全垃圾收集(Major GC/Full GC)。
6. Major GC/Full GC后,若Survivor 及老年代仍然无法存放从Eden 复制过来的部分对象,导致JVM 无法在Eden 区为新对象创建内存区域,JVM 此时就会抛出内存不足的异常。

2、Java堆相关调优参数

JVM的调优参数,我这里首推官方文档,常用的Java堆有关的调优参数如下:
* -Xms size:设置初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制

  • -Xmx size:设置最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 。等价的参数:-XX:MaxHeapSize=size
  • -Xmn size:设置新生代的内存空间大小,即Eden+ 2个survivor space。 在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 等价的参数:-XX:NewSize=size
  • -XX:SurvivorRatio:设置新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。
  • -XX:NewRatio=ratio:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
  • -XX:MaxTenuringThreshold=threshold:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概论。
  • -Xss size:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小默认为1M,以前每个线程堆栈大小默认为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
  • -XX:TLABSize=size:设置TLAB的初始大小,将它设置为0表示让JVM自动选择初始大小。
  • **-XX:+PrintGCDetails **:打印 GC 信息
  • -XX:+HeapDumpOnOutOfMemoryError: 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

留言区

还能输入500个字符