1. java类加载过程
(1)java类的生命周期
- 一个
Java
文件从编码完成到最终执行,一般主要包括两个过程: 编译、运行。
编译
:通过javac
命令将写好的java
文件变成字节码
,也就是我们常说的.class文
件即实现从机器码到字节码的变化
。
运行
:则是把编译后声称的.class文件交给Java虚拟机(JVM)执行
。 - 类加载过程 即是指
JVM
虚拟机把.class文件中类信息加载进内存
,并进行解析生成对应的class对象的过程
。 JVM
在执行某段代码时,遇到了class A
, 然而此时内存中并没有class A
的相关信息。于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。由此可见,JVM不是一开始就把所有的类都加载进内存中
,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次
。- 类从被加载到
JVM
中开始,到卸载为止,整个生命周期包括:加载
、验证
、准备
、解析
、初始化
、使用和卸载
七个阶段。
(2)java类的加载过程
- 类加载的过程主要分为三个部分:
加载、链接、初始化
,而链接又可以细分为三个小部分:验证、准备、解析
。
Loading(加载)
- Loading 指的是
把.class字节码文件从各个来源通过类加载器装载入内存中
,Loading需要完成的事情有:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在
java堆中
生成一个代表这个类的Class对象
,作为访问方法区中这些数据的入口。
- 字节码来源: 一般的加载来源包括从本地路径下
编译生成的.class文件
,从jar包中的.class文件
,从远程网络以及动态代理实时编译。 - 类加载器: 一般包括启动类加载器(
Bootstrap ClassLoader
),扩展类加载器(Ext ClassLoader
),应用类加载器(App ClassLoader
),以及用户的自定义类加载器。 自定义类加载器
需要继承抽象类ClassLoader
,实现findClass
方法,该方法会在loadClass调用的时候被调用
,findClass
默认会抛出异常。
findClass
方法:表示根据类名查找类对象
loadClass
方法:表示根据类名进行双亲委托模型
,进行类加载并返回类对象
defineClass
方法:表示跟根据类的字节码转换为类对象
- 为什么会有自定义类加载器?
一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的
自定义类加载器
进行解密,最后再加载。
另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
验证
- 验证: 主要是为了
保证加载进来的字节流符合虚拟机规范,不会造成安全错误
。 对于文件格式的验证
,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?对于元数据的验证
,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?对于字节码的验证
,保证程序语义的合理性,比如要保证类型转换的合理性。对于符号引用的验证
,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
准备
- 准备: 主要是
为类变量(被static修饰的变量,不是实例变量)分配内存,并且赋予初值
,这个内存分配是发生在方法区中。特别需要注意,初值
,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值
。 - 比如
8种基本类型的初值
,默认为0;引用类型的初值
则为null;常量的初值
即为代码中设置的值,final static tmp = 456
, 那么该阶段tmp的初值就是456 - 注意: 这里并没有对实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。这里设置的初始值,通常
是指数据类型的零值
。
private static int a = 3;
- 这个类变量a在准备阶段后的值是0,将3赋值给变量a是发生在初始化阶段。
解析
- 解析:
将常量池内的符号引用替换为直接引用的过程
。 - 符号引用: 即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法、一个变量、一个类的相关信息。符号引用就是class文件中的:
CONSTANT_Class_info
、CONSTANT_Field_info
、CONSTANT_Method_info
等类型的常量
。 - 直接引用: 可以理解为
一个内存地址
,或者一个偏移量
、或者一个能间接定位到目标的句柄
。 - 比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
- 举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
- 在解析阶段,虚拟机会把所有的类名、方法名、字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
。
初始化
- 初始化: 该阶段主要是
对类变量(被static修饰的变量,不是实例变量)初始化
,是执行类构造器
的过程。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。 - 初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,
除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导
。到了初始阶段,才开始真正执行类中定义的Java程序代码
。 - 初始化阶段是执行类构造器
client()
方法的过程。client()
方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证client()
方法执行之前,父类的client()
方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成client()方法。 - 注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化。
- 通过
Class.forName
加载指定类时,如果指定参数initialize为false时
,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 - 通过
ClassLoader
默认的loadClass
方法,也不会触发初始化动作。
(3)双亲委派模型
① 双亲委派模型介绍
- 双亲委派模式 要求
除了顶层
的启动类加载器
外,其余的类加载器都应当有自己的父类加载器
。注意: 双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
- JVM提供了3种类加载器:
- 启动类加载器(Bootstrap ClassLoader): 负责加载
JAVA_HOME\lib
目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。由C++实现,没有父类
,因此每次类的加载都会执行启动类加载器。 - 扩展类加载器(Extension ClassLoader): 负责加载
JAVA_HOME\lib\ext
目录中的,或通过java.ext.dirs
系统变量指定路径中的类库。由Java语言实现,父类加载器为null,便可使用启动类加载器作为父加载器
。 - 应用程序类加载器(Application ClassLoader): 负责
加载用户路径(classpath)上的类库
。由Java语言实现,父类加载器为ExtClassLoader
。 - 自定义类加载器: JVM
通过双亲委派模型进行类的加载
,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器
。自定义类加载器,由Java语言实现,父类加载器肯定为AppClassLoader
。
- 双亲委派模型是在Java 1.2后引入的,其工作原理的是: 如果
一个类加载器收到了类加载请求
,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
。如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
。 - 如果父类加载器可以完成类加载任务,就成功返回;倘若
父类加载器无法完成此加载任务
,子加载器才会尝试自己去加载
,这就是双亲委派模式。即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这就是传说中的实力坑爹! - 双亲委派模型的好处:
java类随着它的类加载器一起具备了一种带有优先级的层次关系
。例如类java.lang.Object
,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载。因此,Object
类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object
的类,并放在程序的Classpath
中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。 - 双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的
代码都集中在java.lang.ClassLoader的loadClass()方法中
,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常
后,再调用自己的findClass方法进行加载。 ClassLoader类
,它是一个抽象类
,其后所有的类加载器都继承自ClassLoader
(不包括启动类加载器)
② ClassLoader中几个重要的方法
loadClass()方法
loadClass()
:该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法。loadClass()方法
是ClassLoader类自己实现的
,该方法中的逻辑就是双亲委派模式的实现
,其源码如下,loadClass(String name, boolean resolve)是一个重载方法
,resolve参数代表是否生成class对象的同时进行解析相关操作。
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//检查该类是否已经被加载过
Class c = findLoadedClass(name);
/*
* 没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型
* 首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。
*/
if (c == null) {
try {
if (parent != null) {//递归查找父加载器
c = parent.loadClass(name, false);
} else {//父加载器为空则默认使用启动类加载器作为父加载器
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果非空的父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//根据resolve的值,判断这个class是否需要解析。
if (resolve) {
resolveClass(c);
}
return c;
}
findClass(String)方法
在JDK1.2之前
,在自定义类加载时,总会去继承ClassLoader
类并重写loadClass方法
,从而实现自定义的类加载类。但是在JDK1.2之后
已不再建议用户去覆盖loadClass()方法
,而是建议把自定义的类加载逻辑写在findClass()方法中
。- 从前面的分析可知,
findClass()方法是在loadClass()方法中被调用的
,当loadClass()方法中父加载器加载失败后
,则会调用自己的findClass()方法来完成类加载
,这样就可以保证自定义的类加载器也符合双亲委托模式。 - 注意:
ClassLoader
类中并没有实现findClass()方法
的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常;findClass方法
通常是和defineClass方法
一起使用的(稍后会分析),ClassLoader
类中findClass()方法
源码如下:
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass(byte[] b, int off, int len) 方法
defineClass()
方法的作用:将byte字节流解析成JVM能够识别的Class对象
(ClassLoader
中已实现该方法逻辑)。- 通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象。如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。
defineClass()
方法通常与findClass()方法一起使用
:一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法
并编写加载规则,取得要加载类的字节码
后转换成流,然后调用defineClass()方法生成类的Class对象
,简单例子如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
参考链接:
深入理解Java类加载器(ClassLoader)
JVM 类加载机制详解
深入理解Java:类加载机制及反射
Java类加载过程
面试官:请你谈谈Java的类加载过程
JAVA类加载机制详解
转载:https://blog.csdn.net/u014454538/article/details/88907784
查看评论