首页
«深入理解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)。
-
Graal编译器:用于替换HotSpot VM的C2编译器(服务端编译器:侧重优化代码质量)。
-
Substrate VM:属于Graal VM的一个极小型的运行时环境,目的是用于提前编译后的程序执行,能显著的降低程序的内存占用及启动时间。
-
提前编译:提前编译代码,这样JVM就可以直接调用预编译后的二进制库,无须再等待即时编译器运行时编译。能够减少即时编译带来的预热时间,但是显著的降低了Java链接过程的动态性,因为要求代码在编译器就是已知的,而动态链接是在运行时才确定的。
JVM运行时数据区域
程序计数器
程序计数器是一块较小的内存空间,为线程私有;用于记录当前线程正在执行的那条字节码指令的地址,若当前线程正在执行的是一个本地方法则应为空(Undefined);是唯一不会产生OOM的区域。
Java虚拟机栈:
线程私有,描述的是Java方法执行的线程内存模型:每一个方法被执行时,都会为其创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。线程请求栈超出虚拟机允许的深度会抛出StackOverflowErr异常;如果栈内存允许扩展且栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
- 局部变量表:存放方法参数和方法内部定义的局部变量,即编译期间即已知的基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址);以局部变量槽(slot)来表示,容量大小(变量槽的数量)在编译期就完全确定下来,在运行期间不会改变局部变量表的大小。
- 一个变量槽可以存放一个32位以内的数据类型,long和double类型会占据两个变量槽,由虚拟机自己决定一个变量槽使用多大的内存空间(32、64或更多位);
- 局部变量表中的变量是重要的GC Roots。
- 操作数栈:其最大深度也是在编译期就确定好的,同样是每32位占用一个栈单位深度。
- 动态连接:每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用;主要是为了调用过程中的动态连接,一部分的符号引用在每一次运行期间都会被转化为直接引用。
- 方法返回地址:用于在方法退出时帮助程序返回最初方法被调用时的位置;主调方法的程序计数器的值可以作为方法返回地址。
本地方法栈
线程私有,与Java虚拟机栈相似,是为本地方法服务的空间。
Java堆
被所有内存共享,在虚拟机启动时创建,唯一目的就是存放对象实例,是垃圾收集器管理的主要内存区域。堆可以是固定或可拓展的(通过参数-Xmx和-Xms设定),如果没有空闲内存用于分配实例,且堆也无法再拓展,则会抛出OutOfMemoryError异常。
- 所有的对象实例及数组都应当在堆上分配并不绝对,虚拟机的逃逸分析技术可能存在栈上分配、标量替换等优化手段;
TLAB
Thread Local Allocation Buffer,Java堆中线程私有的分配缓存区,用于提高对象分配时的效率;多个线程同时在堆上进行实例分配时,不需要进行同步而是分配在自己的TLAB上,只有当缓冲区都配完了才需要同步锁定。
方法区
非堆,不属于堆,被描述为堆的一个逻辑部分,也是各个线程共享的内存区域。用于存储已被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。永久代和元空间属于HotSpot虚拟机对方法区的实现。垃圾收集器在方法区的主要目标是回收常量池和类型卸载,当方法区无法满足新的内存分配需求时会抛出OutOfMemoryError异常。
- 永久代使用的是虚拟机内存,优点是可以使用垃圾收集器像Java堆一样管理这部分内存,缺点是Java应用容易出现内存溢出;
- 元空间是对永久代的替代,使用的是本地内存(只受物理内存限制),主要存储的是类型信息及常量池,原本位于永久代的字符串常量池、静态变量等已被移出堆。
运行时常量池
运行时常量池属于方法区的一部分,字节码常量池中的字面量会在类加载后存放到运行时常量池中,同时由符号引用翻译出来的直接引用也会存储在运行时常量池中;运行时常量池是动态性的,即在运行期间也能将新的常量放入池中,典型的如String类的intern()方法。
- 直接引用:直接指向目标的指针或偏移量、能间接定位到目标的句柄。
常量池(类文件)
每个Class文件中都包含一个常量池,紧跟在主、次版本号之后;每一项常量都是一个表,有不同的表数据结构;主要存放编译期间生成两大类常量:
- 字面量:代码中定义的文本字符串和常量值等;
- 符号引用:被模块导出或开放的包、类和接口的全限定名、字段及方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量。
直接内存
并不属于虚拟机运行时数据区的一部分,也并未在虚拟机规范中定义。在NIO类中使用,使用Native函数直接分配Java堆外内存(即属于本地内存),然后通过DirectByteBuffer对象作为这块堆外内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据。直接内存的容量大小可通过-XX:MaxDirectMemorySize参数指定,不指定则默认与Java堆最大值(-Xmx)一致。只能在Full GC时“顺便”对直接内存里的废弃对象进行清理。
对象
创建
- 遇到new指令后,定位到常量池中类的符号引用,检查是否已被加载、解析、初始化;
- 类加载检查通过后,为对象分配内存;
- 内存分配完成后要将分配到的空间初始化为0值,可以提前至TLAB分配阶段进行;
- 对对象头进行必要的设置,此时new指令执行完成;
- 通常new指令之后会跟随invokespecial指令,即接着执行<init>()方法(构造方法),按照程序的意愿对对象进行初始化。
内存布局
分为三部分:对象头、实例数据和对齐填充(Padding)。
- 对象头:Mark Word,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等,在32位和64位的虚拟机中分别占据32位和64位;类型指针,即指向对象的类型元数据的指针;如果对象是一个数组,则还在对象头中记录了数组的长度;
- 实例数据:是对象真正储存的有效信息,即程序代码中定义的类的字段内容;
- 对齐填充:仅仅起到占位符的作用,为了方便内存管理,HotSpot虚拟机要求对象的大小必须是8字节的整数倍,所以需要对齐填充来进行补全。
访问定位
Java程序通过栈上的reference数据类型操作堆上的具体对象,而对象的访问方式是由虚拟机实现的。
- 句柄访问:堆中可能划分出额外的内存作为句柄池,reference存储的是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自具体的地址,优点是在对象被移动(垃圾回收的普遍行为)时只需要改变句柄中的实例数据指针,而不需要修改reference。
- 直接指针访问:HotSpot虚拟机使用的方式,reference直接存储对象地址,访问速度更快,只需要一次指针定位。
OutOfMemoryError异常
虚拟机栈和本地方法栈溢出
HotSpot虚拟机不区分虚拟机栈和本地方法栈,都是使用-Xss参数设定,同时也不支持栈的动态扩展。当创建线程时如果申请不到足够的内存会出现OOM;当新的栈帧内存无法分配时抛出的都是StackOverFlowError异常,最常见的场景就是由无限递归引起(如两个对象循环依赖然后调用toString方法)。
Java堆溢出
是Java堆中最常见的OOM异常情况,需要判断是出现了内存泄漏还是内存溢出;如果是内存泄漏,则需要分析泄漏对象到GC Roots的引用链,找出产生内存泄漏的代码的具体位置;如果不是内存泄漏,则应该检查堆参数设置(-Xmx与-Xms),再从代码方面进行优化,减少程序运行期的内存消耗。。
- 通过参数(-XX:+HeapDumpOnOutOfMemoryError)可以让虚拟机在出现OOM时Dump出当前内存堆转储快照。
方法区溢出
方法区主要存放的是类型相关的信息,加载了太多类或者运行时生成了大量动态类会导致方法区溢出,HotSpot虚拟机支持一些参数防止元空间溢出:
- -XX:MaxMetaspaceSize:设置元空间最大值,默认-1不限制,即只受限于本地内存的大小;
- -XX:MetaspceSize:指定元空间的初始大小,如果达到该值会触发垃圾收集进行类型卸载,同时垃圾收集器也会对该值进行调整;
- -XX:MinMetaspaceFreeRatio/-XX:MaxMetaspaceFreeRatio:垃圾收集之后元空间剩余容量的百分比,垃圾收集器会调整元空间的大小以保证元空间剩余容量百分比在此区间内,因为空闲空间过大会造成内存空间的浪费,空闲空间过小又会引起垃圾收集的频率增加。
直接内存溢出
使用DirectByteBuffer分配内存时也会抛出OOM,但并不是向操作系统申请分配内存后才抛出,而是通过计算得知内存无法分配后就会抛出异常。
垃圾回收上头
GC overhead limit exceeded,Java进程花费98%以上的时间执行GC,但只恢复了不到2%的内存,且该动作连续重复了5次,可能会先触发Java堆溢出。
本地交换空间不足
Out of swap space,表示所有可用的虚拟内存已被耗尽(虚拟内存由物理内存和交换空间组成),一般很难出现。
被操作系统‘杀死’
Kill process or sacrifice child,Linux内核允许进程申请的内存总量可以大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源,而当内存不足时,将自动激活OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。