《深入理解Java虚拟机:JVM高级特性与最佳实践》- 内存管理机制

Java 内存区域与内存溢出

Java 虚拟机运行时数据区(结构图)

  1. 程序计数器:是一块内存空间较小的空间,当前线程所执行的字节码的行号指示器。线程私有内存,Java 虚拟机规范中唯一一个没有规定任何 OutOfMemory 情况的区域。
  2. 虚拟机栈:描述 Java 方法执行的内存模型(每个方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数帧、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程)。线程私有内存,生命周期与线程相同。可能会抛出 StackOverFlowError(线程请求栈深度大于虚拟机允许的深度) 或 OutOfMemory(虚拟栈机动态扩展时无法申请到做够的内存) 异常。
  3. 本地方法帧:类似虚拟机栈(虚拟机栈为 Java 方法服务,本地方法栈为 Native 方法服务),会抛出 StackOverFlowError 或 OutOfMemory 异常。
  1. Java 堆:虚拟机启动时创建,对大部分应用来说是内存最大的区域,也是垃圾收集器管理的主要区域。用于存放对象实例。线程共享内存(从内存分配角度看,线程共享的 Java 堆中可能划出多个线程私有的分配缓冲区[TLAB])。可位于物理内存可不连续但逻辑内存连续的空间中。会抛出 OutOfMemory 异常。
  2. 方法区:存储已经被虚拟机加载的类的信息、常量()、静态变量、即时编译器编译后的代码等数据。线程共享内存。可位于不连续的物理内存空间。会抛出 OutOfMemory 异常。
  3. 运行时常量池:方法区的一部分。类常量池(编译时期生成的各种字面量和符号引用存放的地方)中的内容在类加载后进入方法区的运行时常量池。
  4. 直接内存:Java 堆之外的内存。如 NIO 中使用 Native 函数库直接分配的堆外内存。会抛出 OutOfMemory 异常。

对象的创建

常量池中检测类的符号引用 -> 检查类是否加载(未加载则加载类) -> 为新生对象分配内存 -> 将分配到的空间都初始化零值 -> 虚拟机对对象的设置(对象属于哪个类的实例、对象的哈希码等) -> 执行init方法初始化构造对象

对象的内存布局

对象的内存中的存储布局分为三部分(HotSpot 虚拟机为例):

  • 对象头(Header):分为两部分。第一部分用于存储自身对象运行时数据(哈希码、GC 分代年龄、锁状态标识、线程持有的锁、偏向线程 ID、偏向时间戳等);另一部分是类型指针(对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例)。
  • 实例数据(Instance Data):在程序代码中定义的各种类型的字段类容(包括从父类继承下来的和子类中定义的)。
  • 对齐填充(Padding):不是一定存在,起占位符的作用。

OutOfMemory 异常

  1. Java 堆溢出:在对象数量到达最大堆的容量限制之后就会产生内存溢出异常。
  2. 虚拟机栈与本地方法栈溢出:线程请求栈深度大于虚拟机允许的深度抛出 StackOverFlowError 异常; 虚拟栈机动态扩展时无法申请到做够的内存抛出 OutOfMemory 异常。
  3. 方法区和运行时常量池溢出:大量类被加载且无法被回收/大量常量被创建且无法被回收(Stirng.intern方法可以将常量池中没有的字符串添加到常量池中,并返回该 String 对象的引用,在 JDK 1.6 中,intern()方法把首次遇到的字符串实例复制到永久代中并返回永久代中该实例的引用,但在 JDK 1.7 中intern()方法不会复制实例,只是在常量池中记录首次出现的实例的引用)。
  4. 直接内存溢出。

垃圾收集器与内存分配策略

Java 中的引用

  1. 强引用(Strong Reference):类似Object obj = new Object()这类引用。只要引用存在永远不会回收被引用的对象。
  2. 软引用(Soft Reference):描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够内存,才会抛出内存溢出异常。
  3. 弱引用(Weak Reference):描述一些非必需的对象,强度比软引用弱,被弱引用关联的对象只能生存到下一次垃圾回收之前,无论当前内存是否够用,若引用关联的对象都会被回收掉。
  4. 虚引用(Phantom Reference):又称为幽灵引用或幻影引用,它是最弱的一种引用关系。对象生存不会受虚引用影响,无法通过虚引用取得一个对象实例。为一个对象设置虚引用唯一的作用就是在这个对象被回收时能收到一个通知。

可达性分析算法

概括:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连,则此对象不可用。Java 中可作为 GC Roots 的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。

回收方法区

永久代的垃圾收集主要包括两部分:废弃常量和无用的类。

垃圾收集算法

垃圾收集器

内存回收的具体实现,HotSpot 虚拟机的垃圾收集器如下图所示:

hotspot_vm_garbage_collecor.jpeg

内存分配与回收策略(Serial/Serial Old 收集器)

  1. 对象优先在 Eden 分配:大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有做够空间进行分配时,虚拟机将发起一次 Minor GC(新生代 GC)。
  2. 大对象直接进入老年代:大对象指需要大量连续内存空间的 Java 对象(比如很长的字符串以及数组等)。大对象经常导致内存还有不少空间时就提前触发垃圾收集以获取足够的空间”安置“它们。
  3. 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在 Eden 出生并且经过第一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,此时对象年龄为 1,对象在 Survivor 每”熬过“一次 Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认 15 岁),就将会被晋升到老年代中。
  4. 动态对象年龄判断:如果在 Survivor 空间中相同年龄所有对象大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 MaxTenuring Threshold 中要求的年龄。
  5. 空间分配担保:在发生 Minor GC 之前虚拟机会检查老年代最大可用的连续内存空间是否可容纳新生代所有对象,可以则发生 Minor GC,否则检查 HandlePromotionFailure 设置值是否允许担保失败,允许就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行 Minor GC(有风险),否则(或者 HandlePromotionFailure 设置值为不允许)进行一次 Full GC。

注意:

  • 新生代 GC(Minor GC):发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC/Full GC):发生在老年代的 GC,出现了 Major GC,经常至少会伴随一次的 Minor GC(非绝对,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般比 Minor GC 慢 10 倍以上。