获取中...

-

Just a minute...

解释java字节码

为什么需要编译成class文件

我们首先要理解JVM
java是是一种混合型语言
有编译的步骤也有解释步骤
我们编写的java文件需要被编译成class文件之后才能被JVM运行

什么是class文件

传统编译型语言(C C++之类)

1
源代码 → 编译器 → 机器码(.exe 或 .out) → 操作系统直接运行

java

1
源代码 (.java) → javac 编译器 → 字节码 (.class) → JVM 加载并运行

独特之处在于javac编译出的class文件不是针对某个CPU的机器码 是一种字节码 面向的是JVM

  • 机器码:CPU直接执行的二进制指令
  • 字节码:JVM的中间指令 需要有JVM解释或编译成机器码之后才能被CPU执行

我们先随便创建一个java文件

1
2
3
4
5
public class Main{  
public static void main(String[] args){
System.out.println("sU@Su");
}
}

之后使用javac Main.java编译成Main.class(会出现在和Main.java同一目录之下)这个文件是二进制格式 无法直接用记事本看懂
将其放入十六进制工具之中查看一下效果
以上内容遵循JVM规定的ClassFile结构(这是JVM规范之中定义的一种严格二进制格式)

我们可以大致将以上内容分成左中右三部分
左边
诸如00000000:表示当前行起始字节再文件之中规定偏移(十六进制)
中间
诸如CA FE BA BE....:每个字节用两个十六进制数字表示 空格隔开 一共十六个字节
右边
诸如|........A.......|:将每个字节当作ASCII字符显示 非打印字符显示为.

这其中的内容包含魔数 主/次版本号 常量池计数 常量池条目示例 方法字节码等等内容 这些具体是干啥的我们后续再聊 我们要学习的类加载机制加载阶段最核心的内容就是读取这个class文件 按照ClassFile结构解析出魔数 版本 常量池 方法字节码等等内容 之后再方法区之中创建对应的·运行时的数据结构并再堆之中生成一个Class对象

实际上字节码有两种形式 除了我们上面看到的这种二进制形式的 还有助记符形式的
我们可以通过javap -v Main.class查看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
D:\java study\newjava\src>javap -c Main.class
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String sU@Su
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

其中的getstatic ldc这些 都是JVM字节码指令的文本助记符 每条助记符对应一共二进制操作码 (比如ldc对应0x12)
这些内容是为了帮助人类阅读设计的 但是JVM实际执行时候读取的是二进制字节码
我们来解析一下整体结构

1
2
3
4
5
Compiled from "Main.java"
public class Main {
public Main();
public static void main(java.lang.String[]);
}

第一行的Compiled from "Main.java"代表这个class文件是由Main.java 编译得到
第二行的public class Main展示了这个类的访问修饰符和名称
剩下两行分别展示了类之中定义的两个方法
默认构造器public Main()public static void main(String[])

我们取默认构造器之中规定一段来解释示例一下

1
1: invokespecial #1                  // Method java/lang/Object."<init>":()V
  • 前面这个1是代表地址
  • invokespecial代表指令
  • 操作数#1
  • 以上内容实际上就是调用常量池之中编号为#1的方法 根据注释 是java/lang/Object."<init>":()V 也即是object类的无参构造器 invokespecial用于调用构造器 私有方法或者父类方法 这里是弹出栈顶的this 作为目标对象 之下object的构造器
    来重点分析一下main方法
1
2
3
4
5
6
7
  public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String sU@Su
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

这个方法设计栈帧的操作

  • 第一个命令获取静态字段System.out 其中#7是常量池之中规定索引 通关注释可以知道她指向的 java/lang/System.out:Ljava/io/PrintStream; 作用是将其压入栈顶
  • 第二个命令是从常量池将在一共常量 其中#13是对应的常量字符串”Su@sU“ 作用是将haha压入栈顶
  • 第三个命令是调用实例方法 其中#15表示这个方法再常量池的索引 也就是 PrintStream.println:(Ljava/lang/String;)V 之后incokevirtual会从栈顶弹出参数”Su@sU“和对象引用”System.out“ 之后调用printlen方法 执行后打印Su@sU到控制台
  • 第四个命令是消除这个栈帧
    示意图如下
1
PrintStream引用 ]
1
[ PrintStream引用, "Su@sU"引用 ]
1
[ PrintStream引用 ]"Su@sU"引用
1
[   ]"Su@sU"引用  PrintStream引用
1
[   ]打印Su@sU(完成之后将栈帧消除)

经过以上解释大家肯定堆常量池和栈帧有所疑惑
我们来讲解一下

栈帧

JVM再执行方法的时候会创建一共栈帧 这个栈帧主要有两个部分组成

局部变量表

存放方法的参数和局部变量(比如 String[] args 就在里面)

操作数栈

类似一个计算器 所有的计算 传参 返回值都是通过这个栈进行
这也就是我们上面演示的部分 也就是我们广义之上所说的栈

常量池

常量池是class文件之中的一块表格 存放了这个类用到的各种常量信息
类名 方法名 字段名 字符串字面量 数字常量等等

当 javac 编译时,遇到 "Su@sU" 它就在常量池里新建一个条目 保存这个字符串的内容 并分配一个编号(例如 #13)。
那么字节码里就不需要直接写 "Su@sU" 这个文本(会浪费空间) 而是写 ldc #13 意思就是“到常量池的第13号条目去取那个字符串”

我们可以通过javap -v来·查看
这个命令得到的是完整的类结构信息
包括

  1. 版本信息
  2. 常量池
  3. 类访问标志
  4. 字段表
  5. 方法表
  6. 属性信息
1
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
63
64
65
66
67
D:\java study\newjava\src>javap -v Main.class
Classfile /D:/java study/newjava/src/Main.class
Last modified 2026年5月7日; size 407 bytes
SHA-256 checksum 5da74c98f668f3c3e7c2b561638bcd8c8fcb531df08a1b71a7b1778cd2ea1abd
Compiled from "Main.java"
public class Main
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // Main
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // sU@Su
#14 = Utf8 sU@Su
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // Main
#22 = Utf8 Main
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 Main.java
{
public Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String sU@Su
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Main.java"

可以比较直观的看到常量池等信息

学习以上字节码的内容堆我们后续学习类加载机制很有帮助 因为类将在的这个过程本质上就是堆字节码的验证处理解析等过程 我们可以后续说明

我们先要了解类加载的过程如下所示
加载(Loading) → 连接验证(Verification) → 准备(Preparation) → 解析(Resolution) )→ 初始化(Initialization) → 使用(Using) → 卸载(Unloading)

其中连接阶段包含验证 准备 解析三个子阶段

类加载过程之中每个阶段详细解释

加载阶段

  1. 通过一个类的全限定名称获取定义此类的二进制字节流(也就是我们之前javac得到的class文件)
  2. 讲这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
  3. 再对内存之中生成一个代表这个类的java.lang.Class对象 作为方法去各种数据的访问入口(这个对象也就是我们通过反射API获取的那个class对象)

加载的具体动作是由类加载器(ClassLoader) 完成

连接阶段

子阶段一 验证阶段

这一步是为了保证JVM安全 对字节码解析合法性校验 防止恶意或者错误代码破坏JVM

  • 文件格式验证:魔数、主次版本号、常量池类型等。
  • 元数据验证:是否有父类、是否继承了 final 类、是否实现了抽象方法等语义检查。
  • 字节码验证:通过数据流和控制流分析,确保方法体在运行时不会危害 JVM(如类型转换安全、操作数栈类型匹配等)。
  • 符号引用验证:在解析阶段发生,检查符号引用能否正确解析为直接引用(如能不能找到被引用的类、方法、字段)。

子阶段二 准备阶段

为类静态变量分配内存并且设置默认零值

此处有两个特点

  • 只针对静态变量 实例变量则是再对象实例化时随对象分配到java堆之中
  • 分配的值是默认零值(int-0 boolen-false 应用类型-null)
    除非这个静态变量同时被哦final和static修饰并且值是编译期常量 那么此时就会直接赋值为代码之中的值

子阶段三 解析阶段

将常量池之中的符号应用替换成直接引用
符号引用

  • 本质是一串字符串 用来描述目标的名字和描述信息

  • 特点是不依赖内存布局 只再 .class 文件的常量池中记录 即使 JVM 还没加载目标类 也能存在

              直接引用
    
  • 本质是JVM可以直接使用的内存指针 偏移量等

  • 特点是依赖内存布局 必须等目标类呗加载到内u你 分配好地址之后才能得到

**为什么要进行区分
java程序在编译时不知道所引用的类 方法 字段 在运行时候的具体内存位置 但是JVM在执行的时候必须知道这些位置才能实际访问

JVM 在执行字节码指令时(例如 getstaticinvokevirtual),需要直接操作内存地址或偏移量 如果不转换 每次执行都要根据符号引用重新查找(例如按名字查找字段 按名字+描述符查找方法) 效率极低 通过解析阶段一次转换 后续直接使用,可以大幅提升执行效率

初始化阶段

  • 生成并且执行<clinit()>方法
    编译器会自动收集类中所有静态变量的赋值语句和所有的静态代码块 之后会合并后成一个特殊的类加载器——“<clinit>()”方法
  • 如果类有父类 且父类尚未初始化 JVM会先递归初始化父类 再初始化当前类——这一步保证了父类的静态成员再子类之前完成初始化
  • JVM会为类初始化加上锁 保证<clionit>()方法在多线程环境之下只会被执行一次 其他线程如果同时触发初始化 会被阻塞到初始化完成

初始化的作用

  1. 在准备阶段·的时候 静态变量已经被分配内存并且设置为数据类型的零值 这一步执行赋值语句 赋值我们真正想赋值的值
  2. 有些复杂的行为 比如说读取配置文件 注册驱动 初始化日志这些 可以通关将这些逻辑卸载static{ }之中 强制在类加载的时候执行一次
1
2
3
4
5
6
public class Parent {
static { System.out.println("Parent init"); }
}
public class Child extends Parent {
static { System.out.println("Child init"); }
}

访问child类的时候输出结果会是

1
2
Parent init
Child init

这也证明了先父类后子类的初始化顺序

初始化是类加载和类使用之间的重要一环 初始化之后类才能被使用

类加载器分类

启动类加载器(Bootstrap ClassLoader)

实现语言:C++
加载路径:

  • JAVA_HOME/jre/lib 目录下的核心库(如 rt.jarresources.jarcharsets.jar 等)。

  • 被 -Xbootclasspath 参数指定的路径中的类。
    典型类:java.lang.Objectjava.lang.Stringjava.util.* 等所有 Java 标准库的核心类。

这是所有类加载器的源头 在java代码之中是无法直接获取引用的 任何通关类.getClassLoader()获取启动类加载器加载的类时 都会返回null

1
2
3
4
5
6
7
public class TestClassLoader {
public static void main(String[] args) {
// String 类由 Bootstrap 加载,getClassLoader() 返回 null
ClassLoader cl = String.class.getClassLoader();
System.out.println(cl); // 输出 null
}
}

几乎所有java.* 包下的基础类几乎都由 Bootstrap 加载

拓展类加载器(Extension ClassLoader)

java类名:sun.misc.launcher$ExtClassLoader
父加载器:启动类加载器
加载路径:

  • JAVA_HOME/jre/lib/ext 目录下的所有 JAR 包。

  • 或被系统属性 java.ext.dirs 指定的路径中的类库。
    用于加载java的拓展功能
    访问:ExtClassLoader 是 Java 对象,可以通过 ClassLoader.getSystemClassLoader().getParent() 获取

1
2
3
4
5
6
7
public class TestExtClassLoader {
public static void main(String[] args) {
// X509Certificate 通常位于 jre/lib/ext 中(比如 dnsns.jar 或类似扩展)
ClassLoader cl = javax.security.cert.X509Certificate.class.getClassLoader();
System.out.println(cl); // 典型输出:sun.misc.Launcher$ExtClassLoader@...
}
}

扩展类加载器加载 javax.* 中的部分扩展类以及存放在 lib/ext 中的第三方库

应用程序类加载器(Application ClassLoader)

java类名:sun.misc.Launcher$AppClassLoader
父加载器:拓展类加载器
加载路径:

  • 环境变量 CLASSPATH 或 -classpath 或 -cp 指定的路径。

  • 如果没有指定,则默认是当前工作目录(.)。
    加载用户自己编写的类以及第三方jar包
    在大多数应用中 AppClassLoader 是线程上下文类加载器的默认值 也是 ClassLoader.getSystemClassLoader() 的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyClass {
// 空类
}

public class TestAppClassLoader {
public static void main(String[] args) {
// 我们自定义的类,由 AppClassLoader 加载
ClassLoader cl1 = MyClass.class.getClassLoader();
System.out.println(cl1); // 输出 sun.misc.Launcher$AppClassLoader@...

// 也可以获取系统类加载器本身
ClassLoader systemCl = ClassLoader.getSystemClassLoader();
System.out.println(systemCl); // 同样是 AppClassLoader
}
}

几乎所有你自己写的类和通过 Maven/Gradle 引入的 jar 包中的类 都由 AppClassLoader 加载

自定义ClassLoader

  • 加载特定位置的类(如网络、加密文件、数据库)

  • 实现热部署(重新加载类)

  • 类隔离(如 Tomcat 为每个 Web 应用创建独立的类加载器)

类加载器获取方法

Class.getClassLoader()

返回加载这个类的类加载器引用
若返回null则表示这个类是由类加载器加载

ClassLoader.getParent()

获取当前类加载器的父加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package java_foundation;  

public class LoaderChain {
public static void main(String[] args) {
// 1. 当前类(LoaderChain)的类加载器 = AppClassLoader ClassLoader appCl = LoaderChain.class.getClassLoader();
System.out.println("AppClassLoader: " + appCl);

// 2. 父加载器 = ExtClassLoader ClassLoader extCl = appCl.getParent();
System.out.println("ExtClassLoader: " + extCl);

// 3. 父加载器的父加载器 = Bootstrap (null) ClassLoader bootCl = extCl.getParent();
System.out.println("Bootstrap: " + bootCl); // null

// 验证 String 由 Bootstrap 加载
System.out.println(String.class.getClassLoader()); // null
}
}

输出结果为

1
2
3
4
AppClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c
ExtClassLoader: jdk.internal.loader.ClassLoaders$PlatformClassLoader@4e50df2e
Bootstrap: null
null

值得注意的是在JDK9之中将扩展类加载器(ExtClassLoader) 变成了 平台类加载器(PlatformClassLoader)
但是委派链

1
AppClassLoader  →  PlatformClassLoader  →  Bootstrap (null)

依然没变

ClassLoader.getSystemClassLoader()

返回系统类加载器 通常返回的是(appClassLoader)

双亲委派模型

定义:当一个类加载器收到加载某个类或者接口的请求的时候 不会立即尝试自己家在 而是将加载任务委派给父加载器去完成 每一级类加载器都遵循同样的规则 如此递归 直到最顶层的启动类加载器都反馈无法完成加载的时候 自家在其才会尝试自己加载

设计双亲委派模型的作用是什么

保护Java核心类

假设没有双亲委派模型 攻击者可以直接编写自定义的java核心类(比如java.lang.String)里面包含窃取信息或者破坏逻辑的代码 然后将这个类的class文件放在classpath或者当前目录之下
当JVM需要加载这个核心类的时候 引用程序类加载器会先从classpath下找到这个恶意类并且加载 之后所有使用这个核心类的地方都会之下恶意代码

但是在有了双亲委派模型之后 这个核心类就会有于双亲委派的圆规 向上委派给启动类加载器
启动类加载器在JDK核心库之中找到标准的安全核心类然后成功加载 那么攻击者的恶意类就不会被加载

保证类的唯一性

JVM 判定两个类是否相同 依据是 全限定类名 + 加载该类的类加载器实例
双亲委派保证了:对于所有 Java 核心类 最终都由 同一个加载器(Bootstrap) 加载。
因此 整个 JVM 中只存在一个 java.lang.String 类型 即使攻击者通过其他方式(如自定义类加载器)再次加载一个名为 java.lang.String 的类 也会因为:

  • 包名限制(java.* 包禁止非系统加载器加载)或者

  • 类型不匹配(无法与官方 String 相互赋值)

而无法造成全局破坏

类加载器核心方法

loadClass(String name) —— 类加载的入口

这是JVM或反射调用类加载器的公开方法 传入类的全限定名称 并且要求加载这个类

  1. 检查这个类是否已经被当前加载器加载过
  2. 如果没有加载 委派给父加载器去加载
  3. 如果父加载器无法加载 就会调用自己的findClass去实际加载

findClass(String name) —— 实际加载字节码的地方

根据类名 到某个特定位置读取字节码 然后调用defineClass将其转换成class对象
调用实际是在loadClass之中父加载器无法加载这个类的时候 会回调当前加载器的findClass

defineClass(String name, byte[] b, int off, int len) —— 将字节数组转换为 Class 对象

将字节码数组解析成一个Class实例 会完成格式校验 常量池解析等底层工作 最终在JVM之中创建代表这个类的 java.lang.Class 对象
调用时机是在findClass之中获得字节码之后 调用它生成Class对象

resolveClass(Class<?> c) —— 触发类的链接

要求JVM对已经定义的Class对象进行解析(也就是把常量池之中规定符号引用替换成直接引用的那一步)
一般不会主动调用因为 loadClass 有一个 resolve 参数 如果为 true 则会在加载后调用 resolveClass 大多数情况下我们并不需要立即解析

findLoadedClass(String name) —— 检查类是否已经被加载

查询当前类加载器是否已经加载过指定名称的类 也就是loadClass的第一步

1
2
3
4
5
6
7
loadClass(name)                         // 入口(不重写)
→ findLoadedClass(name) // 检查是否已加载
→ parent.loadClass(name) // 委派给父加载器
→ findClass(name) // 父加载器失败后,自己加载(需要重写)
→ 读取字节码(自定义逻辑)
→ defineClass(...) // 将字节码转成 Class 对象
→ resolveClass(...)(可选) // 立即解析符号引用

类加载的方式

主要分为隐式加载和显式加载

隐式加载

当JVM执行字节码的时候遇到某个类的情况 会自动加载这个类
隐式加载的情况包括

  • 使用new关键字创建对象实例
  • 访问或者修改类的静态变量
  • 调用类的静态方法
  • 初始化子类的时候 父类尚未初始化
  • 使用反射
  • 启动类

显示加载

常用的方式有:

  1. Class.forName(String className)``
    会执行类的初始化(执行static)
  2. Class.forName(String name, boolean initialize, ClassLoader loader)
    这种方法的第二个参数initialize控制是否执行初始化
  3. ClassLoader.loadClass(String name)
1
2
ClassLoader cl = MyClass.class.getClassLoader();
Class<?> clazz = cl.loadClass("com.example.MyClass");

这种方法不会执行初始化 (不会触发static)
适用于只希望获取Class对象 延迟初始化或者避免触发静态代码块
4. ClassLoader.findClass(String name)
一般不直接调用 而是在自定义类加载器之中内部重写findClass

通过调试观察双亲委派模型

调试功能是我们学习类加载机制的一个非常便捷的功能
通过设置断点 步入方法 我们可以很详细的了解到一个java程序再执行的时候调用的各种代码逻辑

环境准备和基础操作认识

再IDEA之中随便创建一个类

1
2
3
4
5
6
7
package java_foundation;//委托类  

public class Text implements Event {
public void sale(){
System.out.println("Su@sU");
}
}

为了方便存储我将其放入java_foundation包之内
接下来创建一个加载上一个类的核心类

1
2
3
4
5
6
7
8
9
10
11
package java_foundation;  

import java.lang.ClassLoader;

public class Main {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class clazz = classLoader.loadClass("java_foundation.Text");
System.out.println("加载完成:" + clazz);
}
}

以上代码的逻辑就是获取应用程序类加载器之后使用其loadClass方法加载类java_foundation.Text
准备工作已经完成
由于我们是要研究类的加载 那么就需要对loadClass方法进行研究
Class clazz = classLoader/loadClass("java_foundation.Text")处打上断点
之后进入调试模式点击步进
就进入了ClassLoader.java 可以看到这是类加载器对外提供的简化入口 只需要传入类的全限定名 最终会返回loadClass之后的结果

loadClass(name, false)这里面有两个参数第一个是类的全限定名第二个是是否链接
接下来我们一直点击步进 遇到循环可以选择步出跳过 过程中可能会跳转到别的java类(String Launcher等)无需理会 这是因为我们步进之后再方法的源码之内设计的其他类也不少 在这里我们只需要关注CLassLoader.java的内容就行

到达这一段方法的时候停下 这是双亲委派模型的核心

1
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
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();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader }

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class. long t1 = System.nanoTime();
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;
}
}

我们来分析一下这一段内容

1
Class<?> c = findLoadedClass(name);

检查缓存 查询当前类加载器是否依据在内部缓存之中加载过这个类

1
2
3
4
5
6
7
8
9
10
11
if (c == null) {  
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader

这是双亲委派模型最精彩的地方 :当前加载器不会自己贸然加载 而是先向上询问自己的父加载器 并且递归处理 知道顶层的启动类加载器

1
2
3
4
if (c == null) {  
// If still not found, then invoke findClass in order
// to find the class. long t1 = System.nanoTime();
c = findClass(name);

只有当所有父加载器都找不到这个类的时候 当前加载器才会调用自己的findClass加载字节码

小结

至此我们依据完整走完java类从磁盘上的.class文件到内粗你之中可用规定Class对象的全过程
着重点在于对java字节码的了解和双亲委派模型的体会

相关文章
评论
分享
  • 社会工程学——cupp获取windows密码

    这其实本质是一个社会工程学问题至于为什么 在文章的最后想必大家也能理解假设现在有一个人短时间离开电脑之后并未锁屏 那么旁人就可以通过U盘拷贝重要密码文件的方式获取他的密码 密码文件获取windows并不会直接存储你的明文密码 它存的是...

    社会工程学——cupp获取windows密码
  • URLDNS链入门

    在一些测试环境之中 目标是否发生反序列化是无回显的 是否有一种办法能够测试目标是否能够发生反序列化呢?有的兄弟有的如果我们能让目标服务器通过反序列化发起一次DNS解析请求 那么在接收到请求的时候不就知道了吗那么这就是我们需要学习的UR...

    URLDNS链入门
  • 无痛入门java反序列化

    在学习URLDNS链以及之后的cc链之前 我们得先掌握java反序列化的基础知识 序列化和反序列化序列化把一个java对象转换成字节序列以便存储或者通过网络传输反序列化把字节序列还原成java对象 12序列化:对象->字符串反...

    无痛入门java反序列化
  • 用租房视角学习动态代理

    关于这一部分的知识点 我们先从静态代理说起 静态代理什么是代理? 代理就是:不操作目标对象而是通过一个中间人(代理)来间接操作最常见的现实例子就是租房问题:你要租房子 不需要直接找到房东 而是通过租房中介 中介帮你去和房东沟通 那么...

    用租房视角学习动态代理
  • 动态加载字节码

    动态加载字节码是一种让JVM可以接收由程序在运行时动态产生的字节数组并将其装在的能力 本义是让程序变得更加灵活 但是也产生了一定的安全问题 核心方法在java.lang.ClassLoader类中有一个方法 1protected fi...

    动态加载字节码
  • 反射随笔

    反射概念在java之中 万物都是对象 包括“类本身也是一个对象”(属于java.lang.Class类的对象) 反射的本质就是在程序运行期间 通过操控这个Class对象 反向解析和操控它所代表的哪个类里面的内容(构造器,属性,方法) ...

    反射随笔
  • LangChain反序列化

    最近在打SCTF的时候遇到一个LangChain反序列化的题目 比较新颖 于是想着学习一下 LangChain介绍LangChain是一个用于构建大语言模型应用的开源框架 通过这个可以把常用的组件抽象成可串联的链(Chain)举个例...

    LangChain反序列化
  • filter-chain漏洞演示

    这个漏洞链被称为CNEXT 由安全员Charles Fol披露 核心思想是利用系统内置的看似无害的模块(流过滤器)的巧妙组合 在受限的web环境之中执行命令 原理讲解filter协议对于filter伪协议 我们肯定都不陌生这个伪协议允...

    filter-chain漏洞演示
  • 死亡绕过

    前言以及环境介绍本文是对p神文章谈一谈php://filter的妙用 | 离别歌知识点的学习 本文主要介绍使用filter伪协议进行死亡绕过的三种方式 测试环境:小皮面板测试代码:exit.php 12345<...

    死亡绕过
  • Phar反序列化漏洞演示

    前言我们在之前学习的反序列化漏洞 无一不是利用了unserialize()这个函数 但是我们没有去考虑 假设此时的题目并没有给我们这个函数 我们还能否实现反序列化呢 其实是可以的 在2018年的BlackHat 大会上 Sam Tho...

    Phar反序列化漏洞演示
Please check the parameter of comment in config.yml of hexo-theme-Annie!