聊聊类加载过程和双亲委派模型
Posted HelloCoder
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊类加载过程和双亲委派模型相关的知识,希望对你有一定的参考价值。
作为JVM层面的Java面试题,类加载过程和双亲委派模型是几乎必问的内容之一。
这篇文章就来聊聊类加载过程和双亲委派模型。
絮
我面试曾经也不遇到不少这个题目,第一次被问到我是懵逼的,因为我确实不知道这东西,而且我也没用过。
我觉得这类面试题考察的还是程序员对Java虚拟机的深层次底层功力。
至于它在工作过程中有没有用得上,这个并不是很重要的;重要的是面试官觉得你知道了,说明你的Java底子还是很好的。
关于虚拟机的知识,我是跟着周志明的《深入理解Java虚拟机》学习的,不知不觉已经是第三版了,如果你对一个.java
文件在执行过程中发生了什么、对性能调优、对底层知识感兴趣的,我建议大家阅读一下。
在这本书里面,也提到了类加载机制和双亲委派模型,我这里总结一下。
1、类加载过程
首先我们知道一个.java
文件它的编译过程是这样的:
上面图中描述的是宏观的过程,但是微观的过程并没有提及,而微观过程的.class
字节码是如何解释成机器码而被不同的平台执行的呢?
这就是我们本篇文章研究的 类加载过程和双亲委派模型
再进一步拆分,它其实是这样的:
且看看《深入理解Java虚拟机》中对类加载过程的定义:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型。
类从被加载到虚拟机内存中开始,它的整个生命周期包括:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
1、加载
加载分为三步:
第一步:通过一个类的全限定名来获取定义此类的二进制字节流。
第二步:将静态的存储结构转换为方法区中的运行时数据结构。
第三步:生成一个对象放入java堆中,作为对方法区的引用。
(类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态数据结构转化为方法区中运行的数据结构,并且在堆内存中生成一个java.lang.Class
对象作为访问方法区数据结构的入口)
对于第一步的二进制字节流,其实这个虚拟机并没有说的很具体,所以说现在很多开发人员都可以打破它,比如说
1、通过jar、zip、war 获取2、提供网络获取,比如说applet
3、动态代理
4、JSP这种,编译后还是一个class类
2、验证
验证主要的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
所以说Java虚拟机并不是完全信任加载过来的字节流。
整体上验证分为四个动作:
文件格式验证
包括 class文件的表示(魔数),class文件的版本号,class文件的每个部分是否正确(字段表、方法表等),验证常量池(常量类型、常量类型数据结构是否正确,utf-8是否标准),,字节码(指令)验证,符号引用验证(是否能根据符号找到对应的字段、表、方法等)
只有通过了第一步的文件格式验证,字节流才会进入到内存的方法区中进行存储,所以接下来的三个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流。
元数据验证
元数据验证(父类验证,继承验证,final验证),比如说是否继承了不允许被继承的类(即final修饰的类)、是否实现类抽象类的方法、重载回参、类型是否一样
字节码验证
这个阶段是最复杂的,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
如果一项不对,就会验证失败。
符号引用验证
这一步发生在虚拟机将符合引用转化为直接引用的时候,目的是为了确保 解析 动作能正常执行。比如说类是否能找到、访问类的字段、方法是否被private修饰无法访问了。
3、准备
准备阶段为类变量分配内存 和设置类变量初始化。
这个过程中,只对static类变量进行内存分配,这个时候只是分配内存,没有进行复制,所有的类变量都是初始化值。
如果是final的话,会直接对应到常量池中。会在准备阶段直接赋值。
public static final int value = 123;
这里变量的初始化值是0 而不是 123,123在初始化阶段才会赋值。
题外话,这里我演示一下,大家可以看看:
这是我的HelloCoder.java
文件:
package com.yudianxx.basic.字节码;
/**
* @author HaC
* @date 2021/5/15 14:26
* @webSite https://rain.baimuxym.cn
* @Description
*/
public class HelloCoder {
public static int staticValue = 123;
public static final int finalValue = 456;
public static void main(String[] args) {
System.out.println(staticValue);
System.out.println(finalValue);
}
}
使用命令打印一下字节码:
javap -verbose -private -c -s -l HelloCoder
这是字节码:
Classfile /G:/源码/springBootLogback/yudianxx-core/target/classes/com/yudianxx/basic/字节码/HelloCoder.class
Last modified 2021-5-15; size 712 bytes
MD5 checksum ac91b5504efcf6aa372cdc10cd899626
Compiled from "HelloCoder.java"
public class com.yudianxx.basic.字节码.HelloCoder
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Fieldref #5.#29 // com/yudianxx/basic/字节码/HelloCoder.staticValue:I
#4 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#5 = Class #32 // com/yudianxx/basic/字节码/HelloCoder
#6 = Class #33 // java/lang/Object
#7 = Utf8 staticValue
#8 = Utf8 I
#9 = Utf8 finalValue
#10 = Utf8 ConstantValue
#11 = Integer 456
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/yudianxx/basic/字节码/HelloCoder;
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 <clinit>
#24 = Utf8 SourceFile
#25 = Utf8 HelloCoder.java
#26 = NameAndType #12:#13 // "<init>":()V
#27 = Class #34 // java/lang/System
#28 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#29 = NameAndType #7:#8 // staticValue:I
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 com/yudianxx/basic/字节码/HelloCoder
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public static int staticValue;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int finalValue;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 456 //ConstantValue指令
public com.yudianxx.basic.字节码.HelloCoder(); //构造方法
descriptor: ()V
flags: 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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/yudianxx/basic/字节码/HelloCoder;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field staticValue:I
6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: sipush 456
15: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
18: return
LineNumberTable:
line 14: 0
line 15: 9
line 16: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 args [Ljava/lang/String;
static {}; //这里是static语法块
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 123
2: putstatic #3 // 通过指令putstatic标志位初始化值
5: return
LineNumberTable:
line 10: 0
}
SourceFile: "HelloCoder.java"
以上字节码中,static 的值会通过putstatic
指令标志,存放于类构造器<clinit>()
方法中,final 的值则是 ConstantValue
指令
4、解析
解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程。(指向目标的指针或者偏移量)
符号引用 是以一组符号来描述所引用的目标,只是定位目标,和虚拟机的内存布局无关,所以它不一定已经加载到内存中,不同的虚拟机的内存布局也可以不一样,但是符号标注都是一样的。
直接引用 直接指向目标的指针、相对偏移量,和虚拟机的内存布局是直接相关的,如果有了直接引用,那么引用的目标必定已经存在内存中了。
主要涉及到的解析有类,接口,字段,方法等。如果权限不够就会抛出IllegalAccessError
,找不到字段就会抛出NoSuchFiledError
、找不到方法就会抛出NoSuchMethodError
5、初始化
类初始化就是执行类中定义的Java程序代码(或者说是字节码),从字节码层面来说,初始化阶段是执行类构造器<clinit>()
方法的过程。
包括static{ }
代码块的语句执行和static变量赋值(可以参考上面,static 修饰的在这里的初始化阶段进行赋值,而且是父类的<clinit>()
先执行,如果程序没有static,那么编译器可以不生成<clinit>()
方法)
6、使用
使用阶段就是使用这个class。
7、卸载
卸载阶段就是不在使用,将class给卸载。
2、双亲委派模型
类加载器的种类:
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
1、双亲委派的工作流程是什么?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,层层套娃(葫芦娃们)。因此,默认情况下,最终是送到顶层的 启动类加载器(Bootstrap ClassLoader),只有当父类(葫芦娃爷爷 以上是关于聊聊类加载过程和双亲委派模型的主要内容,如果未能解决你的问题,请参考以下文章