JVM基础入门
# JVM 基础入门
# JVM 基础
# 聊一聊 Java 从编码到执行到底是一个怎么样的过程?
假设我们有一个文件 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 虚拟机,其实它本身是一个规范,是虚构出来的一台计算机。拥有自己的操作系统,是一个跨语言平台。
为什么 JVM 虚拟机能够支持多种语言运行在上面呢?
最关键的原因是就是因为 class 这个东西,我们可以说任何的语言,只要你能编译成 class,符合 class 文件的规范,你就可以扔在 Java 虚拟机上去执行。
注意:JVM 只和 class 文件有关,与 java 无关。
JDK 官网:Java SE Specifications (oracle.com) (opens new window)
维基百科:Java虚拟机 - 维基百科,自由的百科全书 (wikipedia.org) (opens new window)
甲骨文中国:Java 软件 | Oracle 中国 (opens new window)
# 常见的 JVM 实现
Hotspot(最常用)
- oracle 官方,我们做实验用的 JVM,Java 虚拟机
java -version
命令可查看使用的是什么 JVM- Hotspot 8 之后要收费,但 Open JDK 是开源的版本,免费
Jrockit
- BEA 曾经号称世界上最快的 JVM
- 被 Oracle 收购,合并于 hotspot
TaobaoVM(免费)
- hotspot 深度定制版
LiquidVM
- 直接针对硬件
azul zing(特别贵)
- 最新垃圾回收的业界标杆(号称 1ms 以内)
- www.azul.com
J9-IBM
- Microsoft VM
# JDK JRE JVM 的关系
包含关系。
JVM -- 运行 java 字节码的虚拟机
JRE -- java 运行环境 == jvm + core(核心类库)
JDK -- java 开发工具包 == jre + development kit(开发工具)
# Class 文件格式
Class 文件格式(File Format)
- 二进制字节流(由 java 虚拟机解释)
- 数据类型: u1 u2 u4 u8 和 _info (表类型)
- info 的来源是 hotspot 源码中的写法
- 查看 16 进制格式的 ClassFile
- 软件 - sublime / notepad /
- IDEA 插件 - BinEd
- 有很多可以更好观察 ByteCode 的方法
- 终端命令:
javap <class文件路径>
,-v
参数详细查看 - JBE 可以直接修改
- JClassLib - IDEA 插件之一
- 终端命令:
- 文件结构
- General Information(通用信息): 这个部分包含了一些通用的信息,比如文件的魔数(magic number),文件版本号等。
- Constant Pool(常量池): 常量池是一个重要的数据结构,它存储了一系列常量,包括字面值、符号引用、类和接口的名称等。常量池在字节码文件中起到了存储和管理常量数据的作用。
- Interfaces(接口): 这部分定义了该类实现的接口。
- Fields(字段): 这部分描述了类的字段(成员变量),包括字段的名称、类型以及修饰符等信息。
- Methods(方法): 这部分描述了类的方法,包括方法的名称、参数列表、返回类型、字节码指令等。
- Attributes(属性): 属性提供了关于类、字段或方法的其他信息,例如源代码行号、注解等。这个部分可以包含多个不同类型的属性,每个属性都有一个名称和相应的数据。
如下图示:
- magic:魔数,是一个固定的字节序列,用于标识 Java 字节码文件。它的值为 0xCAFEBABE,用于确定文件是否是有效的 Java 字节码文件。
- minor version:次版本号,指示 Java 编译器版本的次要更新。用于描述字节码文件与Java虚拟机版本的兼容性。
- major version:主版本号,指示 Java 编译器版本的主要更新。同样用于描述字节码文件与 Java 虚拟机版本的兼容性。
- constant_pool_count:常量池计数,表示常量池中常量的数量。十六进制 0010 转化为十进制为 16。常量池真正存储内容的时候存了 16 -1 项。后面的内容都是引用它。
- #1::常量池项,通常以 # 开头,表示一个常量池中的单个项。
- access flags:访问标志,描述类或接口的访问级别和特性。
- this_class:当前类的索引,指示当前类在常量池中的位置。
- super class:父类的索引,指示父类在常量池中的位置。
- interface_count:接口数量,表示该类实现的接口数量。
- interfaces:接口列表,描述该类实现的接口在常量池中的位置。
- fields_count:字段数量,表示该类中声明的字段数量。
- methods_count:方法数量,表示该类中声明的方法数量。
- method_info:方法信息,描述方法的访问标志、方法名、参数列表等。
- attribute_count:属性数量,表示该类的属性数量。
每个部分描述了 Java 字节码文件的不同方面,从类的声明到方法的定义,以及与常量池等相关的信息。这些信息在 Java 虚拟机中被解析和使用,以正确地加载和执行 Java 类。
Java 的汇编指令有 200 多条。
# 类加载 - 初始化
# 加载过程
加载、链接和初始化是 Java 程序运行时的三个主要阶段。
Loading -- 加载
Linking -- 链接
- Verification -- 验证
- Preparation -- 准备
- Resolution -- 解析
Initializing -- 初始化
具体来说:
Loading(加载): 这是类加载过程的第一个阶段。在这个阶段,Java 虚拟机(JVM)会从类的外部源加载类的二进制数据,通常是从磁盘文件中加载,但也可以是网络、内存等。
加载器将类的二进制数据(class文件)从外部源加载到内存中,并将其放置在运行时数据区的方法区内。
Linking(链接): 这是加载过程的第二个阶段,它将类的二进制数据链接到 JVM 的运行时状态。
链接过程分为以下三个步骤:
- Verification(验证):确保类的二进制数据符合 JVM 规范,不违反类加载的安全性要求。这个步骤确保类的结构正确,不会引发运行时错误。
- Preparation(准备):为类的静态变量分配内存空间,并设置默认的初始值(通常为零值)。这个步骤为类变量分配内存并初始化,但不会为实例变量分配内存。(把 class 文件的静态变量赋默认值,比如 int 类型的默认值为 0)
- Resolution(解析):将(class文件里面的常量池里面用到的)符号引用转换为直接引用,解析动作可以在运行时再完成,也可以在编译时静态完成。这个步骤处理类、接口、方法和字段的符号引用,将其解析为实际的引用。
Initializing(初始化): 这是链接过程的最后一个阶段,也是类加载的最终阶段。在这个阶段,类的静态初始化器会被执行,初始化静态字段和执行静态块。这个阶段是在类被首次使用时触发的,例如创建类的实例、访问类的静态字段等。(调用类初始化代码
<clinit>
,给静态成员变量赋初始值)
总结来说,类加载过程涉及到从外部源加载类的二进制数据,然后将其链接到 JVM 的运行时状态,最终进行初始化。链接阶段包括验证、准备和解析,而初始化阶段则会执行类的静态初始化器。这个过程确保类在 Java 虚拟机中正确加载和使用。
加载之后会发生什么?
任何一个 class 被加载到内存之后,会生成了两块内容。
- 第一块内容是这个二进制的,这块东西确实被落到内存,放到了内存的一块区域,可以原封不动的扔进去。
- 第二个,它生成了一个 class 类的对象,通过以后其他的那些我们自己写的对象去访问这个对象,通过这个对象去访问 class 类的文件,所以生成一个 class 的对象,这个 class 对象是指向了这块内容。
在 Java 虚拟机中,类的元数据,包括类的结构信息、字段、方法、父类、接口等,都会被加载到方法区(元空间Metaspace,替代了永久代PermGen)中。Class 对象本身也是类的元数据之一,因此 Class 对象也存放在方法区中。 Class 对象在内存中的位置可以看作是类的描述符,用来操作该类的字节码以及其他相关的元数据。
可以看看下面这两篇文章:
# 类加载器
JVM 它本身有一个类加载器的层次,这个类加载器就是一个普通的 class,JVM 有一个类加载器的层次,分别来加载不同的 class。或者说,JVM 里面所有的 class 都是被类加载器给加载到内存的,那么这个类加载器简单说我们可以把它叫做 ClassLoader。
注意:从下往上,是委托给父加载器,不是继承关系,是语法上的一种关系。
在 Java 虚拟机中,类加载器按照层次结构进行组织,分为三个主要层次:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader,也称为系统类加载器)。这些类加载器形成了类加载器的双亲委派模型。
- 启动类加载器(Bootstrap ClassLoader):这是 Java 虚拟机的一部分,用于加载 Java 核心库(例如
java.lang
包中的类)。它是所有其他类加载器的父加载器,但它本身不是一个普通的 Java 类,因此在源码中并没有对应的类。它位于虚拟机内部,通常用本地代码实现。它是最顶层的类加载器,负责加载 Java 核心类库。 - 扩展类加载器(Extension ClassLoader): 扩展类加载器用于加载 Java 虚拟机的扩展类库,位于
jre/lib/ext
目录中的类。它的父加载器是启动类加载器。 - 应用程序类加载器(Application ClassLoader):这个加载器也称为系统类加载器,它负责加载应用程序的类,包括用户自定义的类和第三方库中的类。它的父加载器是扩展类加载器。
启动类加载器、扩展类加载器和应用程序类加载器构成了类加载器的层次结构,通过双亲委派模型来保证类的加载的一致性和安全性。在加载一个类时,首先会尝试由父加载器加载,只有在父加载器无法加载时,子加载器才会尝试加载。这个模型可以防止类的重复加载,同时保证了类的隔离性。
在启动类加载器加载的类中,有一部分是虚拟机内部的类,比如 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()); // 解开注释就会报空指针异常
}
}
2
3
4
5
6
7
8
9
输出结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
null
sun.misc.Launcher$ExtClassLoader@4554617c
null
2
3
4
# 双亲委派
- 父加载器
- 父加载器不是【类加载器的加载器】!!!也不是【类加载器的父类加载器】
- 双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程
- 思考:为什么要搞双亲委派?
- java.lang.String 类由自定义类加载器加载行不行? -- 不行,因为不安全
先是自底向上检查是否已经加载,然后再回过头来找 class 并加载,查看是否加载成功,一直到底都没加载成功就会抛出一个 class 找不到异常。
这个缓存是在哪缓存?
可以简单的认为是它自己内部维护的一个容器,一个 list 或者一个数组加载的东西都扔在里面。每个加载器都有自己的缓存。
# 为什么类加载器要使用双亲委派?(重要)
主要是为了安全。次要原因是资源浪费问题,避免重新加载问题(防止类重复加载)。
# 类加载器范围
来自 Launcher 源码
- 启动类加载器 Bootstrap ClassLoader:
- 加载路径:
sun.boot.class.path
- 范围:它负责加载 Java 核心类库,这些类库包括 Java API 中的基础类,如
java.lang
包下的类等。 - 作用:它是 Java 类加载器中最顶层的类加载器,负责加载虚拟机自身需要的类,以及在运行期间会被系统使用的类。由于 Bootstrap ClassLoader 是用本地代码来实现的,所以在 Java 中无法直接获取到该类加载器的引用。
- 加载路径:
- 扩展类加载器 ExtensionClassLoader:
- 加载路径:
java.ext.dirs
- 范围:它负责加载 Java 的扩展库,这些库位于 JRE 的
lib/ext
目录或者由系统变量java.ext.dirs
指定的路径。 - 作用:它负责加载一些 Java 标准扩展库以及一些自定义的扩展库,这些库通常是一些可选的功能。扩展类加载器是由
sun.misc.Launcher$ExtClassLoader
实现的。
- 加载路径:
- 系统类加载器 AppClassLoader:
- 加载路径:
java.class.path
- 范围:也被称为系统类加载器,它负责加载用户类路径(Classpath)上指定的类。
- 作用:它负责加载应用程序中的类,包括开发者自己编写的类以及引用的第三方类库。应用类加载器是由
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()));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输出结果:
根目录下加载
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 自定义类加载器
当你想加载某个类的时候,可以调用 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("");
}
}
2
3
4
5
6
7
8
9
# loadClass 源码解析
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
主要步骤如下:
- 首先,方法会尝试从已加载的类中查找目标类是否已经加载,如果已经加载,则直接返回该类。
- 如果目标类未加载,它会尝试委派给父类加载器来加载。这是双亲委派模型的核心思想之一。父类加载器会重复这个过程,直到到达启动类加载器(Bootstrap ClassLoader)为止。
- 如果父类加载器也无法加载目标类,则
loadClass
方法会调用findClass
方法,尝试自己加载目标类。这是子类加载器在加载自己类的最后一步。 - 最后,如果
resolve
参数为true
,则会调用resolveClass
方法,用于解析加载的类,确保类的完整性和正确性。
什么时候需要自己去加载?
加载进去就生成 class 对象吗?
不是,要经过 初始化 才生成 class 对象。
# 自定义一个类加载器需要做什么?
只需要做一件事,就是定义自己的 findClass 就可以了。
具体来说:
继承 ClassLoader
重写模板方法 findClass
- 调用 defineClass(byte[] -> Class clazz)
加密:可自定义类加载器加载自加密的 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!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
这段代码实现了一个自定义的类加载器 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
2
3
4
5
# 加密
大家都知道 java 的代码 class 文件很容易就被反编译了。
但是我要是定义自己的格式,我不想让别人反编译,这时候怎么办?
你可以通过自定义的 class loader 来进行。然后在写逻辑的时候加一个加密操作。
# 编译器
三大模式:
- 解释器(bytecode intepreter) -- 解释模式
- JIT(Just In-Time compiler) -- 编译模式
- 混合模式
- 混合使用解释器 + 热点代码编译(编译成本地代码,就不用在解释器里面解释来执行了,效率提升)
- 起始阶段采用解释执行
- 热点代码检测
- 多次被调用的方法(方法计数器:监测方法执行频率)
- 多次被调用的循环(循环计数器:检测循环执行频率)
- 进行编译
为什么不干脆直接编译成本地代码,那执行效率不更高吗?
有两个原因。
- 第一,Java 的解释器,现在它的效率也已经非常高了,在一些简单代码的执行上,它并不输于你编译成本地代码。
- 第二,如果你一个执行的文件特别多,各种各样的类库的时候,好几十个 class,你上来二话不说先在内存里编译一遍,这个启动过程会长得吓人。所以它现在默认的模式是混合模式。
可以用指明参数的方式指定用什么模式
-Xmixed 默认为混合模式
开始解释执行,启动速度较快,对热点代码实行检测和编译
-Xint 使用解释模式,启动很快,执行稍慢
-Xcomp 使用纯编译模式,执行很快,启动很慢(要编译的类少的时候,启动也会很快)
# 懒加载
lazyloading
- 严格讲应该叫 lazylnitializing
- JVM 规范并没有规定何时加载
- 但是严格规定了什么时候必须初始化,五种情况
- 使用 new,getstatic,putstatic,invokestatic 指令时,访问 final 变量除外
- java.lang.reflect 对类进行反射调用时
- 初始化子类的时候,父类必须首先初始化
- 虚拟机启动时,被执行的主类必须初始化
- 动态语言支持 java.lang.invoke.MethodHandle 解析的结果为 REF_getstatic, REF_putstatic, REF_invokestatic 的方法句柄时,该类必须初始化
# 拓展知识
# 双亲委派如何打破?
parent 是如何指定的,打破双亲委派
- 用
super(parent)
指定 - 双亲委派的打破
- 如何打破:重写
loadClass()
方法 - 何时打破过?
- JDK1.2 之前,自定义 ClassLoader 都必须重写 loadClass()
- ThreadContextClassLoader 可以实现基础类调用实现类代码,通过 thread.setContextClassLoader 指定
- 热启动,热部署
- osgi tomcat 都有自己的模块指定 classloader(可以加载同一类库的不同版本)
- 如何打破:重写
# 类加载器涉及到了哪些设计模式?
在 Java 虚拟机 (JVM) 中的类加载器 (ClassLoader) 实现中,通常使用了以下两种设计模式:
- 委托模式 (Delegation Pattern):ClassLoader 的实现通常使用了委托模式来处理类加载请求。委托模式指的是当一个对象收到请求时,它将请求委托给其他对象来处理。在类加载器的情况下,当一个类加载器接收到加载类的请求时,它首先会将请求委托给父类加载器进行处理。如果父类加载器无法加载该类,子类加载器才会尝试加载类。这种委托模式的设计方式可以实现类加载器的层次结构,从而实现类加载器的隔离和类加载的委派。
- 单一职责模式 (Single Responsibility Pattern):ClassLoader 的主要责任是加载类文件并定义对应的类。ClassLoader 的设计符合单一职责模式,即一个类应该只负责一项职责。ClassLoader 将类加载的职责封装在一个独立的类中,从而使得类加载的逻辑与其他功能解耦,提高了代码的可维护性和可扩展性。
除了委托模式和单一职责模式,ClassLoader 的实现可能还涉及其他设计模式,具体取决于实际的实现细节和需求。例如,一些类加载器的缓存机制可能使用了享元模式 (Flyweight Pattern) 来提高性能和资源利用效率。