类的加载阶段分别有:
加载,验证,准备,解析,初始化
其中只有加载是java层面完成的,将class文件载入到内存,后续都是通过调动native方法完成。
JAVA层面的类加载
类是由类加载系统创建的,没有类加载器,也就没法执行任何java程序,所以只能由C++先构件类加载系统。
用户执行main.class → C++初始化虚拟机 → C++创建BootstrapClassLoader →
C++使用BootstrapClassLoader加载sun.misc.Launcher类 →
C++创建Launcher类 →
Launcher在构造函数中构件完整的类加载系统 →
C++通过Launcher.getClassLoader找到AppClassLoader,用于加载main所在的类 →
C++调用静态方法mian → 用户程序开始执行
public Launcher() {
//先创建ExtClassLoader
//extClassLoader的父级加载器是bootstrapClassLoader, 但并不是通过parent属性传递的
//而是在loadClass方法中通过代码处理的,因为bootstrapClassLoader是C++对象,无法直接引用。
Launcher.ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader();
//然后创建AppClassLoader,将extClassLoader设置为appClassLoader的父加载器
//将appClassLoader设置为系统默认加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
SecurityManager var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
System.setSecurityManager(var3);
}
public ClassLoader getClassLoader() {
return this.loader;
}
双亲委派
BootstrapClassLoader
↑ ↓
ExtClassLoader
↑ ↓
AppClassLoader
java的类加载:
protected Class<?> loadClass(String name)
{
//检查是否已被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
//若没有被加载过,委托给父级加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//若没有父级(只有extClassLoader没有父)
//委托给引导类加载器
c = findBootstrapClassOrNull(name);
}
if (c == null) {
//所有上级都没加载,自身开始加载
c = findClass(name);
}
}
return c;
}
双亲委派组成了金字塔式的结构,通过最底部的加载器进行类加载,底部的加载器先不做任何处理直接委托给上级加载,直至最顶级;当顶级不处理时,才交给下级处理,下级才开始尝试使用findClass进行类加载。金字塔中越高层的加载器,加载的优先级就越高,
若是从金字塔顶部(BootstrapClassLoader)开始,就可以少遍历一次,为什么没有这么设计呢?
因为顶部加载器只负责加载java运行时的内部类,数量不多; 而用户类则可能很庞大,若是从顶级加载器开始,找内部类会很快, 但找用户类就比较麻烦。若是用户实现了很多的自定义加载器,组成了树形结构,又产生了分支问题。
作用
沙箱安全:内部类通过上级加载器加载,用户无法再覆盖类定义
实现类名的唯一性:通过类名询问了所有的类加载器,保证了不会被重复加载。
自定义类加载器
网络加载类:重写findClass的字节码加载部分。
热加载类:通过废弃类对应的classloader,从而使类重新被加载。
打破双亲委派的应用
通过类名实现了唯一性,虽然有好处,但也存在不方便的地方。
比如同一个类名,不同版本的类需要同时存在。
tomcat通过打破双亲委派实现了web容器的互相隔离,同一个类名,可以多版本共存。
class字节码如何转存到方法区
class文件结构
类Cat
按16进制打开后
class文件的前半部分具有一定的阅读性,都是各种类名和字符串,它是class文件的常量区,编译器将我们的java文件中固定不变的部分(各种名称:包名,类名,属性名,方法名,引用的类名,方法名,java的常量等)全部提取到一块,并赋予一个编号#1 #2 #3等
在指令逻辑部分,就可以直接使用#1 #2等编号来代替常量
常量池后面几乎都是复杂的结构体(给C++使用),需要通过工具转成一定的结构才能阅读。
通过javap命令可以美化class文件的展示
javap -v Cat.class
Classfile /E:/maomaotou/test/target/classes/com/maomaotou/test/Cat.class
Last modified 2023-3-14; size 577 bytes
MD5 checksum 084a57aed2d276b98b66f5871a47afd8
Compiled from "Cat.java"
public class com.maomaotou.test.Cat
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
//常量池,几乎是拆解了的java文件零件盒
Constant pool:
#1 = Methodref #10.#24 // java/lang/Object."<init>":()V
#2 = String #25 // mi
#3 = Fieldref #9.#26 // com/maomaotou/test/Cat.name:Ljava/lang/String;
#4 = Class #27 // java/lang/StringBuilder
#5 = Methodref #4.#24 // java/lang/StringBuilder."<init>":()V
#6 = String #28 // i am
#7 = Methodref #4.#29 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#30 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #31 // com/maomaotou/test/Cat
#10 = Class #32 // java/lang/Object
#11 = Utf8 name
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/maomaotou/test/Cat;
........
{
//构造函数
public com.maomaotou.test.Cat();
//返回void
descriptor: ()V
//public权限
flags: ACC_PUBLIC
Code:
//stack操作数栈深度为2(可见操作数栈是编译器已经确定了)
//locals本地变量表数量为1
stack=2, locals=1, args_size=1
//取this
0: aload_0
//调用父类构造函数
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
//设置name属性
5: ldc #2 // String mi
7: putfield #3 // Field name:Ljava/lang/String;
10: return
//指令行与java代码行映射,用于报错时提示java代码行
LineNumberTable:
line 3: 0
line 4: 4
//本地变量表详情
//start 指令行 length 长度 slot 槽 name变量名
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/maomaotou/test/Cat;
public java.lang.String say();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: new #4 // class java/lang/StringBuilder
3: dup
4: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
7: ldc #6 // String i am
9: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_0
13: getfield #3 // Field name:Ljava/lang/String;
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: areturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/maomaotou/test/Cat;
}
//源文件
SourceFile: "Cat.java"
验证阶段
主要验证的是class文件规范,格式、语义、指令是否正常等; 但是它无法对class文件与java文件是否一致做验证, 如果在class文件修改code区指令, 并且符合规范,则会被正常加载。 比如将某个update操作,改为delete,就能实现破坏性行为。
准备
为静态变量分配内存。
解析
在上面看到的class文件中,常量池是通过123编号关联指令的,当要加入到运行时内存时,有很多class文件一同被加入,肯定不能再用这个编号了。解析阶段就是将这些符号引用(#1 #2),转为直接引用(内存地址)。
初始化
为静态变量初始化值