一、基础部分
1、JVM是什么
jvm全称是Java Virtual Machine,即Java虚拟机,是一个编译在虚拟机上的程序,职责是运行Java字节码文件。
组成
2、JVM的三大核心功能
①内存管理:
自动为对象和方法等分配内存、自动的垃圾回收机制,回收不再使用的对象。
②解释执行虚拟机指令
对字节码文件中的指令实时的解释成机器码让计算机执行。
③即时编译
对热点代码进行优化,提高执行效率。
即时编译的目的是:对Java代码进行性能优化,支持跨平台性。
3、常见的JVM
常见的JVM有HotSpot、GraalVM、OpenJ9等,另外DragonWell龙井JDK也提供了一款功能增强
版的JVM。使用最广泛的是HotSpot虚拟机。
4、JVM的组成
类加载器ClassLoader:加载class字节码文件中的内容到内存中。
运行时数据区域(JVM管理的内存):负责管理JVM使用到的内存。
执行引擎:将字节码文件中的指令解释成机器码,同时使用即使编译器优化性能
5、字节码文件的组成
基本信息 |
魔数、字节码文件对应的Java版本号、访问标识(public final等等) 、父类、接口
|
常量池 |
保存了字符串常量、类或接口名、字段名;主要在字节码指令中使用
|
字段 | 当前类或接口中声明的字段信息 |
方法 |
当前类或接口声明的方法信息 ;字节码指令
|
属性 | 类的属性 |
对基本信息的解释:
魔数 | Java字节码文件中的头文件 魔数值固定为0xCAFEBABE。不会改变。 |
版本号 | 编译字节码文件的JDK版本号 副版本:5,6字节 主版本:7,8字节 作用:主要是是判断当前字节码的版本和运行时的jdk是否兼容 主版本号不兼容解决办法:降低第三方依赖的版本号或更换依赖 |
访问标识 |
标识是类(ACC_ 开头的常量)接口(ACC_INTERFACE)、注解、枚举、模块
标识:public、final、abstract(ACC_PUBLIC | ACC_FINAL|ACC_ABSTRACT)
|
类、父类、接口索引 | 通过这些索引来找到类、父类、接口信息 |
访问标识
对常量池的解释:
作用 | 避免相同内容重复定义,节省空间 |
常量池中的数据都有一个编号,编号从1开始。
主要存放两大类常量:字面量(声明为final的常量值)和符号引用(类和接口的全限定名)
|
八大基本数据类型(byte、char、double、float、int、long、short、boolean)
方法解释:
面试题:int i = 0; i = i++; 最终i的值是多少? 0。
面试题解释:i++先把0取出来放入临时的操作数栈中, 接下来对i进行加1,i变成了1,最后再将之前保存的临时值0放入i,最后i就变成了0。
作用 |
字节码中的方法区域是存放
字节码指令
的核心位置,
字节码指令的内容存放在方法的Code属性中。
|
操作数栈 | 临时存放数据的地方 |
局部变量 | 存放方法中局部变量的位置 |
二、类的生命周期
1、装载阶段(Loading)
将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
①类模板对象
所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
作用:使得Java能够更加灵活地处理不同类型的对象,提高了代码的可重用性和可扩展性。
②Class实例的位置
在Java中,.class文件加载到原空间(方法区)之后,会在堆内存中创建Java.lang.Class对象,来封装类位于方法区的数据结构,而对象的引用则存储在栈(stack)内存中。栈内存用于保存局部变量和方法调用的上下文信息。对象的引用指向堆内存中实际存储对象数据的位置。
Class类的构造方法是私有的,只有JVM能创建。
2、链接阶段(Linking)
①验证(Verification)
当类加载到系统中,就开始链接操作,验证阶段就是为了验证加载的字节码是否符合规范。
②准备(Preparation)
为类的静态变量分配内存,只给静态变量赋初始值。
注意:final修饰的基本数据类型的静态变量,会直接将代码中的值赋值;不会为实例对象分配初始化,实例变量会随着对象一起分配到Java堆中。
③解析(Resolution)
将类、接口、字段和方法,即常量池的符号引用转为直接引用。
3、初始化阶段(Initialization)
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>总是在子类<clinit>之前被调用。也就是说,父类的static块优先级高于子类。
口诀:由父及子,静态先行。
准备阶段为静态变量(static)分配内存并设置初始值。并且执行类的初始化方法<clinit>()。
只有在给类的中的static的变量显式赋值或在静态代码块中赋值了,才会生成<clinit>()方法。
不会生成<clinit>()方法的字段
//非静态的字段,不管是否进行了显式赋值
public int num = 1;
//静态的字段,没有显式的赋值
public static int num1;
//声明为static final的基本数据类型的字段,不管是否进行了显式赋值方法
public static final int num2 = 1;
4、类的使用(Using)
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。
5、类的卸载(Unloading)
6、类加载器
类加载器是 JVM 执行类加载机制的前提。
启动类加载器(BootStrap ClassLoader) | 该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。 |
扩展类加载器(ExtClassLoader) | 该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。 |
应用类加载器(AppClassLoader) | 该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。 |
自定义类加载器 | 开发者自定义类继承ClassLoader,实现自定义类加载规则。 |
7、类加载执行过程
三、双亲委派机制
1、定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
2、优劣
优势 | 劣势 |
通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。 | 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。 |
为了安全,保证类库API不会被修改 |
3、打破双亲委派机制
使用自定义加载器并且重写loadClass方法。例如:Tomcat。
Tomcat使用了自定义类加载器来实现应用之间类的隔离。 每一个应用会有一个独立的类加载器加载对应的类。
public Class<?> loadClass(String name)
类加载的入口,提供了双亲委派机制。内部会调用findClass 重要
protected Class<?> findClass(String name)
由类加载器子类实现,获取二进制数据调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据。重要
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final void resolveClass(Class<?> c)
执行类生命周期中的连接阶段
打破双亲委派机制实现
package icu.islunatic.other.JVM;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.regex.Matcher;
/**
* 打破双亲委派机制 - 自定义类加载器
*/
public class BreakClassLoader extends ClassLoader {
private String basePath;
private final static String FILE_EXT = ".class";
//设置加载目录
public void setBasePath(String basePath) {
this.basePath = basePath;
}
//使用commons io 从指定目录下加载文件
private byte[] loadClassData(String name) {
try {
String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
try {
return IOUtils.toByteArray(fis);
} finally {
IOUtils.closeQuietly(fis);
}
} catch (Exception e) {
System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
return null;
}
}
//重写loadClass方法
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//如果是java包下,还是走双亲委派机制
if(name.startsWith("java.")){
return super.loadClass(name);
}
//从磁盘中指定目录下加载
byte[] data = loadClassData(name);
//调用虚拟机底层方法,方法区和堆区创建对象
return defineClass(name, data, 0, data.length);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
//第一个自定义类加载器对象
BreakClassLoader classLoader1 = new BreakClassLoader();
classLoader1.setBasePath("D:\\lib\\");
Class<?> clazz1 = classLoader.loadClass("icu.islunatic.my.A");
//第二个自定义类加载器对象
BreakClassLoader classLoader2 = new BreakClassLoader();
classLoader2.setBasePath("D:\\lib\\");
Class<?> clazz2 = classLoader2.loadClass("icu.islunatic.my.A");
System.out.println(clazz1 == clazz2);
Thread.currentThread().setContextClassLoader(classLoader1);
System.out.println(Thread.currentThread().getContextClassLoader());
System.in.read();
}
}
四、运行时数据区
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区。即下图
1、程序计数器
定义:每个线程通过程序计数器记录当前要执行的字节码指令的地址。
作用:程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。不管是分支、跳转、异常,只需要在程序计数器中放入下一行要执行的指令地址即可。
2、内存溢出
内存溢出指的是程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。由于每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。程序员无需对程序计数器做任何处理。
3、Java虚拟机栈
Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每个方法的调用使用一个栈帧来保存。
-
局部变量表,局部变量表的作用是在运行过程中存放所有的局部变量
-
操作数栈,操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
-
帧数据,帧数据主要包含动态链接、方法出口、异常表的引
4、栈内存溢出
Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出。Java虚拟机栈内存溢出时会出现StackOverflowError的错误。
设置虚拟机栈的大小:-Xss栈大小 字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
例如:-Xss1048576 -Xss1024K -Xss1m -Xss1g 或者 -XX:ThreadStackSize=1024。
5、本地方法栈
Java虚拟机栈存储了Java方法调用时的栈帧,本地方法栈存储的是native本地方法的栈帧。
在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
6、堆
一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
7、堆内存溢出
堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory错误
设置堆的大小:-Xmx值(最大值,即必须大于2M) -Xms值(初始的total,即必须大于1M)
建议将-Xmx和-Xms设置为相同的值。
8、方法区
方法区是存放基础信息的位置,线程共享,存放在元空间中,主要包含三部分内容:
-
类的元信息,保存了所有类的基本信息,一般称之为InstanceKlass对象。
-
运行时常量池,保存了字节码文件中的常量池内容
-
字符串常量池,保存了字符串常量