JVM概述
的主要功能
JVM
解释和运行:对字节码文件中的指令,实时地解释成机器码让计算机执行内存管理:自动为对象、方法等分配内存;自动的垃圾回收机制,回收不再使用的对象即时编译:对热点代码进行优化,提升执行效率对热点代码(短时且执行多次)翻译为机器码,并保存在内存中,下次使用直接调用
组成
JVM
通过类加载器,将类和接口的字节码文件加载到运行时数据区域执行引擎会将字节码文件中的指令解释成机器码,用即时编译器优化性能,通过调用本地接口执行类和接口同时本地接口自身也会创建类和接口在
JVM
的运行时数据区域
JVM
字节码文件
字节码文件的组成
基本信息
字节码文件保存了源码编译后的内容,以二进制的方式存储
基本信息存储魔数、**字节码文件对应的
版本号, 访问标识 (
Java
等等),父类和接口
public final
魔数
文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。
字节码文件中,将文件头称为魔数。
java
主副版本号
主副版本号指的是编译字节码文件的
版本号,主版本号用来标识大版本号,
JDK
使用了
JDK1.0-1.1
,
45.0-45.3
是
JDK1.2
之后每升级一个大版本就加
46
;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。 版本号的作用主要是判断当前字节码的版本和运行时的
1
是否兼容
JDK
常量池
保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用,避免相同内容重复定义,节省空间。
常量池中的数据都有一个编号,编号从
开始。在字段或者字节码指令中通过编号快速找到对应的数据。
1
字节码指令中通过编号引用到常量池的过程称之为符号引用。
方法
存储当前类或接口声明的方法信息的字节码指令
字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的
属性中。操作数栈:临时存放数据的地方,局部变量表是存放方法中的局部变量的位置。局部变量表数组:局部变量存放的位置
Code
类的生命周期
加载
加载
阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。 程序员可以使用
(Loading)
代码拓展的不同的渠道
Java
本地文件:磁盘上的字节码文件动态代理生成:程序运行时使用动态代理生成
类加载器在加载完类之后,
虚拟机将字节码中的信息保存内存的方法区中。生成
Java
对象,保存类的所有信息,里边还包含实现特定功能,比如多态的信息。
InstanceKlass
同时,
虚拟机会在堆中生成与方法区中数据类似的
Java
对象。 作用是在
java.lang.Class
代码中去获取类的信息以及存储静态字段的数据(
Java
及之后)。
JDK8
对于开发者来说,只需要访问堆中的
对象而不需要访问方法区中所有信息。这样
Class
虚拟机就能很好地控制开发者访问数据的范围
Java
连接

验证
连接
阶段的第一个环节是验证,验证的主要目的是检测
(Linking)
字节码文件是否遵守了《
Java
虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
Java
文件格式验证,比如文件是否以
开头,主次版本号是否满足当前
0xCAFEBABE
虚拟机版本要求。元信息验证,例如类必须有父类(
Java
不能为空)。验证程序执行指令的语义是否正确符号引用验证,例如是否访问了其他类中
super
的方法等
private
准备
准备阶段为静态变量
分配内存并设置初始值, 而每一种基本数据类型和引用数据类型都有其初始值,并不是程序员代码赋的初始值
(static)
修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值
final
解析
解析阶段主要是将常量池中的符号引用替换为直接引用。直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。
初始化
初始化阶段会执行静态代码块中的代码,并为静态变量赋值。初始化阶段会执行字节码文件中
部分(类的初始化)的字节码指令。[
clinit
方法中的执行顺序与
clinit
中编写的顺序是一致的。]
Java
以下几种方式会导致类的初始化:
访问一个类的静态变量或静态方法,注意变量是
修饰的并且等号右边是常量不会触发初始化。 反射调用
final
。 [可以通过传递参数不初始化类,反射工具类经常只需要类的元信息(字段、方法),并不想触发类的初始化逻辑(避免耗时/副作用) ]
Class.forName(String className)
一个该类的对象时。执行
new
方法的当前类。
Main
Class<?> clazz = Class.forName("com.example.MyClass", true, currentClassLoader);
这个调用会:
1. 加载类的字节码到 JVM(如果没加载过)
2. 链接(验证 + 准备 + 解析)
3. 初始化类(执行 <clinit> 静态代码块、静态变量赋值)
4. 所以静态变量赋值和静态代码块一定会执行
Class<?> clazz = Class.forName("com.example.MyClass", false, MyClass.class.getClassLoader());
这里的第二个参数 initialize:
1. true → 按正常流程初始化类(执行 <clinit>)。
2. false → 只加载和链接,不执行初始化。
以下几种方式不会出现初始化指令
clinit
无静态代码块且无静态变量赋值语句。有静态变量的声明,但是没有赋值语句。静态变量的定义使用
关键字,这类变量会在准备阶段直接进行初始化。
final
继承下的初始化
直接访问父类的静态变量,不会触发子类的初始化。子类的初始化
调用之前,会先调用父类的
clinit
初始化方法。 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
clinit
类加载器
只会运行二进制文件,类加载器的作用就是将字节码文件加载到
JVM
中,从而让
JVM
程序启动
Java
本地接口
是
JNI
缩写,允许
Java Native Interface
调用其他语言编写的方法。在
Java
类加载器中,主要用于调用
hotspot
虚拟机中的方法,这些方法使用
Java
编写。 加载阶段
C++
获取字节码文件 生成方法区对象生成堆上
对象
Class
类加载器的分类
代码中实现
Java
中默认提供或自定义继承自抽象类
JDK
ClassLoader
Java虚拟机底层源码实现
源代码位于虚拟机的源码中,实现语言与虚拟机底层语言一致,比如
Java使用
Hotspot加载程序运行时的基础类
C++
类加载器的设计
和
JDK8
之后的版本差别较大,
8
及之前的版本中默认的类加载器有如下几种
JDK8
启动类加载器
启动类加载器(
)是由
Bootstrap ClassLoader
虚拟机提供的、使用
Hotspot
编写的类加载器。用于加载
C++
中最核心的类。
Java
扩展类加载器和应用程序类加载器都是
中提供的、使用
JDK
编写的类加载器。 是一个静态内部类,继承自
Java
,具备通过目录或指定
URLClassLoader
包将字节码文件加载到内存中。
jar
扩展类加载器
扩展类加载器(
)是
Extension Class Loader
中提供的、 使用
JDK
编写的类加载器。 加载
Java
安装目录
Java
下的类文件
/jre/lib/ext
应用程序加载器
加载
下的类文件
classpath
双亲委派机制
双亲委派机制的核心是解决一个类到底由谁来加载的问题
保证类加载的安全性,避免恶意代码替换
中的核心类库避免同一个类被多次加载
JDK
每个
实现的类加载器中保存了一个成员变量叫“父”(
Java
)类加载器,可以理解为它的上级, 并不是继承关系。 当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。向上查找如果已经加载过,就直接返回
Parent
对象,加载过程结束。这样就能避免一个类重复加载。若未加载,则向下加载,找该类在哪个加载器的路径中。
Class
应用程序类加载器的
父类加载器是扩展类加载器,而扩展类加载器的
parent是空(因为启动类加载器由
parent编写,无法直接获取),但在代码逻辑上,扩展类加载器依会把启动类加载器当成父类加载器处理。启动类加载器使用
C++编写,没有父类加载器。
C++
打破双亲委派机制
自定义类加载器
一个
程序中是可以运行多个
Tomcat应用的,如果这两个应用中出现了相同限定名的类,比如
Web类,
Servlet要保证这两个类都能加载并且它们应该是不同的类。
Tomcat如果不打破双亲委派机制,当应用类加载器加载
应用
Web中的
1之后,
MyServlet应用
Web中相同限定名的
2类就无法被加载了。
MyServlet
使用自定义类加载器来实现应用之间类的隔离。每一个应用有一个独立的类加载器加载对应的类。
Tomcat
中包含了
ClassLoader
个核心方法。双亲委派机制的核心代码就位于
4
方法中。
loadClass
//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
//否则调用父类加载器的加载方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//父类未加载,则由本类加载
if (c == null) {
c = findClass(name);
}
打破双亲委派机制的核心就是将上述代码重新实现
自定义类加载器默认的父类加载器是应用程序类加载器
两个自定义类加载器加载相同限定名的类不会冲突,在同一个
虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。若只是为了拓展渠道加载类,正确的实现应该是实现一个自定义类加载器,重写其
Java
方法,不破坏其双亲委派机制
findClass
线程上下文类加载器
中使用
JDBC来管理项目中引入不同数据库的驱动,如
DriverManager驱动等。
mysql类位于
DriverManager包中,由启动类加载器加载。而依赖中的
rt.jar驱动对应的类,由应用程序类加载器来加载。
mysql
属于
DriverManager是启动类加载器加载的。而用户
rt.jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。 也就是在启动类加载器加载的时候, 只会查找核心类库,不会去委派给子类加载器,因为双亲委派是“向上委派”,不是“向下”,这就回导致找不到
jar的驱动类
mysql
通过线程上下文类加载器解决这个问题:
加载后,会通过启动类加载器委托应用程序类加载器加载依赖中的类
DriverManager
通过
DriverManager
机制获取
SPI
包中要加载的驱动
jar
全称为(
SPI),是
Service Provider Interface内置的一种服务提供发现机制。
JDK
的工作原理:
SPI在
路径下的
ClassPath文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。使用
META-INF/services加载实现类
ServiceLoader中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
SPI
模块化
OSGi
模块化框架存在同级之间的类加载器的委托加载。
OSGi
还使用类加载器实现了热部署的功能。热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。
OSGi
JDK9之后的类加载器
由于
引入了
JDK9的概念,类加载器在设计上发生了很多变化。从
module包文件中加载转变为从
jar文件中加载。
jmod
启动类加载器使用
编写,位于
Java
类中。Java中的
jdk.internal.loader.ClassLoaders
继承自
BootClassLoader
实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过
BuiltinClassLoader
代码获取到,返回的仍然是
java
,保持了统一
null
扩展类加载器被替换成了平台类加载器(
)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从
Platform Class Loader
变成
URLClassLoader
,
BuiltinClassLoader
实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。
BuiltinClassLoader
Java内存区域
运行时数据区
虚拟机在运行
Java
程序过程中管理的内存区域,称之为运行时数据区,其主要分为以下四类。
Java
程序计数器:每个线程会通过每个线程私有的程序计数器记录当前要执行的的字节码指令的地址(字节码的行号),程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
虚拟机栈和本地方法栈:虚拟机栈采用栈的数据结构来管理方法调用中的基本数据(局部变量、操作数等),每一个方法的调用使用一个栈帧来保存。堆:存放的是创建出来的对象,这也是最容易产生内存溢出的位置方法区:主要存放的是类的元信息,同时还保存了常量池
java
程序计数器
程序计数器
也叫
(Program Counter Register)
寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址。
PC
在解析阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。
程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。多线程执行情况下,
虚拟机通过程序计数器记录
Java
切换前解释执行到哪句指令并继续解释运行。内存溢出指的是程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。 因为每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。程序员无需对程序计数器做任何处理。
CPU
栈
虚拟机栈
Java
虚拟机栈
Java
采用栈的数据结构来管理方法调用中的基本数据,先进后出
(Java Virtual Machine Stack)
,每一个方法的调用使用一个栈帧
(First In Last Out)
来保存。
(Stack Frame)
虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈,代表该线程运行所需要的内存。每个栈由多个栈帧(
Java
)组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的方法虚拟机栈主要包括局部变量表(存放局部变量)、操作数栈(存放中间变量)、帧数据(动态链接、方法出口、异常表的引用)三大部分
frame
垃圾回收不涉及栈内存,当栈帧弹出后,内存就会释放栈内存未必越大越好,栈帧过大会导致线程数变少,例如,机器总内存为
,目前能活动的线程数则为
512m个,如果把栈内存改为
512,那么能活动的栈帧就会减半如果方法内局部变量没有逃离方法的作用范围,它是线程安全的;如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
2048k
虚拟机栈-局部变量表
Java
局部变量表的作用是在方法执行过程中存放所有局部变量。编译成字节码文件就可以确定局部变量表内容。
虚拟机通过局部变量表来控制局部变量能够访问的范围【起始
Java
和长度来控制】栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽
PC
,
(slot)
和
long
类型占用两个槽,其他类型占用一个槽。
double
实例方法中的序号为
的位置存放的是
0
,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。 为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。
this
Java虚拟机栈-操作数栈
操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。
Java虚拟机栈-帧数据
帧数据主要包含动态链接、方法出口、异常表的引用动态链接:当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池(方法区的一部分)中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
方法出口:方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址异常表:异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置
Java虚拟机栈-栈内存溢出
虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出。
Java
虚拟机栈内存溢出时会出现
Java
的错误如果不指定栈的大小,
StackOverflowError
将创建一个 具有默认大小的栈。大小取决于操作系统和计算机的体系结构。
JVM
本地方法栈
虚拟机栈存储了
Java
方法调用时的栈帧,而本地方法栈存储的是
Java
本地方法的栈帧。在
native
虚拟机中,
Hotspot
虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
Java
堆
一般
程序中堆内存是空间最大的一块内存区域,是一个线程共享的区域。主要用于保存对象实例,数组等栈上的局部变量表中,可以存放堆上对象的引用。静态变量(方法区)也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出
Java
错误。堆空间有三个需要关注的值,
Out Of Memory
。
used, total, max
指的是当前已使用的堆内存,
used
是
total
虚拟机已经分配的可用堆内存,
java
是
max
虚拟机可以分配的最大堆内存。当
java
不够用时,会继续分配空间,直到
total
如果不设置任何的虚拟机参数,
total=max
默认是系统内存的
max
,
1/4
默认是系统内存的
total
。在实际应用中一般都需要设置
1/64
和
total
的值。
max
方法区
方法区是存放基础信息的位置,线程共享,主要包括类的元信息,运行时常量池,字符串常量池
元信息
方法区是存储每个类的基本信息(元信息),一般称之为
对象。在类的加载阶段完成。
InstanceKlass
运行时常量池
方法区除了存储类的元信息之外,还存放了运行时常量池。运行时常量池中存放字节码中的常量池内容。常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(如字符串常量、整数常量、浮点数等)等信息字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当类被加载,其常量池信息就会加载到内存中,并把里面的符号地址变为真实地址,可以通过内存地址快速的定位到常量池中的内容,这种加载后的常量池称为运行时常量池。
将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数
JDK7
来控制。
-XX:MaxPermSize=值
将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用
JDK8
将元空间最大大小进行限制。
-XX:MaxMetaspaceSize=值
字符串常量池
字符串常量池存储在代码中定义的常量字符串内容。比如
这个
“123”
就会被放入字符串常量池。
123
早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整, 将字符串常量池和运行时常量池做了拆分。
及之前的版本中,静态变量是存放在方法区中的,也就是永久代。
JDK6
及之后的版本中,静态变量是存放在堆中的
JDK7
对象中,脱离了永久代。
Class
直接内存
直接内存
并不在《Java虚拟机规范》中存在,所以并不属于
(Direct Memory)
运行时的内存区域,而是虚拟机的系统内存。 在
Java
中引入了
JDK 1.4
机制,使用了直接内存,主要为了解决以下两个问题:
NIO
堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
Java
操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到
IO
堆中。
Java
现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少数据复制的开销。写文件也是类似思路。
要创建直接内存上的数据,可以使用
。
ByteBuffer
栈上存储
数据实际大小
数据在
中占用的空间大小
JVM
虚拟机采用的是空间换时间方案,在栈上不存储具体的类型,只根据
Java槽进行数据的处理,浪费了一些内存空间但是避免不同数据类型不同处理方式带来的时间开销。同时,像
slot型在
long位系统中占用
64个slot,使用了
2字节空间,但实际上在
16虚拟机中,它的高
Hotspot个字节没有使用,这样就满足了
8型使用
long个字节的需要
8
堆中的数据加载到栈上
堆中的数据加载到栈上,由于栈上的空间大于或者等于堆上的空间,所以直接处理但是需要注意下符号位。
、
boolean
为无符号,低位复制,高位补
char
0
、
byte
为有符号,低位复制,高位非负则补
short
,负则补
0
1
栈中的数据要保存到堆上
栈中数据要保存到堆上,
、
byte
、
char
由于堆上存储空间较小,要将高位去掉。
short
比较特殊,只取低位的最后一位保存
boolean
堆上存储
对象在堆中的内存布局
标记字段
标记字段相对比较复杂。在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的, 同时在
位(又分为是否开启指针压缩)、
64
位虚拟机中的布局都不同。以
32
位开启指针压缩为例:
**64**
元数据指针
指针指向方法区中保存的
对象
InstanceKlass
指针压缩,保存对象的位置顺序,而不直接保存对象的真实地址,因此对象需要内存对齐,使其大小为
字节的整数倍。
8
寻址大小仅仅能支持
的
2次方个字节(
35,如果超过
32GB指针压缩会自动关闭)。不用压缩指针,应该是
32GB的
2次方
64,用了压缩指针就变成了
=16EB(字节)
8
= 2的3次方 * 2的32次方 = 2的35次方
内存对齐
内存对齐主要目的是为了解决并发情况下
缓存失效的问题:内存对齐之后,同一个缓存行中不会出现不同对象的属性。在并发情况下,如果让
CPU
对象一个缓存行失效,是不会影响到
A
对象的缓存行的。
B
通过内存对齐,一个对象就会占用整个
缓存行,而不会和另一个对象共用一个缓存行
CPU
方法调用与异常捕获
方法调用
方法调用本质是通过字节码指令的执行,在栈上创建栈帧,并执行调用方法中的字节码执行。以
开头的字节码指令的作用是执行方法的调用
invoke
一共有五个字节码指令可以执行方法调用:
:调用静态方法
invokestatic
: 调用对象的
invokespecial
方法、构造方法,以及使用
private
关键字调用父类实例的方法、构造方法, 以及所实现接口的默认方法
super
:调用对象的非
invokevirtual
方法
private
:调用接口对象的方法
invokeinterface
:用于调用动态方法,主要应用于
invokedynamic
表达式中,机制极为复杂了解即可。
lambda
指令执行时,需要找到方法区中
Invoke
中保存的方法相关的字节码信息。但是方法区中有很多类, 每一个类又包含很多个方法,怎么精确地定位到方法的位置呢?
instanceKlass
静态绑定
编译期间,
指令会携带一个参数符号引用,引用到常量池中的方法定义。方法定义中包含了类名+ 方法名+返回值+参数。在方法第一次调用时,这些符号引用就会被替换成内存地址的直接引用,这种方式称之为静态绑定。 静态绑定适用于处理静态方法、私有方法、或者使用
invoke
修饰的方法,因为这些方法不能被继承之后重写。
final
动态绑定
对于非
、非
static
、非
private
的方法,有可能存在子类重写方法,那么就需要通过动态绑定来完成方法地址绑定的工作。
final
因为 Java 的多态特性要求在运行时决定到底调用的是父类的方法,还是子类重写后的方法,所以必须用动态绑定来支持这种“根据对象真实类型决定方法实现”的机制
动态绑定是基于方法表来完成的,
使用了虚方法表
invokevirtual
,
(vtable)
使用了接口方法表
invokeinterface
,整体思路类似。所以接下来使用
(itable)
和虚方法表来解释整个过程。 每个类中都有一个虚方法表,本质上它是一个数组,记录了方法的地址。子类方法表中包含父类方法表中的所有方法; 子类如果重写了父类方法,则使用自己类中方法的地址进行替换。
invokevirtual
产生
调用时,先根据对象头中的类型指针找到方法区中
invokevirtual
对象,获得虚方法表。再根据虚方法表找到对应的对方,获得方法的地址,最后调用方法。
InstanceClass
异常捕获
在
中,程序遇到异常时会向外抛出,此时可以使用
Java
捕获异常的方式将异常捕获并继续让程序按程序员设计好的方式运行。异常捕获机制的实现,需要借助于编译时生成的异常表。
try-catch
异常表在编译期生成,存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
程序运行中触发异常时,
虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,
Java
虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。
Java
如果匹配,跳转到“跳转
”对应的字节码位置。 如果遍历完都不能匹配,说明异常无法在当前方法执行时被捕获,此方法栈帧直接弹出,在上一层的栈帧中进行异常捕获的查询。
PC
的处理方式就相对比较复杂一点了,分为以下几个步骤:
finally
中的字节码指令会插入到
finally
和
try
代码块中,保证在
catch
和
try
执行后一定会去执行
catch
中的代码如果抛出的异常范围超过了
finally
,比如
Exception
或者
<font>Error</font>
,此时也要执行
Throwable
,所以异常表中增加了两个条目。覆盖了
finally
和
try
两段字节码指令的范围,
catch
代表可以捕获所有种类的异常。在最后需要将异常继续向外抛出。
any
自动垃圾回收
在
这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
C/C++
中为了简化对象的释放,引入了自动的垃圾回收
Java
机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。
(Garbage Collection简称GC)
方法区的回收
判定一个类可以被卸载。需要同时满足下面三个条件:
此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。加载该类的类加载器已经被回收。该类对应的
对象没有在任何地方被引用。
java.lang.Class
如果需要手动触发垃圾回收,可以调用
方法。调用
System.gc()方法并不一定会立即回收垃圾,仅仅是向
System.gc()虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收
Java虚拟机会自行判断。
Java
堆的回收
引用计数法和可达性分析法
中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
Java
引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加
,取消引用时减
1
。
1
缺点:
每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响存在循环引用问题,所谓循环引用就是当
引用
A
,
B
同时引用
B
时会出现对象无法回收的问题。
A
可达性分析法
使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象
Java
和普通对象,对象与对象之间存在引用关系。下图中
(GC Root)
到
A
再到
B
和
C
,形成了一个引用链,可达性分析算法指的是如果从某个到
D
对象是可达的,对象就不可被回收。
GC Root
被称之为
对象的对象
GC Root
线程
对象。系统类加载器加载的
Thread
对象。监视器对象,用来保存同步锁
java.lang.Class
关键字持有的对象。本地方法调用时使用的全局对象
synchronized
五种对象引用
强引用
可达性算法中描述的对象引用,一般指的是强引用,即是
对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。
GC Root
软引用
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。在
版之后提供了
JDK 1.2
类来实现软引用,软引用常用于缓存中。软引用执行过程
SoftReference
将对象使用软引用包装起来,
。内存不足时,虚拟机尝试进行垃圾回收。如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。如果依然内存不足,抛出
new SoftReference<对象类型>(对象)
异常。
Out Of Memory
byte[] bytes =new byte[1024*1024*100];
// 创建了软引用对象
SoftReference<byte[]>softReference = new SoftReference<byte[]>(bytes);
// 去除强引用
bytes = null;
// 获取软引用中的对象
softReference.get();
提供了一套队列机制:
SoftReference
软引用创建时,通过构造器传入引用队列 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列 通过代码遍历引用队列,将
的强引用删除
SoftReference
// 引用队列
ArrayList<SoftReference> softReferences = new ArrayList<>();
// 软引用
ReferenceQueue<bytel[]> queues = new ReferenceQueue<byte[]>();
// 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
for(int i = 0; i < 10; i++) {
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference studentRef = new SoftReference<byte[]>(bytes,queues);
softReferences.add(studentRef);
}
SoftReference<byte[]> studentRef = null;
int count = 0;
while((ref=(SoftReference<byte[]>) queues.poll())!= null) {
count ++;
}
System.out.println(count);
软引用也可以使用继承自
类的方式来实现,
SoftReference
类就是一个软引用对象。 通过构造器传入软引用包含的对象,以及引用队列
StudentRef
// 继承软引用
class StudentRef extends SoftReference<Student>{
// 构造器传入对象
public StudentRef(Student em, ReferenceQueue<Student> q){
super(em, q);
}
}
弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK 1.2版之后提供了
类来实现弱引用,弱引用主要在
WeakReference
中使用。弱引用对象本身也可以使用引用队列进行回收。
ThreadLocal
byte[]bytes =new byte[1024*1024*100];
WeakReference<bytel]> weakReference = new WeakReference<byte[]>(bytes);
虚引用
虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。
中使用
Java
实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
PhantomReference
终结器引用
终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在
类中的引用队列中,在稍后由一条由
Finalizer
线程从队列中获取对象,然后执行对象的
FinalizerThread
方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在
finalize
方法中再将自身对象使用强引用关联上,但是不建议这样做
finalize
垃圾回收算法
找到内存中存活的对象释放不再存活对象的内存,使得程序能再次利用这部分空间
垃圾回收算法的评价标准
垃圾回收过程会通过单独的
Java
线程来完成,但是不管使用哪一种
GC
算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为
GC
简称
Stop The World
,如果
STW
时间过长会影响用户的使用。
STW
吞吐量
吞吐量指的是
用于执行用户代码的时间与
CPU
总执行时间的比值,即吞吐量 = 执行用户代码时间 /
CPU
(执行用户代码时间 +
时间)。吞吐量数值越高,垃圾回收的效率就越高。
GC
最大暂停时间
最大暂停时间指的是所有在垃圾回收过程中的
时间最大值。最大暂停时间越短,用户使用系统时受到的影响就越短。
STW
堆使用效率
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算
法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。
标记清除算法
标记阶段,将所有存活的对象进行标记。
中使用可达性分析算法,从
Java
开始通过引用链遍历出所有存活对象。清除阶段,从内存中删除没有被标记也就是非存活对象。
GC Root
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。缺点:
由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法
准备两块空间
空间和
From
空间,每次在对象分配阶段,只能使用其中一块空间(
To
空间)在垃圾回收
From
阶段,将
GC
中存活对象复制到
From
空间。清理
To
空间,将两块空间的
From
和
From
名字互换
To
优点:
吞吐量高:复制算法只需要遍历一次存活对象,复制到
空间即可 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入
To
空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
To
缺点:内存使用效率低,每次只能让一半的内存空间来为创建对象使用
标记整理算法
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
标记阶段,将所有存活对象进行标记。
中使用可达性分析算法,从
Java
开始通过引用链遍历出
GC Root
所有存活对象。
整理阶段,将存活对象移动到堆的一端。清理掉非存活对象的内存空间。
优点:
内存使用效率高: 整个堆内存都可以使用,不会像复制算法只能使用半个堆内存 不会发生碎片化: 在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
缺点:整理阶段的效率不高
分代垃圾回收算法
分代垃圾回收算法会将上述描述的垃圾回收算法组合进行使用,分代垃圾回收将整个内存区域划分为年轻代和老年代
分代回收时,创建出来的对象,首先会被放入
伊甸园区。 随着对象在
Eden
区越来越多,如果
Eden
区满,新创建的对象已经无法放入,就会触发年轻代的
Eden
,称为
GC
或者
Minor GC
。
Young GC
会把
Minor GC
中和
Eden
需要回收的对象回收,把没有回收的对象放入
From
区。
To
接下来,
会变成
S0
区,
To
变成
S1
区。当
From
区满时再放入对象,依然会发生
Eden
。 此时会回收
Minor GC
区和
eden
中的对象,并把
S1(from)
和
eden
区中剩余的对象放入
from
。每次
S0
中都会为对象记录他的年龄,初始值为
Minor GC
,每次
0
完加
GC
。
1
如果
后对象的年龄达到阈值(最大
Minor GC
,默认值和垃圾回收器有关),对象会被晋升至老年代。 当老年代中空间不足,无法放入新的对象时,先尝试
15
,如果还是不足,就会触发
minor gc
,其会对整个堆进行垃圾回收。 如果
Full GC
依然无法回收掉老年代的对象,那么当对象继续放入老年代时,会抛出
Full GC
异常。
Out Of Memory
垃圾回收器
分代
算法将堆分成年轻代和老年代主要原因有:
GC
可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高 分代的设计中允许只回收新生代
,如果能满足对象分配的要求就不需要再对整个堆进行回收
(minor gc)
,
(full gc)
时间就会减少
STW
由于垃圾回收器分为年轻代和老年代,除了
之外其他垃圾回收器必须成对组合进行使用。
G1
串行垃圾回收器
年轻代-
垃圾回收器
Serial
是一种单线程串行回收年轻代的垃圾回收器。
Serial
使用复制算法实现优点:单
处理器下吞吐量非常出色缺点:多
CPU
下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待适用场景:
CPU
编写的客户端程序或者硬件配置有限的场景
Java
老年代-
垃圾回收器
SerialOld
是
SerialOld
垃圾回收器的老年代版本,采用单线程串行回收
Serial
新生代、老年代都使用串行回收器。
-XX:+UseSerialGC
使用标记-整理算法实现优点:单
处理器下吞吐量非常出色缺点:多
CPU
下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待适用场景: 与
CPU
垃圾回收器搭配使用,或者在
Serial
特殊情况下使用
CMS
并行垃圾回收器
年轻代-
垃圾回收器
ParNew
垃圾回收器本质上是对
ParNew
在多
Serial
下的优化,使用多线程进行垃圾回收
CPU
新生代使用
-XX:+UseParNewGC回收器, 老年代使用串行回收器
ParNew
使用复制算法实现优点:多
处理器下停顿时间较短 缺点:吞吐量和停顿时间不如
CPU
, 所以在
G1
之后不建议使用 适用场景:
JDK9
及之前的版本中,与
JDK8
老年代垃圾回收器搭配使用
CMS
老年代-
垃圾回收器
CMS(Concurrent Mark Sweep)
垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
CMS
参数:
XX:+UseConcMarkSweepGC
使用标记清除算法实现优点:系统由于垃圾回收出现的停顿时间较短,用户体验好 缺点:
使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,
CMS
会在
CMS
时进行碎片的整理。 这样会导致用户线程暂停,可以使用
Full GC
参数(默认0)调整
-XX:CMSFullGCsBeforeCompaction=N
次
N
之后再整理。无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。如果老年代内存不足无法分配对象,
Full GC
就会退化成
CMS
单线程回收老年代。
Serial Old
适用场景:大型的互联网系统中用户请求数据量大、频率高的场景比如订单接口、商品接口等
执行步骤:
1. 初始标记,用极短的时间标记出`GC Roots`能直接关联到的对象。
2. 并发标记, 标记所有的对象,用户线程不需要暂停。
3. 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
4. 并发清理,清理死亡的对象,用户线程不需要暂停。
年轻代
垃圾回收器
-Parallel Scavenge
是
Parallel Scavenge
默认的年轻代垃圾回收器, 多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。
JDK8
使用复制算法实现优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数 缺点:不能保证单次的停顿时间 适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象比如:大数据的处理,大文件导出
老年代
垃圾回收器
-Parallel Old
是为
Parallel Old
收集器设计的老年代版本,利用多线程并发收集。
Parallel Scavenge
参数:
或-XX:+UseParallelOldGC可以用
-XX:+UseParallelGC这种组合。
Parallel Scavenge + Parallel Old
使用标记-整理算法实现优点:并发收集,在多核
下效率较高 缺点:暂定时间会比较长适用场景:与
CPU
配套使用
Parallel Scavenge
允许手动设置最大暂停时间和吞吐量。
Parallel Scavenge
官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
Oracle
最大暂停时间:-XX:MaxGCPauseMillis=n 设置每次垃圾回收时的最大停顿毫秒数吞吐量:-XX:GCTimeRatio=n 设置吞吐量为n(用户线程执行时间 = n/n + 1)自动调整内存大小:-XX:+UseAdaptiveSizePolicy设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小
G1垃圾回收器
之后默认的垃圾回收器是
JDK9
垃圾回收器。
G1(Garbage First)
关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。
Parallel Scavenge
关注暂停时间,但是吞吐量方面会下降。
CMS
设计目标就是将上述两种垃圾回收器的优点融合:
G1
支持巨大的堆空间回收,并有较高的吞吐量。 支持多
并行垃圾回收。 允许用户设置最大暂停时间。采用复制算法
CPU
的整个堆会被划分成多个大小相等的区域,称之为区
G1
,区不要求是连续的。分为
Region
、
Eden
、
Survivor
、
Old
区,其中
humongous
区专门为大对象准备。
humongous
的大小通过堆空间大小
Region
计算得到,也可以通过参数
/2048
指定(其中
-XX:G1HeapRegionSize=32m
指定
32m
大小为
region
),
32M
必须是
Region size
的指数幂,取值范围从
2
到
1M
。
32M
垃圾回收分为三个阶段
G1
年轻代回收并发标记混合回收
执行流程
新创建的对象会存放在
区。当
Eden
判断年轻代区不足(
G1
默认
max
),无法分配对象时需要回收时会执行
60%
。标记出
Young GC
和
Eden
区域中的存活对象根据配置最大暂停时间选择某些区域将存活对象复制到一个新的
Survivor
区中(年龄
Survivor
),清空这些区域,该过程需要暂停用户线程。
+1
后续
时与之前相同,只不过
Young GC
区中存活对象会被搬运到另一个
Survivor
区。 当某个存活对象的年龄到达阈值(默认
Survivor
),将被放入老年代。
15
部分对象如果大小超过
的一半,会直接放入老年代,这类老年代被称为
Region
区。比如堆内存是
Humongous
,每个
4G
是
Region
,只要一个大对象超过了
2M
就被放入
1M
区,如果对象过大会横跨多个
Humongous
。
Region
多次回收之后,会出现很多
老年代区,此时总堆占有率达到阈值时(
Old
默认
XX:InitiatingHeapOccupancyPercent
)会触发混合回收
45%
。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。此过程无需暂停用户线程
MixedGC
混合回收分为:初始标记(
)、并发标记(
initial mark
)、最终标记(
concurrent mark
或者
remark
)、并发清理(
Finalize Marking
)
cleanup
对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是
G1
(
G1
)名称的由来。
Garbage first
参数1:
打开
-XX:+UseG1GC的开关,
G1之后默认不需要打开
JDK9参数2:
最大暂停的时间
-XX:MaxGCPauseMillis=毫秒值
使用年轻代+老年代、复制算法实现 优点: 对比较大的堆如超过
的堆回收时,延迟可控不会产生内存碎片并发标记的
6G
算法效率高 缺点:
SATB
之前还不够成熟 适用场景:
JDK8
最新版本、
JDK8
之后建议默认使用
JDK9
内存调优
内存溢出和内存泄漏
+ 内存泄漏(`memory leak`):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上, 这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
+ 内存泄漏绝大多数情况都是由**堆内存泄漏**引起的,所以后续没有特别说明则讨论的都是堆内存泄漏。
监控内存
命令是
top
下用来查看系统信息的一个命令,它提供给我们去实时地去查看系统的资源,比如执行时的进程、线程和系统参数等信息。
linux
是多功能合一的
VisualVM
故障排除工具并且他是一款可视化工具,整合了命令行
Java
工具和轻量级分析功能,功能非常强大。
JDK
是一款线上监控诊断产品,通过全局视角实时查看应用
Arthas
、内存、
load
、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
gc
是企业中运维常用的监控方案,其中
Prometheus+Grafana
用来采集系统或者应用的相关数据,同时具备告警功能。
Prometheus
可以将
Grafana
采集到的数据以可视化的方式进行展示。
Prometheus
内存泄漏原因:
和
equals()
:不正确的
hashCode()
和
equals()
实现导致内存泄漏
hashCode()
在定义新类时没有重写正确的
和
equals()
方法。在使用
hashCode()
的场景下,如果使用这个类对象作为
HashMap
,
key
在判断
HashMap
是否已经存在时会使用这些方法,如果重写方式不正确,会导致相同的数据被保存多份。
key
内部类引用外部类:非静态的内部类和匿名内部类的错误使用导致内存泄漏
非静态的内部类默认持有外部类, 尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者。
的使用:由于线程池中的线程不被回收导致的
ThreadLocal
内存泄漏
ThreadLocal
的
String
方法:由于
intern
中的字符串常量池位于永久代,
JDK6
被大量调用并保存产生的内存泄漏
intern
通过静态字段保存对象:大量的数据在静态变量中被引用,但是不再使用,成为了内存泄漏
尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为
。使用单例模式时,尽量使用懒加载,而不是立即加载。
null
的
Spring
中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。
Bean
并发请求问题:并发请求问题指的是用户通过发送请求向
应用获取数据,正常情况下
Java
应用将数据返回之后,这部分数据就 可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于 内存中,最终超过了内存的上限,导致内存溢出。
Java
使用
软件可以进行并发请求测试。
Apache Jmeter
是一款开源的测试软件,使用
Apache Jmeter
语言编写,最初是为了测试
Java
程序,目前已经发展成支持数据库、消息队列、邮件协议等不同类型内容的测试工具。步骤:
Web
1. 安装
软件,添加线程组。
Jmeter
2. 在线程组中增加
请求,添加随机参数。
Http
3. 在线程组中添加监听器 – 聚合报告,用来展示最终结果。
4. 启动程序,运行线程组并观察程序是否出现内存溢出。
诊断原因
内存快照
当堆内存溢出时,需在堆内存溢出时将整个堆内存保存下来,生成内存快照(
)文件。生成内存快照的Java虚拟机参数:
Heap Profile
:发生
-XX:+HeapDumpOnOutOfMemoryError
错误时,自动生成
OutOfMemoryError
内存快照文件。
hprof
:指定
-XX:HeapDumpPath=
文件的输出路径。使用
hprof
打开
MAT
文件,并选择内存泄漏检测功能,
hprof
会自行根据内存快照中保存的数据分析内存泄漏的根源。导出运行中系统的内存快照:
MAT
通过
自带的
JDK
命令导出,格式:
jmap
通过
jmap -dump:live,format=b,file=文件路径和文件名进程ID
的
arthas
命令导出,格式:
heapdump
heapdump --live 文件路径和文件名
GC调优
性能优化
JIT即时编译器
在
中,
Java
即时编译器是一项用来提升应用程序代码执行效率的技术。字节码指令被
JIT
虚拟机解释执行,如果有一些指令执行频率高,称之为热点代码,这些字节码指令则被
Java
即时编译器编译成机器码同时进行一些优化,最后保存在内存中,将来执行时直接读取就可以运行在计算机硬件上了。
JIT
编译效率比
C1
快,但是优化效果不如
C2
。所以
C2
适合优化一些执行时间较短的代码,
C1
适合优化服务端程序中长期执行的代码。
C2
之后,采用了分层编译的方式,在
JDK7
中
JVM
和
C1
会一同发挥作用,分层编译将整个优化级别分为
C2
个等级
5
即时编译器和
C1
即时编译器都有独立的线程去进行处理,内部会保存一个队列,队列中存放需要编译的任务。 一般即时编译器是针对方法级别来进行优化的,当然也有对循环进行优化的设计。
C2
方法内联
方法体中的字节码指令直接复制到调用方的字节码指令中,节省了创建栈帧的开销。
内联有一定的限制:
方法编译之后的字节码指令总大小 < 35字节,可以直接内联。(通过-XX:MaxInlineSize=值控制)方法编译之后的字节码指令总大小 < 325字节,并且是一个热方法。(通过-XX:FreqInlineSize=值控制)方法编译生成的机器码不能大于1000字节。(通过-XX:InlineSmallCode=值 控制)一个接口的实现必须小于3个,如果大于三个就不会发生内联。
逃逸分析
逃逸分析指的是如果
发现在方法内创建的对象不会被外部引用,那么就可以采用锁消除、标量替换等方式进行优化。
JIT
锁消除: 如果对象被判断不会逃逸出去,那么在对象就不存在并发访问问题,对象上的锁处理都不会执行, 从而提高性能。
标量替换:在
虚拟机中,对象中的基本数据类型称为标量,引用的其他对象称为聚合量。标量替换指的是如果方法中的对象不会逃逸,那么其中的标量就可以直接在栈上分配。
Java
会直接不创建对象,直接对标量进行操作
JVM
内存泄漏
中有三块区域会导致内存泄漏
<font>JVM</font>
方法区:
堆空间:
StackOverFlowError
虚拟机栈:
OutOfMemoryError:Metaspace
OutOfMemoryError:java heap space
排查思路
获取堆内存快照
dump
使用
命令获取运行中程序的
jmap
文件:
dump
使用
jmap -dump:format=b, file=heap.hprof pid
参数获取
vm
文件:
dump
、
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps
通过
分析
VisualVM
文件查看堆信息的情况,可以大概定位内存溢出的代码位置找到对应代码,通过上下文情况进行修改
dump