点击蓝字关注我们
预备知识
1. 一个 Java 文件从编码完成到最终执行,一般主要包括两个过程
编译
运行
编译,即把我们写好的 java 文件,通过 javac 命令编译成字节码,也就是我们常说的.class 文件。
运行,则是把编译声称的.class 文件交给 Java 虚拟机 (JVM) 执行。
2.我们所说的类加载过程即是指 JVM 虚拟机把.class 文件中类信息加载进内存,并进行解析生成对应的 class 对象的过程。
3.举个通俗点的例子来说,JVM 在执行某段代码时,遇到了 class A, 然而此时内存中并没有 class A 的相关信息,于是 JVM 就会到相应的 class 文件中去寻找 class A 的类信息,并加载进内存中,这就是我们所说的类加载过程。
类加载器
1.加载器种类
启动(Bootstrap)类加载器:引导类加载器是用 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib 下面的核心类库 或 -Xbootclasspath 选项指定的 jar 包等 虚拟机识别的类库 加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作。
扩展(Extension)类加载器:扩展类加载器是ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME>/lib/ext 或者由系统变量 - Djava.ext.dir 指定位置中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System)类加载器:系统类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径 (java -classpath 或 - Djava.class.path 变量所指的目录,即当前类所在路径及其引用的第三方类库的路径。开发者可以直接使用系统类加载器。
2.类加载器之间的关系
扩展类加载器和系统类加载器均是继承自 java.lang.ClassLoader 抽象类。
上面图片给人的直观印象是:系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器。
事实上,由于启动类加载器无法被 Java 程序直接引用,因此 JVM 默认直接使用 null 代表启动类加载器。
所以综合一下:
3.1.系统类加载器(AppClassLoader)调用 ClassLoader (ClassLoader parent) 构造函数将父类加载器设置为标准扩展类加载器 (ExtClassLoader)。(因为如果不强制设置,默认会通过调用 getSystemClassLoader () 方法获取并设置成系统类加载器。)
3.2.扩展类加载器(ExtClassLoader)调用 ClassLoader (ClassLoader parent) 构造函数将父类加载器设置为 null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用 getSystemClassLoader () 方法获取并设置成系统类加载器,。)
双亲委派机制
1.概念
当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类(它的搜索范围中没有找到所需要加载的类),便将类的加载任务退回给下一级类加载器去执行加载。
简而言之:自底向上检查是否被加载,自顶向下尝试加载类。
2.优势
能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
3.实现
实现双亲委托的代码都集中在java.lang.ClassLoader的 loadClass() 方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
4. 自定义加载器的意义
java 代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
类加载过程
类加载的过程主要分为三个部分:
加载
链接
初始化
而链接又可以细分为三个小部分:
验证
准备
解析
1.加载
简单来说,加载指的是把 class 字节码文件从各个来源通过类加载器装载入内存中。
2.1 链接之验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
对于文件格式的验证
对于元数据的验证
对于字节码的验证
对于符号引用的验证
2.2 链接之准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
类变量是static修饰的成员变量,也就是静态变量,实例变量是非static修饰的成员变量。(静态变量、常量是存储在方法区的)。
特别需要注意,初值,不是代码中具体写的初始化的值,而是 Java 虚拟机根据不同变量类型的默认初始值。比如 8 种基本类型的初值,默认为 0;引用类型的初值则为 null;常量的初值即为代码中设置的值。
2.3 链接之解析
将常量池内的符号引用替换为直接引用的过程。
符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用。可以理解为一个内存地址,或者一个偏移量。
举个例子来说,现在调用方法 hello (),这个方法的地址是 1234567,那么 hello 就是符号引用,1234567 就是直接引用。
3. 初始化
调用的是<clinit>()方法
这个阶段主要是对类变量初始化,是执行类构造器的过程。换句话说,对 static 修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
PS:关于初始化详细代码示例会单独下一个专题
1.说一下类加载的执行过程?
2.Java中都有哪些加载器?
3.什么是双亲委派模型?
答案在上面的内容都可以找到,如果还有疑问,可以在公众号回复:基础200107
获取详细答案。
▇ 扫码关注我们 ▇
Java面试那点事
微信号 : javajob666