Skip to the content.

首页

«深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)»


JVM

Java虚拟机是用于运行Java字节码(.class)文件的抽象化计算机,使得Java程序无须关注具体的操作系统平台信息。

Sun Classic

发布于JDK 1.0,首个商用JVM;纯解释执行;可以外挂即时(JIT)编译器,但会无法和解释器同时工作;使用句柄方式访问对象。

Exact VM

发布于JDK 1.2,具备现代高性能虚拟机雏形,准确式内存管理、热点探测、编译器与解释器混合工作模式等。

BEA JRockit

专注于服务端场景,不太关注程序启动速度,完全使用即时编译模式,其优秀特性已被融合进HotSpot VM。

HotSpot VM

占据统治地位的JVM,准确式内存管理、分层编译(两级即时编译器)、热点代码探测、编译器与解释器协同工作

Graal VM

在HotSpot基础上增强而成的跨语言全栈虚拟机。它没有任何语言倾向,可以作为“任何语言”的运行平台,原理即是将这些语言的源代码(如js)或源代码编译后的中间格式(如字节码)通过解释器转换为中间表示(IR),此过程称之为程序特化(Specialized/Evaluation)


JVM运行时数据区域

程序计数器

程序计数器是一块较小的内存空间,为线程私有;用于记录当前线程正在执行的那条字节码指令的地址,若当前线程正在执行的是一个本地方法则应为空(Undefined);是唯一不会产生OOM的区域

Java虚拟机栈

线程私有,描述的是Java方法执行的线程内存模型:每一个方法被执行时,都会为其创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。线程请求栈超出虚拟机允许的深度会抛出StackOverflowErr异常;如果栈内存允许扩展且栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

本地方法栈

线程私有,与Java虚拟机栈相似,是为本地方法服务的空间。

Java堆

被所有内存共享,在虚拟机启动时创建,唯一目的就是存放对象实例,是垃圾收集器管理的主要内存区域。堆可以是固定或可拓展的(通过参数-Xmx和-Xms设定),如果没有空闲内存用于分配实例,且堆也无法再拓展,则会抛出OutOfMemoryError异常。

TLAB

Thread Local Allocation Buffer,Java堆中线程私有的分配缓存区,用于提高对象分配时的效率;多个线程同时在堆上进行实例分配时,不需要进行同步而是分配在自己的TLAB上,只有当缓冲区都配完了才需要同步锁定。

方法区

非堆,不属于堆,被描述为堆的一个逻辑部分,也是各个线程共享的内存区域。用于存储已被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。永久代和元空间属于HotSpot虚拟机对方法区的实现。垃圾收集器在方法区的主要目标是回收常量池和类型卸载,当方法区无法满足新的内存分配需求时会抛出OutOfMemoryError异常。

运行时常量池

运行时常量池属于方法区的一部分,字节码常量池中的字面量会在类加载后存放到运行时常量池中,同时由符号引用翻译出来的直接引用也会存储在运行时常量池中;运行时常量池是动态性的,即在运行期间也能将新的常量放入池中,典型的如String类的intern()方法。

常量池(类文件)

每个Class文件中都包含一个常量池,紧跟在主、次版本号之后;每一项常量都是一个表,有不同的表数据结构;主要存放编译期间生成两大类常量

直接内存

并不属于虚拟机运行时数据区的一部分,也并未在虚拟机规范中定义。在NIO类中使用,使用Native函数直接分配Java堆外内存(即属于本地内存),然后通过DirectByteBuffer对象作为这块堆外内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据。直接内存的容量大小可通过-XX:MaxDirectMemorySize参数指定,不指定则默认与Java堆最大值(-Xmx)一致。只能在Full GC时“顺便”对直接内存里的废弃对象进行清理。


对象

创建

  1. 遇到new指令后,定位到常量池中类的符号引用,检查是否已被加载、解析、初始化;
  2. 类加载检查通过后,为对象分配内存;
  3. 内存分配完成后要将分配到的空间初始化为0值,可以提前至TLAB分配阶段进行
  4. 对对象头进行必要的设置,此时new指令执行完成;
  5. 通常new指令之后会跟随invokespecial指令,即接着执行<init>()方法(构造方法),按照程序的意愿对对象进行初始化。

内存布局

分为三部分:对象头、实例数据和对齐填充(Padding)。

访问定位

Java程序通过栈上的reference数据类型操作堆上的具体对象,而对象的访问方式是由虚拟机实现的。


OutOfMemoryError异常

虚拟机栈和本地方法栈溢出

HotSpot虚拟机不区分虚拟机栈和本地方法栈,都是使用-Xss参数设定,同时也不支持栈的动态扩展。当创建线程时如果申请不到足够的内存会出现OOM;当新的栈帧内存无法分配时抛出的都是StackOverFlowError异常,最常见的场景就是由无限递归引起(如两个对象循环依赖然后调用toString方法)。

Java堆溢出

是Java堆中最常见的OOM异常情况,需要判断是出现了内存泄漏还是内存溢出;如果是内存泄漏,则需要分析泄漏对象到GC Roots的引用链,找出产生内存泄漏的代码的具体位置;如果不是内存泄漏,则应该检查堆参数设置(-Xmx与-Xms),再从代码方面进行优化,减少程序运行期的内存消耗。。

方法区溢出

方法区主要存放的是类型相关的信息,加载了太多类或者运行时生成了大量动态类会导致方法区溢出,HotSpot虚拟机支持一些参数防止元空间溢出:

直接内存溢出

使用DirectByteBuffer分配内存时也会抛出OOM,但并不是向操作系统申请分配内存后才抛出,而是通过计算得知内存无法分配后就会抛出异常。

垃圾回收上头

GC overhead limit exceeded,Java进程花费98%以上的时间执行GC,但只恢复了不到2%的内存,且该动作连续重复了5次,可能会先触发Java堆溢出

本地交换空间不足

Out of swap space,表示所有可用的虚拟内存已被耗尽(虚拟内存由物理内存和交换空间组成),一般很难出现。

被操作系统‘杀死’

Kill process or sacrifice child,Linux内核允许进程申请的内存总量可以大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源,而当内存不足时,将自动激活OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。