JVM 设计者将 JVM 内存结构划分为多个区域,每个内存区域有各自的用途,负责存储各自的数据类型。有些内存区生命周期和 JVM 一致,也有些和线程生命周期一致,伴随着诞生,伴随着消亡。

Java 源代码文件会被编译为字节码(.class),然后由 JVM 中类加载器加载类字节码,加载完毕后,交给 JVM 执行引擎,整个程序郭晨中 JVM 会使用一段内存空间来存储执行过程中需要用到的数据和信息,这段空间一般被称为 Runtime Data Area,也就是 JVM 内存。

线程共享内存区

允许被所有线程共享访问的内存区,包括堆,方法区,运行时常量池三个内存区。

Java 堆区在 JVM 启动时被创建,在实际内存空间可以是不连续的。Java 堆用于存储对象实例,GC 执行垃圾回收重点区域。JVM 一些优化会将生命周期长的 Java 对象移动到堆外。所以 Java 堆不再是 Java 对象内存分配唯一的选择。

JVM 中的对象分为,生命周期比较短的瞬时对象,和长时间的对象。针对不同的 Java 对象,采取不同的垃圾手机策略,分代收集。GC 分代手机,新生代 和 老年代。

-Xms-Xmx 参数分别可以设置 JVM 启动时起始内存和最大内存。

方法区

方法区存储了每一个 Java 类结构信息,包括运行时常量池,字段和方法数据,构造函数,普通方法字节码内容以及类,实例,接口初始化需要用到的特殊方法等数据。

-XX:MaxPermSize 设置方法区内存大小,方法区内存不会被 GC 频繁回收,又称“永久代”。

运行时常量池 Runtime Constant Pool

运行时常量池属于方法区中一部分。有效的字节码文件包含类的版本信息、字段、方法和接口等描述信息之外,还包含常量池表(Constant Pool Table),运行时常量池就是字节码文件中常量池表的运行时表现形式。

线程私有内存区

和共享内存区不同,私有内存区是不允许被所有线程共享访问的。线程私有内存区是只允许被所属的独立线程进行访问的一类内存区域,包括 PC 寄存器,Java 栈,本地方法栈三个。

PC 计数器 Program Counter Register

JVM 中的 PC 计数器(又被称为 PC 寄存器,不同于物理的寄存器,这里只是代称),JVM 中的 PC 寄存器是对物理 PC 寄存器的抽象,线程私有,生命周期和线程生命周期一致。

Java 栈

栈用于存储栈帧(Stack Frame),栈帧中所存储的是局部变量表,操作数栈,以及方法出口等信息。

Java 堆中存储对象实例,Java 栈中局部变量表用于存储各类原始数据类型,引用(reference)以及 returnAddress 类型。

Java 栈允许被实现为固定或者动态扩展内存大小,如果 Java 栈被设定为固定大小,一旦线程请求分配的栈容量超过 JVM 允许最大值,JVM 会抛出一个 StackOverflowError 异常,如果配置动态,则抛出 OutOfMemoryError。

本地方法栈 Native Method Stack

本地方法栈(Native Method Stack)用于支持本地方法(native 方法,比如调用 C/C++ 方法),和 Java 栈作用类似。

一般来说,Java 对象引用涉及到内存三个区域:堆,栈,方法区

Object o = new Object()
  • o 是一个引用,存储在栈中
  • new Object() 实例对象存在堆中
  • 堆中还记录能够查询到此 Object 对象的类型数据(接口,方法,field,对象类型),实际的数据则放在方法区。

垃圾回收算法

垃圾标记

常见的垃圾回收算法是引用计数法和根搜索法,引用计数虽然实现简单粗暴,但是无法解决相互引用,无法释放内存的问题,所以引入了根搜索算法。根搜索算法是以根对象集合作为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达,如果不可达,则对象死亡,标记为垃圾对象。

在 HotSpot 中,根对象集合包含:

  • Java 栈中对象引用
  • 本地方法栈中对象引用
  • 运行时常量池中对象引用
  • 方法区中类静态属性的对象引用
  • 与一个类对应的唯一数据类型 Class 对象

垃圾回收

标记压缩算法

垃圾回收分两个阶段,垃圾标记和内存释放,和下面要说的两种回收算法相比,标记清除效率低下,更重要的是,可能造成回收之后内存空间不连续。

复制算法

为了解决标记压缩算法造成的内存碎片问题,JVM 设计者引入了复制算法。Java 堆区如果进一步细分,可以分为新生代,老年代,而新生代又可以分为 Eden 空间,From Survivor 和 To Survivor 空间。在 HotSpot 中,Eden 空间和另外两个空间默认比 8:1,可以通过 -XX:survivorRatio 来调整。执行 Minor GC(新生代垃圾回收)时,Eden 空间中的存活对象会被复制到 To 空间,之前经历过 Minor GC 并且在 From 空间中存活的对象,也会被复制到 To 空间。当下面两种特殊情况下,Eden 和 From 空间中的存活对象不会被复制到 To 空间:

  • 存活对象分代年龄超过 -XX:MaxTenuringThreshold 所指定的阈值,直接晋升到老年代
  • 当 To 空间容量达到阈值,存活对象直接晋升到老年代

当所有存活对象复制到 To 空间或者变为老年代时,剩下都为垃圾对象,意味着 Minor GC,释放掉 Eden 和 From 空间。然后 From 和 To 空间互换。

复制算法适合高效率的 Minor GC,但是不适合老年代的内存回收。

标记压缩算法

因为以上两种算法都有或多或少的问题,所以 JVM 又引入了 标记压缩算法,在成功标记出内存的垃圾对象后,该算法会将所有的存活对象都移动到一个规整的连续的内存空间,然后执行 Full GC 回收无用对象内存空间。当算法成功之后,已用和未用空间各自存放一边。

reference

  • 《Java 虚拟机精讲》