HOME/Articles/

JAVA堆内存浅析

Article Outline

JAVA堆的特点

  • JAVA堆是JAVA虚拟机所管理的内存中最大的一块

  • JAVA堆是被所有线程共享的一块内存区域

  • 虚拟机启动时创建JAVA堆

  • JAVA堆的唯一目的是存放对象实例

  • JAVA堆是垃圾收集器管理的主要区域

  • 从内存回收角度看,由于现在收集器基本都采用分代收集算法,因此JAVA堆可细分为:新生代和老年代,新生代又被划分为三个区域Eden, From Survivor, To Survivor. 进一步划分的目的是为了更好地回收内存或更快地分配内存。

  • JAVA堆的大小是可扩展的,通过 -Xmx 和 -Xms 控制

  • 如果堆内存不够分配实例对象,并且堆也无法再扩展时,将抛出 OutOfMemoryError 异常

Young 新生代(占堆空间的1/3) Old 老年代(占堆空间的2/3)
Eden From Survivor To Survivor
占整个新生代空间的8/10 占整个新生代空间的1/10 占整个新生代空间的1/10

堆内存划分

  • 堆大小 = 新生代 + 老年代。 堆的大小可通过参数 -Xms (堆的初始容量)、 -Xmx (堆的最大容量) 来指定

  • 新生代默认划分比例是 Eden: From Survivor: To Survivor = 8: 1: 1。(可通过参数 -XX:SurvivorRatio设定)

  • JVM 每次只会使用 Eden 和其中一块 Survivor 区域来为对象服务。因此,任意时刻下总有一块 Survivor 区域空闲

  • 新生代实际可用的内存空间是 9/10 的新生代空间

堆的垃圾回收方式

JAVA堆是GC垃圾回收的主要区域。GC分为两种:Minor GC 和 Full GC(亦称 Major GC)

Minor GC

  • Minor GC 是发生在新生代中的垃圾收集动作,所采用的算法是 复制算法

  • GC一般为堆空间某个区域发生了垃圾回收

  • 几乎所有JAVA对象都“出生”于新生代区域,即JAVA对象都在新生代区域进行内存的申请与JAVA对象的存放。JAVA中的大部分对象通常不会长久地“存活”,具有朝生夕死的特点

  • 当一个对象被判定为“死亡”时,GC就负责来回收掉这部分对象的内存空间

  • 新生代是收集垃圾的频繁区域

Full GC

  • Full GC 是发生在老年代的垃圾收集动作,采用 标记-清除算法(可能产生大量内存碎片)或 标记整理算法(解决内存碎片问题)

  • 现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的 老年代(Old) 不同,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉”。因此,Full GC 发生的次数不会像 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,一般是Minor GC的 10倍以上

  • 标记-清除算法 收集垃圾时会产生许多的内存碎片(不连续的内存空间),此后需要为较大的对象分配内存空间时,如果无法找到足够的连续内存空间,就会提前触发一次 GC 的收集动作

垃圾对象的判定标准

JVM 的 GC 工作主要针对的对象是堆内存。在做GC工作之前,首先要判定堆内存中的对象实例是否为垃圾,通常使用以下两种算法来定义

1. 引用计数算法

JAVA 在运行时,当有一个地方引用该对象实例,会将这个对象引用计数值 +1,引用失效时就 -1. JVM 在扫描内存时,发现引用计数值 = 0 即垃圾对象,计数值 > 0 即活跃对象。
目前垃圾回收算法,没有采用引用计数算法,原因是在对象互相引用的情况下,无法判定两者是否为垃圾对象

2. 根搜索算法

根搜索算法是以 “GC ROOTS” 为起始点往下搜索,所有经过的对象合并起来称为引用链,在这引用链里,没有的对象称为垃圾对象(JVM 还做了一个筛选动作,判定当前对象是否执行finalize()方法,如果不需要执行才判定为垃圾对象,这里不做介绍),在引用链里的是活跃对象

  • GC ROOT:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象

    • 方法区中的类静态属性引用的对象

    • 方法区中的常量引用的对象

    • 本地方法栈中 JNI(Native 方法)的引用对象

垃圾回收算法

1. 标记-清除(Mark-Sweep)

JVM 会扫描所有的对象实例,通过根搜索算法,将活跃对象进行标记,JVM 再一次扫描所有对象,将未标记的对象进行清除,只有清除动作,不作任何的处理,这样导致的结果会存在很多的内存碎片

2. 复制(copying)

JVM 扫描所有对象,通过根搜索算法标记被引用的对象,之后会申请新的内存空间,将标记的对象复制到新的内存空间里,存活的对象复制完,会清空原来的内存空间,将新的内存作为 JVM 的对象存储空间。这样虽然解决了内存内存碎片问题,但是如果对象很多,重新申请新的内存空间会很大,在内存不足的场景下,会对 JVM 运行造成很大的影响

3. 标记-整理(Mark-compact)

标记整理实际上是在标记清除算法上的优化,执行完标记清除全过程之后,再一次对内存进行整理,将所有存活对象统一向一端移动,这样解决了内存碎片问题

4. 分代回收

目前 JVM 常用回收算法就是分代回收,年轻代以复制算法为主,老年代以标记整理算法为主。原因是年轻代对象比较多,每次垃圾回收都有很多的垃圾对象回收,而且要尽可能快的减少生命周期短的对象,存活的对象较少,这时候复制算法比较适合,只要将有标记的对象复制到另一个内存区域,其余全部清除,并且复制的数量较少,效率较高;而老年代是年轻代筛选出来的对象,被标记比较高,需要删除的对象比较少,显然采用标记整理效率较高