假设我们有一个文件 x.Java
,你执行 javac
,它就会变成 x.class
。
这个 class 怎么执行的?
当我们调用 Java 命令的时候,class 会被 load 到内存,这块叫【Classloader】,会被 Classloader 装载到内存里。
一般的情况下,我们写自己的类文件的时候也会用到 【Java 的类库】,所以他会把 Java 类库相关的这些个类也要装载到内存里,装载完成之后会调用【字节码解释器】或者是【JIT 即时编译器】来进行解释或编译,编译完之后由【执行引擎】开始执行,这以及下面面对的,那就是操作系统和硬件了。
Java 编译好了之后变成 class, class 会被 load 到内存,与此同时像什么 string, object 这些个 class 也都会被 load 到内存。
Java 是这个解释执行的还是编译执行的?
其实解释和编译是可以混合的,特别常用的一些代码,代码用到的次数特别多,这个时候他会把代码做成一个及时的编译,做成一个本地的编译。
你可以理解为就像 c 语言在 Windows 上执行的时候,把它编译成 exe 一样,那么下次再执行这段代码的时候,就不需要通过解释器来一句一句解释来执行了,执行引擎可以直接交给操作系统去让它调用,这个效率要高很多,不是所有的代码都要都会被 GIT 进行及时编译的,如果是这样的话,那整个 Java 就完全变成了不能跨平台了。
所以有一些特定的,执行起来执行次数好多好多,用的特别多的时候,这个时候会进行一个即时编译器的编译。
所谓的 JVM 虚拟机,其实它本身是一个规范,是虚构出来的一台计算机。拥有自己的操作系统,是一个跨语言平台。
为什么 JVM 虚拟机能够支持多种语言运行在上面呢?
最关键的原因是就是因为 class 这个东西,我们可以说任何的语言,只要你能编译成 class,符合 class 文件的规范,你就可以扔在 Java 虚拟机上去执行。
注意:JVM 只和 class 文件有关,与 java 无关。
JDK 官网:Java SE Specifications (oracle.com)
维基百科:Java虚拟机 - 维基百科,自由的百科全书 (wikipedia.org)
甲骨文中国:Java 软件 | Oracle 中国
Hotspot(最常用)
java -version
命令可查看使用的是什么 JVMJrockit
TaobaoVM(免费)
LiquidVM
azul zing(特别贵)
J9-IBM
包含关系。
JVM – 运行 java 字节码的虚拟机
JRE – java 运行环境 == jvm + core(核心类库)
JDK – java 开发工具包 == jre + development kit(开发工具)
Class 文件格式(File Format)
javap
,-v
参数详细查看如下图示:
每个部分描述了 Java 字节码文件的不同方面,从类的声明到方法的定义,以及与常量池等相关的信息。这些信息在 Java 虚拟机中被解析和使用,以正确地加载和执行 Java 类。
Java 的汇编指令有 200 多条。
好文分享:class类文件结构
加载、链接和初始化是 Java 程序运行时的三个主要阶段。
Loading – 加载
Linking – 链接
Initializing – 初始化
具体来说:
Loading(加载): 这是类加载过程的第一个阶段。在这个阶段,Java 虚拟机(JVM)会从类的外部源加载类的二进制数据,通常是从磁盘文件中加载,但也可以是网络、内存等。
加载器将类的二进制数据(class文件)从外部源加载到内存中,并将其放置在运行时数据区的方法区内。
Linking(链接): 这是加载过程的第二个阶段,它将类的二进制数据链接到 JVM 的运行时状态。
链接过程分为以下三个步骤:
Initializing(初始化): 这是链接过程的最后一个阶段,也是类加载的最终阶段。在这个阶段,类的静态初始化器会被执行,初始化静态字段和执行静态块。这个阶段是在类被首次使用时触发的,例如创建类的实例、访问类的静态字段等。(调用类初始化代码
,给静态成员变量赋初始值)
总结来说,类加载过程涉及到从外部源加载类的二进制数据,然后将其链接到 JVM 的运行时状态,最终进行初始化。链接阶段包括验证、准备和解析,而初始化阶段则会执行类的静态初始化器。这个过程确保类在 Java 虚拟机中正确加载和使用。
加载之后会发生什么?
任何一个 class 被加载到内存之后,会生成了两块内容。
在 Java 虚拟机中,类的元数据,包括类的结构信息、字段、方法、父类、接口等,都会被加载到方法区(元空间Metaspace,替代了永久代PermGen)中。Class 对象本身也是类的元数据之一,因此 Class 对象也存放在方法区中。 Class 对象在内存中的位置可以看作是类的描述符,用来操作该类的字节码以及其他相关的元数据。
可以看看下面这两篇文章:
JVM 它本身有一个类加载器的层次,这个类加载器就是一个普通的 class,JVM 有一个类加载器的层次,分别来加载不同的 class。或者说,JVM 里面所有的 class 都是被类加载器给加载到内存的,那么这个类加载器简单说我们可以把它叫做 ClassLoader。
注意:从下往上,是委托给父加载器,不是继承关系,是语法上的一种关系。
在 Java 虚拟机中,类加载器按照层次结构进行组织,分为三个主要层次:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader,也称为系统类加载器)。这些类加载器形成了类加载器的双亲委派模型。
java.lang
包中的类)。它是所有其他类加载器的父加载器,但它本身不是一个普通的 Java 类,因此在源码中并没有对应的类。它位于虚拟机内部,通常用本地代码实现。它是最顶层的类加载器,负责加载 Java 核心类库。jre/lib/ext
目录中的类。它的父加载器是启动类加载器。启动类加载器、扩展类加载器和应用程序类加载器构成了类加载器的层次结构,通过双亲委派模型来保证类的加载的一致性和安全性。在加载一个类时,首先会尝试由父加载器加载,只有在父加载器无法加载时,子加载器才会尝试加载。这个模型可以防止类的重复加载,同时保证了类的隔离性。
在启动类加载器加载的类中,有一部分是虚拟机内部的类,比如 java.lang.Object
、java.lang.String
等。这些类并不是普通的 Java 类,因此没有对应的源码。而在 Java 虚拟机的源码中,通常会对这些类的加载过程进行描述,但实际上它们是由虚拟机的实现提供的。
最顶层加载器 Bootstrap 会返回一个空值。
public class T004_ParentAndChild {
public static void main(String[] args) {
System.out.println(T004_ParentAndChild.class.getClassLoader());
System.out.println(T004_ParentAndChild.class.getClassLoader().getClass().getClassLoader()); // App
System.out.println(T004_ParentAndChild.class.getClassLoader().getParent()); // Extension
System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent()); // Bootstrap,返回 null
// System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent().getParent()); // 解开注释就会报空指针异常
}
}
输出结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
null
sun.misc.Launcher$ExtClassLoader@4554617c
null
先是自底向上检查是否已经加载,然后再回过头来找 class 并加载,查看是否加载成功,一直到底都没加载成功就会抛出一个 class 找不到异常。
这个缓存是在哪缓存?
可以简单的认为是它自己内部维护的一个容器,一个 list 或者一个数组加载的东西都扔在里面。每个加载器都有自己的缓存。
主要是为了安全。次要原因是资源浪费问题,避免重新加载问题(防止类重复加载)。
来自 Launcher 源码
sun.boot.class.path
java.lang
包下的类等。java.ext.dirs
lib/ext
目录或者由系统变量 java.ext.dirs
指定的路径。sun.misc.Launcher$ExtClassLoader
实现的。java.class.path
sun.misc.Launcher$AppClassLoader
实现的。代码查看:
public class T003_ClassLoaderScope {
public static void main(String[] args) {
System.out.println("根目录下加载");
String pathBoot = System.getProperty("sun.boot.class.path");
System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));
System.out.println("--------------------");
System.out.println("ext 下加载");
String pathExt = System.getProperty("java.ext.dirs");
System.out.println(pathExt.replaceAll(";", System.lineSeparator()));
System.out.println("--------------------");
System.out.println("App 下加载");
String pathApp = System.getProperty("java.class.path");
System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
}
}
输出结果:
根目录下加载
C:\Program Files\Java\jdk1.8.0_321\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_321\jre\classes
--------------------
ext 下加载
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
--------------------
App 下加载
C:\Program Files\Java\jdk1.8.0_321\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\rt.jar
D:\笔记\学习资料\马士兵\JVM视频源码\out\production\JVM // 项目路径
D:\software\idea2022\IntelliJ IDEA 2022.1.3\lib\idea_rt.jar
当你想加载某个类的时候,可以调用 loadClass
方法
public class T005_LoadClassByHand {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz = T005_LoadClassByHand.class.getClassLoader().loadClass("com.mashibing.jvm.c2_classloader.T002_ClassLoaderLevel");
System.out.println(clazz.getName());
//利用类加载器加载资源,参考坦克图片的加载
//T005_LoadClassByHand.class.getClassLoader().getResourceAsStream("");
}
}
loadClass
方法是在双亲委派模型下实现的。
findInCache -> parent.loadClass -> findClass()
private final ClassLoader parent; // final 关键字修饰
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类不在已加载类中,则委派给父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有父加载器,则使用系统类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果找不到类,抛出ClassNotFoundException
// 从非空父类装入器
}
// 如果父加载器也找不到,则尝试自己加载
if (c == null) {
// 如果仍未找到,则按顺序调用 findClass
// to find the class. 要找到这个类
long t1 = System.nanoTime();
c = findClass(name);
// 这是定义类装入器;记录统计数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException { // findClass被protected修饰,保护起来
throw new ClassNotFoundException(name);
}
主要步骤如下:
loadClass
方法会调用 findClass
方法,尝试自己加载目标类。这是子类加载器在加载自己类的最后一步。resolve
参数为 true
,则会调用 resolveClass
方法,用于解析加载的类,确保类的完整性和正确性。什么时候需要自己去加载?
加载进去就生成 class 对象吗?
不是,要经过 初始化 才生成 class 对象。
只需要做一件事,就是定义自己的 findClass 就可以了。
具体来说:
继承 ClassLoader
重写模板方法 findClass
加密:可自定义类加载器加载自加密的 class
代码如下:
首先继承 ClassLoader 这个类
// 自定义类加载器继承自ClassLoader
public class T006_MSBClassLoader extends ClassLoader {
// 重写findClass方法,用于加载类字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 根据类名构建文件路径
File f = new File("c:/test/", name.replace(".", "/").concat(".class"));
try {
// 读取字节码文件
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
// 读取文件内容并写入字节数组输出流
while ((b=fis.read()) !=0) {
baos.write(b);
}
// 将字节数组转换为字节数组
byte[] bytes = baos.toByteArray();
baos.close();
fis.close(); // 关闭流
// 使用defineClass方法定义并返回类
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
// 若加载失败,则调用父类的findClass方法
return super.findClass(name); // throws ClassNotFoundException
}
// 主函数
public static void main(String[] args) throws Exception {
// 创建自定义类加载器实例
ClassLoader l = new T006_MSBClassLoader();
// 通过自定义类加载器加载类
Class clazz = l.loadClass("com.mashibing.jvm.Hello");
Class clazz1 = l.loadClass("com.mashibing.jvm.Hello");
// 输出两次加载的类是否相同
System.out.println(clazz == clazz1);
// 创建加载的类的实例并调用方法
Hello h = (Hello) clazz.newInstance();
h.m();
// 输出类加载器信息
System.out.println(l.getClass().getClassLoader()); // 输出自定义类加载器的类加载器
System.out.println(l.getParent()); // 输出自定义类加载器的父类加载器
System.out.println(getSystemClassLoader()); // 输出系统类加载器
}
}
public class Hello {
public void m() {
System.out.println("Hello JVM!");
}
}
这段代码实现了一个自定义的类加载器 T006_MSBClassLoader
,该类加载器继承自 ClassLoader
,重写了 findClass
方法来加载类的字节码。在主函数中,使用这个自定义的类加载器加载类,并输出加载的类是否相同,然后创建该类的实例并调用方法。最后,输出了自定义类加载器的类加载器和父类加载器信息,以及系统类加载器的信息。这种自定义类加载器的方式允许你从非标准的位置加载类文件,并且可以通过不同的类加载器实现类的隔离。
defineClass
方法是 ClassLoader
类中的一个重要方法,用于将字节数组转换为一个 Class
对象。该方法的作用是将一个【字节数组中的类字节码】转换为一个【Java 类的实例】。当一个类加载器调用 defineClass
方法时,它会将字节数组中的类字节码转换为一个 Class
对象,并返回该对象。(不会验证字节码的正确性)
输出结果
true
Hello JVM!
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
大家都知道 java 的代码 class 文件很容易就被反编译了。
但是我要是定义自己的格式,我不想让别人反编译,这时候怎么办?
你可以通过自定义的 class loader 来进行。然后在写逻辑的时候加一个加密操作。
三大模式:
为什么不干脆直接编译成本地代码,那执行效率不更高吗?
有两个原因。
可以用指明参数的方式指定用什么模式
-Xmixed 默认为混合模式
开始解释执行,启动速度较快,对热点代码实行检测和编译
-Xint 使用解释模式,启动很快,执行稍慢
-Xcomp 使用纯编译模式,执行很快,启动很慢(要编译的类少的时候,启动也会很快)
lazyloading
parent 是如何指定的,打破双亲委派
super(parent)
指定loadClass()
方法在 Java 虚拟机 (JVM) 中的类加载器 (ClassLoader) 实现中,通常使用了以下两种设计模式:
除了委托模式和单一职责模式,ClassLoader 的实现可能还涉及其他设计模式,具体取决于实际的实现细节和需求。例如,一些类加载器的缓存机制可能使用了享元模式 (Flyweight Pattern) 来提高性能和资源利用效率。
?
在 Java 虚拟机 (JVM) 中的类加载器 (ClassLoader) 实现中,通常使用了以下两种设计模式:
除了委托模式和单一职责模式,ClassLoader 的实现可能还涉及其他设计模式,具体取决于实际的实现细节和需求。例如,一些类加载器的缓存机制可能使用了享元模式 (Flyweight Pattern) 来提高性能和资源利用效率。