JVM虚拟机

内容分享2天前发布
0 0 0

JVM概述

JVM虚拟机


JVM
的主要功能

解释和运行:对字节码文件中的指令,实时地解释成机器码让计算机执行内存管理:自动为对象、方法等分配内存;自动的垃圾回收机制,回收不再使用的对象即时编译:对热点代码进行优化,提升执行效率对热点代码(短时且执行多次)翻译为机器码,并保存在内存中,下次使用直接调用


JVM
组成

JVM虚拟机


JVM
通过类加载器,将类和接口的字节码文件加载到运行时数据区域执行引擎会将字节码文件中的指令解释成机器码,用即时编译器优化性能,通过调用本地接口执行类和接口同时本地接口自身也会创建类和接口在
JVM
运行时数据区域

字节码文件

字节码文件的组成

基本信息

字节码文件保存了源码编译后的内容,以二进制的方式存储

基本信息存储魔数、**字节码文件对应的
Java
版本号, 访问标识 (
public final
等等),父类和接口

魔数

文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。
java
字节码文件中,将文件头称为魔数。

JVM虚拟机

主副版本号

主副版本号指的是编译字节码文件的
JDK
版本号,主版本号用来标识大版本号,
JDK1.0-1.1
使用了
45.0-45.3

JDK1.2

46
之后每升级一个大版本就加
1
;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。 版本号的作用主要是判断当前字节码的版本和运行时的
JDK
是否兼容

JVM虚拟机

常量池

保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用,避免相同内容重复定义,节省空间。

常量池中的数据都有一个编号,编号从
1
开始。在字段或者字节码指令中通过编号快速找到对应的数据。

字节码指令中通过编号引用到常量池的过程称之为符号引用。

方法

存储当前类或接口声明的方法信息的字节码指令

字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的
Code
属性中。操作数栈:临时存放数据的地方,局部变量表是存放方法中的局部变量的位置。局部变量表数组:局部变量存放的位置JVM虚拟机

类的生命周期

JVM虚拟机

加载

加载
(Loading)
阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。 程序员可以使用
Java
代码拓展的不同的渠道
本地文件:磁盘上的字节码文件动态代理生成:程序运行时使用动态代理生成
类加载器在加载完类之后,
Java
虚拟机将字节码中的信息保存内存的方法区中。生成
InstanceKlass
对象,保存类的所有信息,里边还包含实现特定功能,比如多态的信息。

JVM虚拟机

同时,
Java
虚拟机会在堆中生成与方法区中数据类似的
java.lang.Class
对象。 作用是在
Java
代码中去获取类的信息以及存储静态字段的数据
JDK8
及之后)。

JVM虚拟机

对于开发者来说,只需要访问堆中的
Class
对象而不需要访问方法区中所有信息。这样
Java
虚拟机就能很好地控制开发者访问数据的范围

连接

![](https://i-blog.csdnimg.cn/img_convert/4c33d958703b6c3c2c51beca23ce374d.png)

验证

连接
(Linking)
阶段的第一个环节是验证,验证的主要目的是检测
Java
字节码文件是否遵守了《
Java
虚拟机规范》中的约束。这个阶段一般不需要程序员参与。

文件格式验证,比如文件是否以
0xCAFEBABE
开头,主次版本号是否满足当前
Java
虚拟机版本要求。元信息验证,例如类必须有父类(
super
不能为空)。验证程序执行指令的语义是否正确符号引用验证,例如是否访问了其他类中
private
的方法等

准备

准备阶段为静态变量
(static)
分配内存并设置初始值, 而每一种基本数据类型和引用数据类型都有其初始值,并不是程序员代码赋的初始值

JVM虚拟机


final
修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值

解析

解析阶段主要是将常量池中的符号引用替换为直接引用。直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。

初始化

初始化阶段会执行静态代码块中的代码,并为静态变量赋值。初始化阶段会执行字节码文件中
clinit
部分(类的初始化)的字节码指令。[
clinit
方法中的执行顺序与
Java
中编写的顺序是一致的。]

JVM虚拟机

以下几种方式会导致类的初始化:
访问一个类的静态变量或静态方法,注意变量是
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
程序启动

JVM虚拟机

本地接口
JNI

Java Native Interface
缩写,允许
Java
调用其他语言编写的方法。在
hotspot
类加载器中,主要用于调用
Java
虚拟机中的方法,这些方法使用
C++
编写。 加载阶段
获取字节码文件 生成方法区对象生成堆上
Class
对象

类加载器的分类


Java
代码中实现

JDK
中默认提供或自定义继承自抽象类
ClassLoader

Java虚拟机底层源码实现
源代码位于
Java
虚拟机的源码中,实现语言与虚拟机底层语言一致,比如
Hotspot
使用
C++
加载程序运行时的基础类

类加载器的设计
JDK8

8
之后的版本差别较大,
JDK8
及之前的版本中默认的类加载器有如下几种

JVM虚拟机

启动类加载器

启动类加载器(
Bootstrap ClassLoader
)是由
Hotspot
虚拟机提供的、使用
C++
编写的类加载器。用于加载
Java
中最核心的类。

扩展类加载器应用程序类加载器都是
JDK
中提供的、使用
Java
编写的类加载器。 是一个静态内部类,继承自
URLClassLoader
,具备通过目录或指定
jar
包将字节码文件加载到内存中。

JVM虚拟机

扩展类加载器

扩展类加载器(
Extension Class Loader
)是
JDK
中提供的、 使用
Java
编写的类加载器。 加载
Java
安装目录
/jre/lib/ext
下的类文件

应用程序加载器

加载
classpath
下的类文件

双亲委派机制

双亲委派机制的核心是解决一个类到底由谁来加载的问题

保证类加载的安全性,避免恶意代码替换
JDK
中的核心类库避免同一个类被多次加载

每个
Java
实现的类加载器中保存了一个成员变量叫“父”(
Parent
)类加载器,可以理解为它的上级, 并不是继承关系。 当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。向上查找如果已经加载过,就直接返回
Class
对象,加载过程结束。这样就能避免一个类重复加载。若未加载,则向下加载,找该类在哪个加载器的路径中。

JVM虚拟机

应用程序类加载器的
parent
父类加载器是扩展类加载器,而扩展类加载器的
parent
是空(因为启动类加载器由
C++
编写,无法直接获取),但在代码逻辑上,扩展类加载器依会把启动类加载器当成父类加载器处理。启动类加载器使用
C++
编写,没有父类加载器。

打破双亲委派机制

自定义类加载器

一个
Tomcat
程序中是可以运行多个
Web
应用的,如果这两个应用中出现了相同限定名的类,比如
Servlet
类,
Tomcat
要保证这两个类都能加载并且它们应该是不同的类。

如果不打破双亲委派机制,当应用类加载器加载
Web
应用
1
中的
MyServlet
之后,
Web
应用
2
中相同限定名的
MyServlet
类就无法被加载了。


Tomcat
使用自定义类加载器来实现应用之间类的隔离。每一个应用有一个独立的类加载器加载对应的类。


ClassLoader
中包含了
4
个核心方法。双亲委派机制的核心代码就位于
loadClass
方法中。

JVM虚拟机


//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
//否则调用父类加载器的加载方法
if (parent != null) {
    c = parent.loadClass(name, false);
} else {
    c = findBootstrapClassOrNull(name);
}
//父类未加载,则由本类加载
if (c == null) {
    c = findClass(name);
}

打破双亲委派机制的核心就是将上述代码重新实现

自定义类加载器默认的父类加载器是应用程序类加载器

JVM虚拟机

两个自定义类加载器加载相同限定名的类不会冲突,在同一个
Java
虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。若只是为了拓展渠道加载类,正确的实现应该是实现一个自定义类加载器,重写其
findClass
方法,不破坏其双亲委派机制

线程上下文类加载器


JDBC
中使用
DriverManager
来管理项目中引入不同数据库的驱动,如
mysql
驱动等。
DriverManager
类位于
rt.jar
包中,由启动类加载器加载。而依赖中的
mysql
驱动对应的类,由应用程序类加载器来加载。


DriverManager
属于
rt.jar
是启动类加载器加载的。而用户
jar
包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。 也就是在启动类加载器加载的时候, 只会查找核心类库,不会去委派给子类加载器,因为双亲委派是“向上委派”,不是“向下”,这就回导致找不到
mysql
的驱动类

通过线程上下文类加载器解决这个问题:
DriverManager
加载后,会通过启动类加载器委托应用程序类加载器加载依赖中的类

JVM虚拟机


DriverManager
通过
SPI
机制获取
jar
包中要加载的驱动


SPI
全称为(
Service Provider Interface
),是
JDK
内置的一种服务提供发现机制。


SPI
的工作原理:


ClassPath
路径下的
META-INF/services
文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。使用
ServiceLoader
加载实现类
SPI
中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

JVM虚拟机


OSGi
模块化


OSGi
模块化框架存在同级之间的类加载器的委托加载。
OSGi
还使用类加载器实现了热部署的功能。热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。

JVM虚拟机

JDK9之后的类加载器

由于
JDK9
引入了
module
的概念,类加载器在设计上发生了很多变化。从
jar
包文件中加载转变为从
jmod
文件中加载。

启动类加载器使用
Java
编写,位于
jdk.internal.loader.ClassLoaders
类中。Java中的
BootClassLoader
继承自
BuiltinClassLoader
实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过
java
代码获取到,返回的仍然是
null
,保持了统一

JVM虚拟机

扩展类加载器被替换成了平台类加载器(
Platform Class Loader
)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从
URLClassLoader
变成
BuiltinClassLoader

BuiltinClassLoader
实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。

JVM虚拟机

Java内存区域

运行时数据区


Java
虚拟机在运行
Java
程序过程中管理的内存区域,称之为运行时数据区,其主要分为以下四类。

JVM虚拟机

程序计数器:每个线程会通过每个线程私有的程序计数器记录当前要执行的的字节码指令的地址(字节码的行号),程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
java
虚拟机栈和本地方法栈:虚拟机栈采用栈的数据结构来管理方法调用中的基本数据(局部变量、操作数等),每一个方法的调用使用一个栈帧来保存。:存放的是创建出来的对象,这也是最容易产生内存溢出的位置方法区:主要存放的是类的元信息,同时还保存了常量池

程序计数器

程序计数器
(Program Counter Register)
也叫
PC
寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址。

JVM虚拟机

解析阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。

JVM虚拟机

程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。多线程执行情况下,
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
类型占用两个槽,其他类型占用一个槽。

JVM虚拟机

实例方法中的序号为
0
的位置存放的是
this
,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。 为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。

Java虚拟机栈-操作数栈

操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。

Java虚拟机栈-帧数据

帧数据主要包含动态链接方法出口异常表的引用动态链接:当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池(方法区的一部分)中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。

JVM虚拟机

方法出口:方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址异常表:异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置

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
对象。在类的加载阶段完成。

JVM虚拟机

运行时常量池

方法区除了存储类的元信息之外,还存放了运行时常量池。运行时常量池中存放字节码中的常量池内容。常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(如字符串常量、整数常量、浮点数等)等信息字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当类被加载,其常量池信息就会加载到内存中,并把里面的符号地址变为真实地址,可以通过内存地址快速的定位到常量池中的内容,这种加载后的常量池称为运行时常量池
JDK7
将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数
-XX:MaxPermSize=值
来控制。
JDK8
将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用
-XX:MaxMetaspaceSize=值
将元空间最大大小进行限制。

字符串常量池

字符串常量池存储在代码中定义的常量字符串内容。比如
“123”
这个
123
就会被放入字符串常量池。

JVM虚拟机

早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整, 将字符串常量池和运行时常量池做了拆分。

JVM虚拟机


JDK6
及之前的版本中,静态变量是存放在方法区中的,也就是永久代。
JDK7
及之后的版本中,静态变量是存放在堆中的
Class
对象中,脱离了永久代。

直接内存

直接内存
(Direct Memory)
并不在《Java虚拟机规范》中存在,所以并不属于
Java
运行时的内存区域,而是虚拟机的系统内存。 在
JDK 1.4
中引入了
NIO
机制,使用了直接内存,主要为了解决以下两个问题:


Java
堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
IO
操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到
Java
堆中。

现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少数据复制的开销。写文件也是类似思路。

JVM虚拟机

要创建直接内存上的数据,可以使用
ByteBuffer

栈上存储

数据实际大小

JVM虚拟机

数据在
JVM
中占用的空间大小

JVM虚拟机


Java
虚拟机采用的是空间换时间方案,在栈上不存储具体的类型,只根据
slot
槽进行数据的处理,浪费了一些内存空间但是避免不同数据类型不同处理方式带来的时间开销。同时,像
long
型在
64
位系统中占用
2
个slot,使用了
16
字节空间,但实际上在
Hotspot
虚拟机中,它的高
8
个字节没有使用,这样就满足了
long
型使用
8
个字节的需要

堆中的数据加载到栈上

堆中的数据加载到栈上,由于栈上的空间大于或者等于堆上的空间,所以直接处理但是需要注意下符号位。


boolean

char
为无符号,低位复制,高位补
0

byte

short
为有符号,低位复制,高位非负则补
0
,负则补
1

栈中的数据要保存到堆上

栈中数据要保存到堆上,
byte

char

short
由于堆上存储空间较小,要将高位去掉。
boolean
比较特殊,只取低位的最后一位保存

堆上存储

对象在堆中的内存布局

JVM虚拟机

标记字段

标记字段相对比较复杂。在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的, 同时在
64
位(又分为是否开启指针压缩)、
32
位虚拟机中的布局都不同。
**64**
位开启指针压缩为例:

JVM虚拟机

元数据指针

指针指向方法区中保存的
InstanceKlass
对象

JVM虚拟机

指针压缩,保存对象的位置顺序,而不直接保存对象的真实地址,因此对象需要内存对齐,使其大小为
8
字节的整数倍。

寻址大小仅仅能支持
2

35
次方个字节(
32GB
,如果超过
32GB
指针压缩会自动关闭)。不用压缩指针,应该是
2

64
次方
=16EB
,用了压缩指针就变成了
8
(字节)
= 2的3次方 * 2的32次方 = 2的35次方

内存对齐

内存对齐主要目的是为了解决并发情况下
CPU
缓存失效的问题:内存对齐之后,同一个缓存行中不会出现不同对象的属性。在并发情况下,如果让
A
对象一个缓存行失效,是不会影响到
B
对象的缓存行的。

JVM虚拟机

通过内存对齐,一个对象就会占用整个
CPU
缓存行,而不会和另一个对象共用一个缓存行

方法调用与异常捕获

方法调用

方法调用本质是通过字节码指令的执行,在栈上创建栈帧,并执行调用方法中的字节码执行。以
invoke
开头的字节码指令的作用是执行方法的调用

一共有五个字节码指令可以执行方法调用:


invokestatic
:调用静态方法
invokespecial
: 调用对象的
private
方法、构造方法,以及使用
super
关键字调用父类实例的方法、构造方法, 以及所实现接口的默认方法
invokevirtual
:调用对象的非
private
方法
invokeinterface
:调用接口对象的方法
invokedynamic
:用于调用动态方法,主要应用于
lambda
表达式中,机制极为复杂了解即可。


Invoke
指令执行时,需要找到方法区中
instanceKlass
中保存的方法相关的字节码信息。但是方法区中有很多类, 每一个类又包含很多个方法,怎么精确地定位到方法的位置呢?

静态绑定

编译期间,
invoke
指令会携带一个参数符号引用,引用到常量池中的方法定义。方法定义中包含了类名+ 方法名+返回值+参数。在方法第一次调用时,这些符号引用就会被替换成内存地址的直接引用,这种方式称之为静态绑定。 静态绑定适用于处理静态方法、私有方法、或者使用
final
修饰的方法,因为这些方法不能被继承之后重写。

动态绑定

对于非
static
、非
private
、非
final
的方法,有可能存在子类重写方法,那么就需要通过动态绑定来完成方法地址绑定的工作。

因为 Java 的多态特性要求在运行时决定到底调用的是父类的方法,还是子类重写后的方法,所以必须用动态绑定来支持这种“根据对象真实类型决定方法实现”的机制

动态绑定是基于方法表来完成的,
invokevirtual
使用了虚方法表
(vtable)

invokeinterface
使用了接口方法表
(itable)
,整体思路类似。所以接下来使用
invokevirtual
和虚方法表来解释整个过程。 每个类中都有一个虚方法表,本质上它是一个数组,记录了方法的地址。子类方法表中包含父类方法表中的所有方法; 子类如果重写了父类方法,则使用自己类中方法的地址进行替换。

JVM虚拟机

产生
invokevirtual
调用时,先根据对象头中的类型指针找到方法区中
InstanceClass
对象,获得虚方法表。再根据虚方法表找到对应的对方,获得方法的地址,最后调用方法。

JVM虚拟机

异常捕获


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
对象是可达的,对象就不可被回收。

JVM虚拟机

被称之为
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
方法中再将自身对象使用强引用关联上,但是不建议这样做

垃圾回收算法

找到内存中存活的对象释放不再存活对象的内存,使得程序能再次利用这部分空间

JVM虚拟机

垃圾回收算法的评价标准


Java
垃圾回收过程会通过单独的
GC
线程来完成,但是不管使用哪一种
GC
算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为
Stop The World
简称
STW
,如果
STW
时间过长会影响用户的使用。

JVM虚拟机

吞吐量

吞吐量指的是
CPU
用于执行用户代码的时间与
CPU
总执行时间的比值,即吞吐量 = 执行用户代码时间 /

(执行用户代码时间 +
GC
时间)。吞吐量数值越高,垃圾回收的效率就越高。

最大暂停时间

最大暂停时间指的是所有在垃圾回收过程中的
STW
时间最大值。最大暂停时间越短,用户使用系统时受到的影响就越短。

堆使用效率

不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算

法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。

一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。

标记清除算法

标记阶段,将所有存活的对象进行标记。
Java
中使用可达性分析算法,从
GC Root
开始通过引用链遍历出所有存活对象。清除阶段,从内存中删除没有被标记也就是非存活对象。

优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。缺点:
由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。

复制算法

准备两块空间
From
空间和
To
空间,每次在对象分配阶段,只能使用其中一块空间(
From
空间)在垃圾回收
GC
阶段,将
From
中存活对象复制到
To
空间。清理
From
空间,将两块空间的
From

To
名字互换

优点:
吞吐量高:复制算法只需要遍历一次存活对象,复制到
To
空间即可 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入
To
空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
缺点:内存使用效率低,每次只能让一半的内存空间来为创建对象使用

标记整理算法

标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。

标记阶段,将所有存活对象进行标记。
Java
中使用可达性分析算法,从
GC Root
开始通过引用链遍历出

所有存活对象。

整理阶段,将存活对象移动到堆的一端。清理掉非存活对象的内存空间。

优点:
内存使用效率高: 整个堆内存都可以使用,不会像复制算法只能使用半个堆内存 不会发生碎片化: 在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
缺点:整理阶段的效率不高

分代垃圾回收算法

分代垃圾回收算法会将上述描述的垃圾回收算法组合进行使用,分代垃圾回收将整个内存区域划分为年轻代老年代

JVM虚拟机

分代回收时,创建出来的对象,首先会被放入
Eden
伊甸园区。 随着对象在
Eden
区越来越多,如果
Eden
区满,新创建的对象已经无法放入,就会触发年轻代的
GC
,称为
Minor GC
或者
Young GC

Minor GC
会把
Eden
中和
From
需要回收的对象回收,把没有回收的对象放入
To
区。

JVM虚拟机

接下来,
S0
会变成
To
区,
S1
变成
From
区。当
Eden
区满时再放入对象,依然会发生
Minor GC
。 此时会回收
eden
区和
S1(from)
中的对象,并把
eden

from
区中剩余的对象放入
S0
。每次
Minor GC
中都会为对象记录他的年龄,初始值为
0
,每次
GC
完加
1

JVM虚拟机

如果
Minor GC
后对象的年龄达到阈值(最大
15
,默认值和垃圾回收器有关),对象会被晋升至老年代。 当老年代中空间不足,无法放入新的对象时,先尝试
minor gc
,如果还是不足,就会触发
Full GC
,其会对整个堆进行垃圾回收。 如果
Full GC
依然无法回收掉老年代的对象,那么当对象继续放入老年代时,会抛出
Out Of Memory
异常。

垃圾回收器

分代
GC
算法将堆分成年轻代和老年代主要原因有:

可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高 分代的设计中允许只回收新生代
(minor gc)
,如果能满足对象分配的要求就不需要再对整个堆进行回收
(full gc)

STW
时间就会减少

由于垃圾回收器分为年轻代和老年代,除了
G1
之外其他垃圾回收器必须成对组合进行使用。

JVM虚拟机

串行垃圾回收器

年轻代-
Serial
垃圾回收器


Serial
是一种单线程串行回收年轻代的垃圾回收器。

JVM虚拟机

使用复制算法实现优点:单
CPU
处理器下吞吐量非常出色缺点:多
CPU
下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待适用场景:
Java
编写的客户端程序或者硬件配置有限的场景

老年代-
SerialOld
垃圾回收器


SerialOld

Serial
垃圾回收器的老年代版本,采用单线程串行回收


-XX:+UseSerialGC
新生代、老年代都使用串行回收器。

使用标记-整理算法实现优点:单
CPU
处理器下吞吐量非常出色缺点:多
CPU
下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待适用场景: 与
Serial
垃圾回收器搭配使用,或者在
CMS
特殊情况下使用

并行垃圾回收器

年轻代-
ParNew
垃圾回收器


ParNew
垃圾回收器本质上是对
Serial
在多
CPU
下的优化,使用多线程进行垃圾回收

JVM虚拟机


-XX:+UseParNewGC
新生代使用
ParNew
回收器, 老年代使用串行回收器

使用复制算法实现优点:多
CPU
处理器下停顿时间较短 缺点:吞吐量和停顿时间不如
G1
, 所以在
JDK9
之后不建议使用 适用场景:
JDK8
及之前的版本中,与
CMS
老年代垃圾回收器搭配使用

老年代-
CMS(Concurrent Mark Sweep)
垃圾回收器


CMS
垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。

JVM虚拟机

参数:
XX:+UseConcMarkSweepGC

使用标记清除算法实现优点:系统由于垃圾回收出现的停顿时间较短,用户体验好 缺点:

CMS
使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,
CMS
会在
Full GC
时进行碎片的整理。 这样会导致用户线程暂停,可以使用
-XX:CMSFullGCsBeforeCompaction=N
参数(默认0)调整
N

Full GC
之后再整理。无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。如果老年代内存不足无法分配对象,
CMS
就会退化成
Serial Old
单线程回收老年代。
适用场景:大型的互联网系统中用户请求数据量大、频率高的场景比如订单接口、商品接口等

执行步骤:


1. 初始标记,用极短的时间标记出`GC Roots`能直接关联到的对象。
2. 并发标记, 标记所有的对象,用户线程不需要暂停。
3. 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
4. 并发清理,清理死亡的对象,用户线程不需要暂停。

年轻代
-Parallel Scavenge
垃圾回收器


Parallel Scavenge

JDK8
默认的年轻代垃圾回收器, 多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。

JVM虚拟机

使用复制算法实现优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数 缺点:不能保证单次的停顿时间 适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象比如:大数据的处理,大文件导出

老年代
-Parallel Old
垃圾回收器


Parallel Old
是为
Parallel Scavenge
收集器设计的老年代版本,利用多线程并发收集。

参数:
-XX:+UseParallelGC
或-XX:+UseParallelOldGC可以用
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

JVM虚拟机


G1
垃圾回收分为三个阶段

年轻代回收并发标记混合回收

执行流程

新创建的对象会存放在
Eden
区。当
G1
判断年轻代区不足(
max
默认
60%
),无法分配对象时需要回收时会执行
Young GC
。标记出
Eden

Survivor
区域中的存活对象根据配置最大暂停时间选择某些区域将存活对象复制到一个新的
Survivor
区中(年龄
+1
),清空这些区域,该过程需要暂停用户线程。

JVM虚拟机

后续
Young GC
时与之前相同,只不过
Survivor
区中存活对象会被搬运到另一个
Survivor
区。 当某个存活对象的年龄到达阈值(默认
15
),将被放入老年代。

JVM虚拟机

部分对象如果大小超过
Region
的一半,会直接放入老年代,这类老年代被称为
Humongous
区。比如堆内存是
4G
,每个
Region

2M
,只要一个大对象超过了
1M
就被放入
Humongous
区,如果对象过大会横跨多个
Region

JVM虚拟机

多次回收之后,会出现很多
Old
老年代区,此时总堆占有率达到阈值时(
XX:InitiatingHeapOccupancyPercent
默认
45%
)会触发混合回收
MixedGC
。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。此过程无需暂停用户线程

JVM虚拟机

混合回收分为:初始标记(
initial mark
)、并发标记(
concurrent mark
)、最终标记(
remark
或者
Finalize Marking
)、并发清理(
cleanup

G1
对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是
G1

Garbage first
)名称的由来。

JVM虚拟机

JVM虚拟机

参数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. 启动程序,运行线程组并观察程序是否出现内存溢出。

诊断原因

内存快照

当堆内存溢出时,需在堆内存溢出时将整个堆内存保存下来,生成内存快照(
Heap Profile
)文件。生成内存快照的Java虚拟机参数:
-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
适合优化服务端程序中长期执行的代码。

JVM虚拟机


JDK7
之后,采用了分层编译的方式,在
JVM

C1

C2
会一同发挥作用,分层编译将整个优化级别分为
5
个等级

JVM虚拟机


C1
即时编译器和
C2
即时编译器都有独立的线程去进行处理,内部会保存一个队列,队列中存放需要编译的任务。 一般即时编译器是针对方法级别来进行优化的,当然也有对循环进行优化的设计。

JVM虚拟机

方法内联

方法体中的字节码指令直接复制到调用方的字节码指令中,节省了创建栈帧的开销。

内联有一定的限制:

方法编译之后的字节码指令总大小 < 35字节,可以直接内联。(通过-XX:MaxInlineSize=值控制)方法编译之后的字节码指令总大小 < 325字节,并且是一个热方法。(通过-XX:FreqInlineSize=值控制)方法编译生成的机器码不能大于1000字节。(通过-XX:InlineSmallCode=值 控制)一个接口的实现必须小于3个,如果大于三个就不会发生内联。

逃逸分析

逃逸分析指的是如果
JIT
发现在方法内创建的对象不会被外部引用,那么就可以采用锁消除、标量替换等方式进行优化。

锁消除: 如果对象被判断不会逃逸出去,那么在对象就不存在并发访问问题,对象上的锁处理都不会执行, 从而提高性能。

标量替换:在
Java
虚拟机中,对象中的基本数据类型称为标量,引用的其他对象称为聚合量。标量替换指的是如果方法中的对象不会逃逸,那么其中的标量就可以直接在栈上分配。

JVM虚拟机


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
文件查看堆信息的情况,可以大概定位内存溢出的代码位置找到对应代码,通过上下文情况进行修改

© 版权声明

相关文章

暂无评论

none
暂无评论...