2.1 结构图
- 类加载器子系统:将字节码文件(.class)加载到内存中的方法区
- JVM 运行时数据区:
-
- 方法区:存储已被虚拟机加载的类的元数据信息(元空间)。也就是存储字节码信息。
- 堆:存放对象实例,几乎所有的对象实例都在这里分配内存。
- 虚拟机栈(java栈):虚拟机栈描述的是Java方法执行的内存模型。每个方法被执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 程序计数器:当前线程所执行的字节码的行号指示器。
- 本地方法栈:本地方法栈则是记录虚拟机当前使用到的
native
方法。
- 本地方法接口:虚拟机使用到的
native
类型的方法,负责调用操作系统类库。(例如Thread类中有 很多Native方法的调用) - 执行引擎:包含解释器、即时编译器和垃圾收集器 ,负责执行加载到JVM中的字节码指令。
自己写的方法在虚拟机栈,本地方法栈是底层有的,比如是 C++ 实现的,会有 native 关键字
2.2 类加载
类加载器
.java -> .class
能不能用是类加载器决定的,它负责加 .class
文件放到 JVM 中的方法区,如下图。
.class
文件在文件开头有的文件标识(CAFE BABE),并且ClassLoader
只负责 .class
文件的加载,至于它是否可以运行,则由 执行引擎 Execution Engine
决定。
【类加载器的种类】
类加载器不止一类,有 3 种。不同下的 .class 文件会被不同的类加载器加载。
- 逻辑类上的父与子
没有 extends 关键字继承
,所以只是逻辑上;下面的通过parent
属性成为逻辑上的儿子。
根类的父类 parent 没有赋值 C++ 层面的权利,赋值不了了,可以这么认为
了解:
Java 9之前的ClassLoader
Bootstrap ClassLoader加载$JAVA_HOME中jre/lib/rt.jar,加载JDK中的核心类库
ExtClassLoader加载相对次要、但又通用的类,主要包括$JAVA_HOME中jre/lib/ext/*.jar
Java 9及之后的ClassLoader
Bootstrap ClassLoader,使用了模块化设计,加载lib/modules启动时的基础模块类,java.base、 java.management、java.xml
ExtClassLoader更名为PlatformClassLoader,使用了模块化设计,加载lib/modules中平台相关模块, 如java.scripting、java.compiler。
双亲委派机制
作用:保证沙箱安全。
这里是从自己编写的一个类 user.java
加载过程来讲解的。
- 左边不是加载,只是询问缓存
findLoadedClass
和委派给父类加载器。 - 右侧才是加载,自己加载不到,就让子类加载器去加载。
缓存就是:保证安全,也保证效率;先从缓存加载。
再一个举例:
我们在外面自己建立包java.lang.String
,那就是对原本的系统源文件 String.java 恶意修改,就不会再加载源文件
所以这个模型也就是这个意思,保护本身的资源。如果 String 在根类加载器下,那我们自己写的这个类就不会被加载,起到了保护作用。
Debug 验证双亲委派
AppClassLoader
去询问缓存
缓存中没找到
如果一直递归到扩展类加载器
缓存还没找到
注意:Java API 层面拿不到 根类加载器
,但是扩展类加载器parent
属性确实是那样的
这里的方法findBootstrapClass
带有native
关键字,这说明已经不是 Java 层面实现的了
这里扩展类加载器
找到了!
加载一个类,都是先去加载其父类Object
,找Object
这里没有截图,只到最后结果了,最终还是应用类加载器
自己加载了,因为本身就是我们自己写的!
自定义类加载器
删除掉编译的target/User.class
,再去执行,就会使用自定义的类加载器去指定目录下加载
/**
* Description: 自定义类加载器 去到自己指定的路径下加载某个.class文件(D:/temp/com/atguig/jvm/classloader/User.class) 重写classloader类中的findclass
*/
public class MyCustomerClassLoader {
static class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassForByte(name);
return defineClass(name, b, 0, b.length);
}
// 给一个.class文件 得到该文件的字节数组
private byte[] loadClassForByte(String name) {
// name:com.atguigu.jvm.classloader.User
// step1:将包中的.替换成/ 且拼接上一个.class文件
// path com/atguigu/jvm/classloader/User.class
String path = name.replace('.', '/').concat(".class");
// endPath:D:/temp/com/atguigu/jvm/classloader/User.class
String endPath = "/Users/macbook/Desktop/tmpFile/网盘资源/奎哥/02_JVM/day01/3_code/Jvm-Resource/src/main/java/"
+ path;
//step2:用文件输入流的方式来读指定目录下的.class文件
try {
FileInputStream fileInputStream = new FileInputStream(endPath);
byte[] bytes = new byte[fileInputStream.available()];
if (fileInputStream != null) {
fileInputStream.read(bytes);
fileInputStream.close();
}
return bytes;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> aClass = myClassLoader.loadClass("com.atguigu.jvm.classloader.User");
String simpleName = aClass.getClassLoader().getClass().getSimpleName();
System.out.println("加载自定义路劲下的User.class文件的类加载器是:"+simpleName);
Object o = aClass.newInstance();
Method testMethod = aClass.getMethod("test");
testMethod.invoke(o);
}
}
最终达到的框图就是这样的,实际的运行过程会拼接过去。
打破双亲委派机制
注意⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些 特殊需求想要打破双亲委派模型,也是可以的。
ClassLoader 类有两个关键的方法:
protected Class loadClass(String name, boolean resolve) :
加载指定的类,实现了双亲委派机制 。 name 为类全类名
resove 如果为 true,在加载时调用 resolveClass(Class c) 方法解析该类。
protected Class findClass(String name) :
根据全类名称来查找类,默认实现是空方法。
- 为什么要打破双亲委派机制?
双亲委派模型的工作流程是:当一个类加载器收到类加载请求时,首先会委托给父类加载器去完成,只有在父类加载器无法完成时,子类加载器才会尝试加载。这种机制保证了Java核心库的类型安全,但也存在局限性:
-
- 无法隔离不同应用的类:如果Web应用A和Web应用B都使用了不同版本的相同类库,双亲委派机制会导致它们共享同一个类;
- 无法支持热部署:一旦类被加载,就无法重新加载新版本的类;
- 想想Tomcat要如何加载webapps⽬录下的多个不同的应⽤?
而Tomcat需要同时部署多个Web应用(webapps目录下的每个文件夹都是一个独立应用),这些应用可能有:
-
- 不同版本的相同类库(如Spring 4和Spring 5);
- 相同全限定名的类(如都包含com.example.MyClass);
- 需要独立热部署的能力;
所以,Tomcat打破了双亲委派机制,实现了自己的类加载器体系:
-
- WebappClassLoader:每个Web应用有自己的类加载器
-
-
- 优先加载
/WEB-INF/classes
和/WEB-INF/lib
中的类 - 无法加载时才会委托给父类加载器
- 这样就实现了应用间的类隔离
- 优先加载
-
- 如何打破双亲委派机制?
- 自定义类加载器,不打破:重写
ClassLoader.findClass
- 打破:重写
ClassLoader.loadClass
/**
* Description: 打破双亲委派(重写classloader类中的findclass and loadClass )
*/
public class BreakPrentModel {
static class MyClassLoader1 extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if (!name.startsWith("com.atguigu")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassForByte(name);
return defineClass(name, b, 0, b.length);
}
// 给一个.class文件 得到该文件的字节数组
private byte[] loadClassForByte(String name) {
// name:com.atguigu.jvm.classloader.User
// step1:将包中的.替换成/ 且拼接上一个.class文件
// path com/atguigu/jvm/classloader/User.class
String path = name.replace('.', '/').concat(".class");
// endPath:D:/temp/com/atguigu/jvm/classloader/User.class
String endPath = "/Users/macbook/Desktop/tmpFile/网盘资源/奎哥/02_JVM/day01/3_code/Jvm-Resource/src/main/java/"
+ path;
//step2:用文件输入流的方式来读指定目录下的.class文件
try {
FileInputStream fileInputStream = new FileInputStream(endPath);
byte[] bytes = new byte[fileInputStream.available()];
if (fileInputStream != null) {
fileInputStream.read(bytes);
fileInputStream.close();
}
return bytes;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws Exception {
MyClassLoader1 myClassLoader1 = new MyClassLoader1();
Class<?> aClass = myClassLoader1.loadClass("com.atguigu.jvm.classloader.User");
String simpleName = aClass.getClassLoader().getClass().getSimpleName();
System.out.println("加载自定义路劲下的User.class文件的类加载器是:" + simpleName);
Object o = aClass.newInstance();
Method testMethod = aClass.getMethod("test");
testMethod.invoke(o);
}
}
- JavaAPI 中验证三个类加载器的关系
public class ClassLoaderLoadPathAndRelation {
public static void main(String[] args) {
// 类加载器的加载路径
System.out.println(String.class.getClassLoader());// 根类加载器对象--实现在c++中
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());// 扩展类加载器对象
System.out.println(ClassLoaderLoadPathAndRelation.class.getClassLoader());// 应用类加载器对象
// 各个类加载器的关系
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();// 获取系统类加载器对象
System.out.println("appClassloader类加载器"+systemClassLoader);
ClassLoader extClassloader = systemClassLoader.getParent();
System.out.println("appClassloader类加载器的父类加载器也即extClassLoader类加载器:"+extClassloader);
ClassLoader bootstrap = extClassloader.getParent();
System.out.println("extClassLoader类加载器的父类加载器也即bootstrap类加载器:"+bootstrap);// 实现在c++中
}
}
类加载过程
就是类加载器
将.class 字节码文件
放到 JVM 的方法区中,需要的细节步骤。
类的生命周期是由7个阶段组成,但是类的加载说的是前5个阶段 + 使用 + 卸载
- 加载:主要完成下面 3 件事情:
在磁盘上查找,并通过IO读入字节码文件
在内存中生成一个代表这个类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性,比如
.class
文件开头是否是 cafe babe,否则 JVM 识别不到 - 准备:给类的静态变量分配内存,并赋予默认值。
注意这个阶段只是给变量值放入到cinit
方法中,该变量还没有真正的值。
(程序还没执行)没有给到,初始化阶段才会给静态变量这个值。
public static int a=1;// 在类加载的准备阶段就会给a赋上一个0;
public static String b;// 在类加载的准备阶段就会给b赋上一个null;
public int c;// 在类加载的准备阶段就不会给实例对象的属性赋。
public static final int value = 123;// 为 value生成 ConstantValue属性,
- 解析(最难理解):将符号引用替换为直接引用。
该阶段会把一些静态方法(符号引用,比如main()方法) 替换为 指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成)
后面会讲解这句话是什么意思
- 初始化:真正执行
cinit
方法。
静态变量初始化为那个值
执行静态代码
为常量赋值
2.3 执行引擎
这里黄色代表,方法区、堆
是共享的,后面是私有,本地方法栈是不可见的。
JVM执行引擎通常由两个主要组成部分构成:
- 解释器:当
.class
文件被加载到内存中时,解释器逐条解析和执行字节码指令 - 即时编译器(JIT Compiler):类似解释器,但是有区别,对需要优化的
字节码指令
进行编译并且存储。 - GC
解释和编译,类似于实时翻译和你说完我再翻译。
解释器
解释器,解释器逐条解析和执行字节码指令。这样导致什么样的问题呢?
特点:执行到哪里,解释到哪里。
【缺点】边解释边执行,性能较差;并且会反复执行重复指令。
【优点】能够更快地解释字节码中的命令。
能不能对重复指令进行优化呢?
JIT
可以优化!JIT 即时编译器就出现了。
功能:能编译 .class -> 机器码
,这样看和解释器没区别。
还有另一点:将编译之后的机器码放到 codeCache
,这时候优点凸显出来了!
特点:要先编译才可以,编译期间要等。
【缺点】先编译,程序才能执行;如果字节码太多,编译时间很长。
【优点】结合 CodeCache 提升性能。
触发条件和时机
- 那哪些指令触发 JIT,需要放到 codeCache 呢?
肯定不可能全部放过去,因为CodeCache
有 240MB 容量限制。
- 如果所有的指令都用 JIT,那我还要解释器干什么呢?
所以,JIT 是为了应对.class
文件中重复的字节码指令才能触发即时编译。
- 怎么去界定重复指令?
阈值判断
- 阈值是多少呢?JIT 有两种计数器。
- c1 client 客户端编译,优化局部编译 速度快
- c2 serer 服务端编译,优化全局编译 速度慢
客户端局部运行,越快越好;服务端要纵观全局
最好的方式肯定是,混合 mixed
两种,那么阈值就不一样;c1 = 1500 c2 = 10000
这里在控制台就可以查看我们本机是不是这样的
java --version
openjdk 21.0.7 2025-04-15
OpenJDK Runtime Environment Homebrew (build 21.0.7)
2.4 本地接口
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
如果 java 想要调用有 native
关键字的方法,走的就是本地方法接口。
2.5 本地方法栈
这个对于线程来说是私有且不可见的。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
调用的是本地方法,存放方法的栈就是本地方法栈。
2.6 程序计数器
线程私有的。
每个线程都有一个程序计数器,就是一个指针,(用来存储下一条将要执行的字节码指令的地址)。
由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
注意:
1. 当程序是在执行Java
方法时,程序计数器中的值为下一条执行字节码指令的地址。
2. 当程序执行的时Native
方法时,程序计数器中的值为空。
3. 程序计数器是不会发生OutMemoryError
的区域。