文章目录
一、JVM基础
1.1 Java虚拟机简介
1、是什么:
- 是一种抽象化的计算机,有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统
- 是一种规范,可用不同的方式加以实现,对应不同的虚拟机
2、内容:一个指令集、堆栈、 “垃圾堆”和一个方法区
3、Java语言跨平台
-
Sun以及其提供商提供了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(.class文件)
-
.Java文件可以被被编译成.class
-
一旦一个JVM在给定的平台上运行,此.java文件都能在这个平台上运行,如图2所示
-
特点:JVM仅与字节码.class文件有关,与任何语言无关
4、Java和Jvm的关系:
- Java发布之初,就考虑不止让Java也要让其他语言运行在JVM上(时至今日,可以在JVM上执行的常见语言有:Python、Groovy、JS、PHP、Lua)
- java规范分为java语言规范和JVM规范。
5、常见的Jvm实现
-
JRockit:BEA公司的虚拟机,全部代码都靠即时编译器编译执行由于在GC方面的优异表现,被Oracle收购
-
HotSpot: 由于LongView技术公司设计,后来因为其在JIT编译上的优异表现被Sun收购,06年ORacle收购了Sun
现在我们使用的HotSpot = 原HotSpot(取JIT) + JRockit(取GC)
-
Zing:Azul公司的虚拟机,是真正的高性能虚拟机。一个VM实例可以管理至少数十个CPU+几百G内存资源,实现可控的GC时间
1.2 指令
1、定义
指令 = 操作码 + 操作数
getstatic #2 :表示去.class文件常量池中[02]处,获取静态字段的值
1.3 执行引擎
混合模式:解释器和JIT即时编译器的共存,使得HotSpot执行引擎能够平衡启动速度和运行效率
1、解释器解释执行
特点:逐条解释字节码指令进行执行
2、JIT及时编译器编译执行
特点:一整段执行
3、HotSpot执行引擎执行过程
1)每当执行完一条指令(操作码 + 操作数)后,PC寄存器会更新下一条需要被执行的指定的地址
2)解释执行阶段
-
当Java程序启动时,HotSpot执行引擎首先采用解释器,逐条解释执行字节码指令
即读取字节码指令,将其翻译成对应的机器码,并直接由硬件执行
-
优势
可以快速启动程序(不需要等待编译器编译整个程序)、节省内存(只需要加载和执行当前需要的代码)
3)编译执行阶段
- 热点代码探测:在执行过程中,HotSpot执行引擎会收集代码的执行情况,特别是那些执行频率高的代码块(称为“热点代码”)。热点代码探测通常基于计数器(如方法调用计数器)来实现
- 即时编译
- 一旦热点代码被探测到,HotSpot执行引擎就会将这些代码提交给JIT即时编译器进行编译
- JIT编译器会将热点代码编译成与本地平台相关的机器码,并进行优化以提高执行效率
- 编译后的代码会被缓存起来,以便后续再次执行时直接使用。这样,热点代码的执行速度就会大大提高
1.4 Jvm基于栈的指令集架构
源于: Java语言的跨平台特性
优点
- 可移植性好:基于栈的指令集架构不依赖于特定的硬件寄存器,因此可以更容易地实现跨平台的操作
- 实现简单:基于栈的指令集架构在设计和实现上相对简单
缺点
- 执行性能:与基于寄存器的指令集架构相比,执行相同操作时需要更多的指令数量,影响执行性能
二 、class文件
2.1 字节码简介
1、是什么:二进制01文件
2、如何生成: .class文件即字节码,是.java文件被编译后(javac)生成的文件,如图3所示
3、使用IDEA查看:
- IDEA安装jclassLib Bytecode插件
- IDEA View栏下使用Show Bytecode with jclassLib ,如图5所示
2.2 class文件内容
2.2.1 一般信息
如图6所示
- 主版本号:52-即Java8
- 访问标志
类是public的、final等修饰的
- 本类索引:#9即在常量池[09]中:全类名
- 字段计数:成员变量个数
- 方法计数:方法的个数
2.2.2 常量池
1、是什么:class文件的资源库
2、内容:14种不同结构的表数据
如:符号引用和字面量等
2.2.2.1 符号引用
class_info
类的全限定名:com/mjp/类名
Fieldref_info
- 字段所述的类|接口:字段所在类信息
- 字段名(name)和字段描述符(java/lang/String)
methodRef_info
名字和描述符:无参构造方法,在常量池[33]处有描述符
- 名字<<init>>:即构造方法
- 描述符()V:方法入参为空()、方法返回值为V即void
2.2.2.2 字面量
string_info
private String name = "mjp";
字符串字面量:cp_info #29 <mjp>
Integer_info
private final int a = 10;
Integer:10
2.2.2.3 字面量和符号引用
1、字面量
1)类型:string_info、Integer_info等
2)分类
- 文本字符串:String name = “mjp”; cp_info # 4
- 常量:final int a = 1;
二者均存在于常量池中,一个是[13]CONSTANT_Integer_info处、一个是[29]CONSTANT_utf8_info字符串常量池中
2、符号引用
1)类型:class_info、Fieldref_info、methodRef_info等
2)分类
- class_info:类、接口全限定名,cp_info # 1
- Fieldref_info:字段名称 和 字段描述符,cp_info # 2
- methodRef_info:方法名、方法入参、方法返回值类型,cp_info # 3
3、符号引用的作用
- 符号引用,保存了方法(所属类、描述符)的信息
- 在类加载过程时,可以通过符号引用,动态链接到直接内存地址引用
这样使得Xxx.class文件更小
2.2.3 字段
private String name;
- 名称:name
- 描述符:String
- 访问标志:public
2.2.4 方法
1、默认的无参构造方法
- 名称:<<init>>
- 描述符:<()V>表示方法的返回值类型是void、方法的入参为空
- 访问标志:public
2、自定义方法
public void func(Integer i, String s) {
this.map.put(s, i);
}
- 名称:<func>
- 描述符:(String;Integer;)V表示方法的返回值类型是void、方法的入参为String 和 Integer类型
- 访问标志:public
3、方法调用字节码指令invoke
public class Demo{
private void func(Animal animal, Map<String, Integer> map) {
// invokevirtual,调用的多态方法
animal.eat();
// invokeinterface,调用的接口方法
map.put("mjp", 18);
// invokestatic,调用的类方法
f1();
// invokespecial,调用的对象私有方法
f2();
// invokevirtual,调用的实力方法
f3();
}
// invokestatic
public static void f1() {
}
// invokespecial
private void f2() {
}
// invokevirtual
public void f3() {
}
}
-
invokevirutal: 当你调用一个对象的非静态、非私有方法,并且这个方法在编译时不能确定具体调用哪个类的方法时(即存在继承和多态的情况) 或者 实力方法(非静态、非私有)
上述animal.eat()即多态在编译时期无法确定调用是父类还是子类的eat方法、对象本身的实力方法-f3()
-
invokespecial:调用对象的private私有方法、构造方法、父类继承方法-f2
-
invokeinterface:调用接口方法,上述map.put即调用的Map接口的put方法
-
invokestatic:调用的类方法-f1()
-
invkoedynamic:使用了lambda表达式
三、类加载
3.1 类加载过程
分为三个阶段:加载、链接、初始化
1、加载(Loading)
- 通过类的全限定名,获取此类的二进制字节流class文件
- 将class文件存储的静态结构,转换为方法区的运行时数据结构
- 并在堆中生成一个代表此类的Class对象,作为这个类在方法区中各种字段、方法的访问入口
2、链接
1)验证(Verification)
确保Class文件的字节流中包含的信息符合JVM规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
验证内容
2)准备(Preparation)
- 在方法区中为静态变量分配内存、并设置默认值
public static int value = 1;
在准备阶段过后,value的值为0,而不是1。赋值1的动作在初始化阶段进行
3)解析(Resolution)
动态链接:将常量池中的符号引用转化为直接引用的过程。
3、初始化(Initialization):<clinit>()
此时Java程序代码才开始真正执行
-
为类的静态变量赋予初始值
- 正常情况下:在编译时就完成了赋值
public static final int a = 1;
- 特殊情况:对于复杂表达式,即使是static final修饰,也不在编译时完成赋值,而是在此处初始化阶段
public static final int a = new Random().next(10);
-
执行 static {} ,仅在此执行一次
-
执行其父类的<clinit>()
4、使用
除了上述1-3类的加载过程外,类的生命周期还包括4-5
- new 类
5、卸载
当堆中代表类的Class
对象不再被引用,即不可达时,表示类的生命周期结束
- GC会回收这部分堆内存
- 同时卸载类在方法区的数据(方法、字段)
JVM自带的类加载器所加载的类(String类等核心类),在虚拟机的生命周期中通常不会被卸载
3.2 类加载时机
JVM规定了五种情况,需立即对类进行初始化(自然的,加载和链接则需要在此之前开始)
1、如果类没有进行过初始化,此时遇到new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时,则触发此类的初始化
2、首次访问(读取或者设置)这个类的静态变量(被final
修饰、已在编译期把结果放入常量池的静态字段除外)和调用静态方法的时候。
3、如果类型没有进行过初始化,使用了反射获取此类的class对象,则触发此类的初始化
A.class;
Class.forName(xxx.xxx.A);
4、当类进行初始化的时,如果其父类还未初始化过,则对其父类进行初始化
5、当虚拟机启动时,会先初始化main方法所在的主类
3.3 类加载器的层次结构
Java的类加载器采用了双亲委派模型(Parents Delegation Model)。这个模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。
1、启动类加载器(Bootstrap ClassLoader)
- 根类加载器负责加载Java核心库,如
rt.jar
(javax、lang、util、io等) - 由JVM使用C++实现的
2、扩展类加载器(Extension ClassLoader)
- 负责加载JRE的扩展目录(
lib/ext
)中的jar包和类库 - Java实现,由根类加载器完成加载
3、系统类加载器(System ClassLoader):应用类加载器(Application ClassLoader),加载类路径CLASSPATH下的类。一般来说,用户编写的类都是由它来完成加载的
4、自定义类加载器
1)自定义
-
如果不打破双亲委派机制-参考:ApClassLoader(本质继承ClassLoader,重写findClass方法)
-
如果打破双亲委派机制-参考:WebappClassLoader
2)场景
- 用于加载非标准路径下的类,如网络上的类文件
- 隔离记载类,尤其是不同版本的相同类
5、上下文类加载器
1)定义
- 是Thread类的一个属性
- 每个线程都可以有自己的上下文类加载器,在运行时动态加载与当前线程上下文相关的类
- 在JDBC -SPI机制中,使用当前线程的上下文类加载器来加载JDBC驱动-con.mysql.driver.Driver
2)显示设置
// 1.显示设置-自定义类加载器为当前线程的上下文加载器
MyClassLoader myClassLoader = new MyClassLoader();
Thread.currentThread().setContextClassLoader(myClassLoader);
// 2.显示设置-系统类加载器为当前线程的上下文加载器
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
3)默认为系统类加载器关系
- 如果没有显式地为当前线程设置上下文类加载器,则默认继承自其父线程-通常是main线程
- 如果main线程也未显式设置上下文类加载器,则使用系统类加载器作为上下文类加载器
3.4 类型安全机制
Java的类型安全机制依赖于
- 类的完全限定名(包括包名和类名)和类加载器
- 两个类如果由不同的类加载器加载,即使它们的完全限定名相同,它们也被视为不同的类型
- 这意味着,如果一个接口是由一个类加载器加载的,而其实现类是由另一个类加载器加载的,那么这些类之间就无法正确地实现多态和类型转换,因为JVM会认为它们是两种完全不同的类型
- 所以,通常情况下接口和实现类应由相同的类加载器加载
3.5 双亲委派机制
3.5.1 流程
当需要加载一个类时
- 首先委托给父类加载器去加载
- 如果父类加载器能够加载该类,就成功返回
- 如果父类加载器加载不到,子类加载器才会尝试自己去加载
3.5.2 api
-
Class<?> loadClass(String name) :加载name类,返回此类的class对象
-
Class<?> findLoadedClass(String name):查找name类是否被加载,若为null则说明未被加载。如果已被加载则返回class对象
-
Class<?> findClass(String name):调用子类加载器去加载name类
- 若子类加载器加载范围包括name类,则可以加载,并返回class对象
- 否则返回null
- 具体实现,可以参考ApClassLoader,底层调用defineClass
- 根据类的全路径名称,找到.class文件
- 将.class文件转换为运行时方法区的动态数据结构
- 返回此类在堆中的class对象,作为此类在方法区字段、方法的访问入口
-
Class<?> defineClass(String name, byte[] b, int off, int len):将.class(二进制字节数组) --> .java(class对象)
-
ClassLoader getClassLoader():使用class对象调用此方法,返回加载此类的类加载器
-
getSystemClassLoader():获取系统类加载器
3.5.3 双亲委派机制源码-loadClass
protected Class<?> loadClass(String name, boolean resolve){
synchronized (getClassLoadingLock(name)) {
// 1.校验下这个类是否已经被加载过了
Class<?> c = findLoadedClass(name);
// 2.没有被加载过
if (c == null) {
// 2.向上委派
// 2.1如果当前类加载器不是启动类根类加载器,则向上委派给其父类加载器去加载
if (parent != null) {
// 递归-委派
c = parent.loadClass(name, false);
} else {
// 2.2如果当前类加载器已经是根类加载器了,则让其尝试去加载
c = findBootstrapClassOrNull(name);
}
// 3.向下尝试
// 如果向上委派直到根类加载器都没能加载,则不断执行此句,尝试子类加载器加载
if (c == null) {
// 自定义类加载器需要重写此方法,当父类加载器无法完成加载时,需自定义加载逻辑去自己加载
c = findClass(name);
}
}
return c;
}
3.5.4 双亲委派机制的作用
1)安全性
防止用户自定义的类(可能含有恶意代码)覆盖掉Java核心库中的类
2)唯一性
loadClass时,一旦发现c != null,则直接返回class对象,保证了一个类在Jvm中只会被加载一次
3)可扩展性
支持自定义类加载器,通过继承ClassLoader,重写findClass方法,实现自定义加载逻辑
3.5.5 打破双亲委派机制
场景一:SPI
1、缺陷:无法直接加载第三方类
2、缺陷描述
- JDBC规范中的Driver接口,是在java.sql下的即rt.jar中的sql包下的,故通过启动类加载器去加载
- 而JDBC驱动的实现类如MySQL-com.mysql.jdbc包下的具体实现Driver类
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
public class Driver implements java.sql.Driver{
}
鉴于类型安全机制,正常情况下实现类应该和接口一样由启动类加载器去加载,但是实现类在类路径下,无法被根类加载器直接加载
- 导致了在JDBC 4.0之前,需要通过
Class.forName("com.mysql.jdbc.Driver")
方法显式的使用系统类加载器加载驱动
3、解决-SPI机制
JDBC 4.0后,支持SPI方式来注册数据库驱动,无需再通过Class.forName("com.mysql.jdbc.Driver")
方法显式加载驱动
1)代码实现
String url = "jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
//一、获取mysql的数据库连接对象
Connection conn = DriverManager.getConnection(url,"root", "941123");
//二、获取SQL语句执行对象
Statement statement = conn.createStatement();
//三、执行SQL语句
ResultSet set = statement.executeQuery("select * from tb_user");
2)SPI实现机制原理
3)打破双亲委派机制
- 在加载Driver驱动时,使用的是SPI服务发现机制
private static void loadInitialDrivers() {
// 一、load
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 二、hasNext
while(driversIterator.hasNext()) {
// 三、next
driversIterator.next();
}
}
- load:使用上下文类加载器去加载驱动Driver类
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
- 因为上下文类加载器没有显示指定,所以默认为系统类加载器,所以本质就是使用系统类加载器去加载的类路径下的Driver实现类
Class<?> c = Class.forName("com.mysql.driver.Driver", false, loader);
场景二:Tomcat
1、背景
-
在容器化环境,如Docker中,每个容器运行自己的Web服务和Jvm实例
-
传统的应用服务器,如Tomcat中,多个Web服务,是共享一个Jvm实例的
- 则两个不同Web服务(如web1和web2)中分别调用
ClassLoader.getSystemClassLoader()
时,返回的是相同的系统类加载器实例 - 因为系统类加载器是JVM级别的,而非应用程序级别的
- 则两个不同Web服务(如web1和web2)中分别调用
2、Tomcat中可能存在的缺陷
如图8,Web1和Web2共享一个Jvm实例时
- web1服务使用系统类加载器去加载Spring4中的某个类A,并获取aClass对象
- 当web2服务使用到类A时,发现全限定名称 和 类加载器(系统类加载器)都相同,则不会再去加载Spring5中的类A,而是直接返回aClass对象
3、 解决-自定义类加载器
Tomcat为每个Web应用程序创建了一个独立的WebAppClassLoader类加载器 ,并打破双亲委派机制
如图9所示
1)打破双亲委派机制
- 当要加载一个类时,tomcat不会按照双亲委派机制,优先让父类加载器去加载,而是优先使用自定义WebAppClassLoader类加载器去优先加载类路径下的全限定类名的类
- 当自定义加载器无法加载到时,才会走双亲委派
具体代码为:WebappClassLoader --继承> WebappClassLoaderBase --继承> URLClassLoader#findClass
2)自定义类加载器加载过程
- web1服务使用自定义类加载器WebApp1加载器去加载Spring4中的某个类A,并获取aClass对象
- 当web2服务使用到类A时,使用自定义类加载器WebApp2加载器去加载Spring5中的类A
3.6 Launcher
1、内容
- static class ExtClassLoader:扩展类加载器
- static class AppClassLoader:系统类加载器
2、构造器
public Launcher() {
// 1.创建扩展类加载器
ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader();
// 2.创建系统类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
// 3.设置上下文类加载器为:系统类加载器
Thread.currentThread().setContextClassLoader(this.loader);
}
四、运行时数据区-栈
4.1 PC寄存器
1、特点
一个线程对应一个栈,当然也对应一个PC寄存器。Java中唯一一块无OOM的内存区域
2、内容
下一条该执行的指令地址
3、作用
当CPU在多个线程之间切换时,PC寄存器能够确保每个线程都能从上次中断的位置继续执行
4.2 本地方法栈
1、作用
管理Java中native方法的调用
- JNI:是Java调用本地方法的一种机制
- 通过JNI,Java程序可以加载并执行用其他语言(如C或C++)编写的库
- 本地方法栈保存JNI调用过程中的状态信息,确保JNI调用的正确性和稳定性
2、特点
- 可能OOM
- 也可以StackOverFlow
4.3 Java虚拟机栈
4.3.1 Java栈
1、定义
- Java栈是线程私有的,它的生命周期与线程相同,在创建线程时,会创建一个Java栈用于存放该线程执行方法的所有栈帧。
2、特点
- 无GC、但是可能有OOM和StackOverFlow
4.3.2 栈帧
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。这个引用用于支持当前方法的代码能够实现动态链接
1、定义
- 是一种数据结构,是Java栈的基本单位
2、作用
-
一个线程对应一个Java栈,一个线程可能有多个执行方法,每个方法对应一个栈帧
-
栈帧中存储了方法的
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
等信息
-
栈帧中存储的方法信息,用于支持Jvm对方法的调用和执行
3、局部变量表
1)作用
- 存储方法的参数
- 存储方法内部定义的局部变量
2)存储基本单元:slot- 变量槽
- 存储内容
- 8中基本类型的值(long、double占用2个slot来存储值)
- 引用类型的堆内存地址值
3)局部变量名
- 局部变量名主要是在.java源代码中,用于帮助开发者理解和编写代码的,是编译时概念
- 在.class文件中,局部变量名会被转换成JVM内部使用的索引index
// 其中d = 1.0、s = "m"
public void testMethod(double d, String s) {
int i = 5;
User user = new User();
// 其他代码
}
slot和局部变量的关系如图10所示
其中index即.java源码中的局部变量名
- index = 11表示d变量名称
- slot1和slot2两个槽用于存储double类型的值,即存储的d变量的值
- double d = 10,即上述的index = 11指向槽,这样就可以通过index访问槽中的值了
一般this的index = 0,即当前对象的堆地址值,在index = 0 指向的slot中
4)局部变量是否线程安全
- 线程安全
public String func1() {
StringBuilder sb = new StringBuilder();
sb.append("a");
String s = sb.toString();
return s;
}
sb在方法结束后,栈帧随着方法的结束,生命周期也结束了
- 线程不安全-逃逸
public StringBuilder func2() {
StringBuilder sb = new StringBuilder();
sb.append("a");
return sb;
}
sb是堆中的对象,非线程安全
4、操作数栈
1)作用
保存计算过程中临时变量
2)特点
FIFO
3)实战
public int add() {
int a = 8;
int b = 15;
int c = a + b;
return c;
}
.class文件中的方法-code字节码信息如图11所示
0- bipush:
- PC寄存器,去指令地址0,获取字节码指令: bipush
- 执行字节码指令bipush 8 : 将8压至操作数栈顶
2- istore_1:
- PC寄存器,去指令地址2,获取字节码指令: istore_1
- 执行字节码指令istore_1 : 将操作数栈顶的int类型值(8)存储到局部变量表中索引index = 1处
- 弹出操作数栈栈顶int值8
- 将int值8,存入局部变量表中的slot槽中(值为8),指向此槽的index = 1(即局部变量名a)
- 此时局部变量表中内容为,如图12所示
3-bipush:同上0,操作数栈,操作值15
5- istore_2:同上2,局部变量表,slot存储值15,index = 2指向此slot
6、7:iload_1和iload_2
- 获取index = 1对应的slot中的值:8,存入操作数栈
- 获取index = 2对应的slot中的值:15,存入操作数栈
8-iadd:iadd 由于JIT即时编译器负责执行,从操作数栈中弹出两个int类型的值进行相加,并将结果压回操作数栈
- 从操作数栈中拿出元素:8
- 从操作数栈中拿出元素:15
- 8 + 15 = 23
- 23压入操作数栈中
9-istore_3:同上2,局部变量表,slot存储值23,index = 3指向此slot
10-iload_3:获取index = 3对应的slot中的值:23,存入操作数栈
11-ireturn:从操作数栈中拿出元素:23
- 将返回值23压入调用者func的操作数栈
public void func() {
// pc = n
add();
// other,pc n + 1
}
public int add() {
int a = 8;
int b = 15;
int c = a + b;