1. 类加载过程
1.1 加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
class来源有很多 , 如本地系统 / 网络中的class文件 / JAR包 / java源文件动态编译 等
类加载由JVM提供的类加载器完成
类加载器后面会讲
1.2 连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。
1.2.1 验证
验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
- 文件格式验证 : 验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理
- 元数据验证 : 对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
- 字节码验证 : 最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
- 符号引用验证 : 主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
1.2.2 准备
类准备阶段负责为类的静态变量分配内存,并设置默认初始值, 非静态变量不会分配内存;
1.2.3 解析
将类的二进制数据中的符号引用替换成直接引用。
- 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
- 直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
- 举例 : 比如拿到某人身份证号123456879(符号引用 也就是占位符的意思) , 我们得不到什么有价值的信息,但是去公安局查询的话就能查到这个人的精准家庭地址( 直接引用 )
1.3 初始化
初始化是为类的静态变量赋予正确的初始值
如果类中有语句:private static int a = 10,它的执行过程是这样的
- 首先字节码文件被加载到内存后先进行链接的验证这一步骤,验证通过后准备阶段
- 给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0
- 然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
1.4 类的加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“com.chen.demo”))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
除此之外,下面几种情形需要特别指出:
对于一个 final 类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
2. 类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。
一个类的唯一标志:
类名.包名.类加载器名
2.1 根类加载器
- 它用来加载 Java 的核心类,是用原生代码来实现的
- 并不继承自 java.lang.ClassLoader
(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)
- 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
2.2 扩展类加载器
- 它负责加载JRE的扩展目录
- lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
- 由Java语言实现,父类加载器为null。
2.3 系统类加载器
被称为系统(or 应用)类加载器
,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。
程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。
如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
2.4 类加载器加载Class步骤
-
检测此Class是否载入过,即在缓冲区中是否有此Class,返回对应的
java.lang.Class
对象,否则进入第2步。 -
没有父类加载器
or
父类是根类加载器or
本身就是根类加载器,则跳到第4步如果父类加载器存在,则进入第3步。
-
请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
-
请求使用根类加载器去载入目标类,如果载入成功则返回对象,否则抛异常。
-
当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
-
从文件中载入Class,成功后跳至第8步。
-
抛出
ClassNotFountException
异常。 -
返回对应的
java.lang.Class
对象。
- 判断缓冲区是否有此Class, 有返回,没有就判断是否有父类加载器
- 父类存在则用父类加载器去加载, 加载成功返回jlc对象, 否则使用当前类加载器寻找Class文件, 如果找到, 则载入, 然后加载成功, 否则抛出CNFE异常
- 如果本身是根类加载器: 加载成功返回jlc对象, 否则抛出CNFE异常
3. 类加载机制
3.1 全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
3.2 双亲委派
所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
双亲委派机制 工作原理 : 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成
双亲委派机制的优势:
可避免类重复加载
: Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次安全
: java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
3.3 缓存机制
缓存机制将会保证所有加载过的Class都会被缓存
当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。
这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。