classLoader加载class过程
都是通过classLoader加载的class,如果已经加载过则不可以再次加载,但是可以通过不同的classLoader加载同一个class
都有哪些类加载器呢
- 引导类加载器=BootstrapClassloader
是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等
由于是C++实现的所以通过Object.class.getClassLoader() == null,无法访问 - 扩展类加载器=ExtClassLoader
是由Bootstrap加载的此类
此类加载器默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包,当然也可以加载由java.ext.dirs系统属性指定的jar包,用来加载java的扩展库,用户也可以直接使用此类加载器 - 应用类加载器=AppClassLoader
是由AppClassLoader加载的此类
此类加载器默认加载用户编写的class
双亲委派
意思是多个亲戚,将加载class的任务委任给多个亲戚。以上三个类加载是都是有对应的加载关系的。如果加载某个class时,他会把任务交给上层处理,上层处理不了在交给上层处理,直到上层加载不了,然后在自己加载。
为什么这样设计呢?比如说String.class,在运行时就已经加载了,我们能重新覆盖吗?并不能,双亲委派机制就是要保证class正常只加载一次
上层加载的class不能引用下层加载的class
URLClassLoader
扩展类加载器ExtClassLoader和应用类加载器AppClassLoader,都继承URLClassLoader
,在加载class时根据所在的路径读取class内容加载的。
如果我们从新写一个String.class,包名和jdk的String一模一样,是加载不了的。因为上层的classLoader已经加载过了。
如果自己写一个没有加载过的class,同时依赖的第三方包中也有这个class,那么可以加载自己写的吗,答案是:和启动时的classPath参数有关
AppClassLoader初始化的代码
1 | static class AppClassLoader extends URLClassLoader { |
class加载过程
1 加载class
读取class的二进制字节流,默认通过URLClassLoader#findClass
读取内容
2 连接class
此步骤由C++实现
2.1 验证
验证JVM是否支持对应的字节码语法
2.2 准备
静态变量分配内存空间,并将其赋予默认值(0,false,null等)
如:static int num = 50;
则此步骤为变量num分配空间,并赋值为0
如:static Object obj = 1;
则此步骤为变量obj分配空间,并赋值为null
分配内存空间,到底要分配多少呢?详情请看java内存模型和GC
如下代码,你猜会输出几?答案是0(默认值),为什么呢?
1 | static class Test { |
为什么输出是0,而不是8?看完初始化你就明白了
2.3 解析
将类中的符号引用转换为直接引用
编译的class字节码都是符号引用,符号的意思就是占位符,因为在实际运行当中要知道明确的地址才能调用
所以在这个解析的阶段,如果有引用其他的class就会加载其他的class到内存中,然后才能得到对应的内存地址
知道内存地址意味着可以直接调用(也就是直接引用)
3 初始化class
1 | import lombok.SneakyThrows; |
运行main方法,如果不看答案的话你能准确的说出他的输出内容及顺序吗?
即使你看过网上一大堆的初始化文章之后,98%的人都答不对,先透露一下总共输出12个,最后几个输出的是
第11个输出: B static after new B(), f_o_12:12
第12输出: 12
总结初始化顺序
- 优先级最高的:如果是static final 修饰的Java 基本类型则不会初始化class,可以直接访问,称之为常量
- 如果是访问static的变量或者是new对象,优先初始化顶级父类的 static 修饰的静态字段或静态块,按照声明的顺序初始化,然后是子类
如果static声明的字段或者方法块引用到其他的class,则会初始化其对应的class,如果已经或正在初始化,可以直接使用
注意:如果正在初始化,则class的内容只做了准备的阶段,所以class里面的属性都是null或者都是默认值 - 如果是创建实例,则上面的静态块初始化完之后,按照声明的顺序在初始化代码块,当一个类所有的代码块初始化完之后会初始化构造方法。然后是子类的代码块子类的构造方法