开篇
简单了解下一个
JAVA
类的执行过程: 加载 -> 连接 -> 初始化
- 加载:查找和导入Class文件
- 链接:链接需要检查、准备和解析,先检查载入class文件数据的正确性然后给类的静态变量分配存储空间最后将符号引用转成直接引用。
- 初始化:对静态变量、静态代码块执行初始化工作
Java文件加载的流程: 源文件.java
文件经过编译器编译之后转成字节码文件.class
文件,JVM
中的类加载器负责读取这个字节码文件并转成java.lang.Class
类,然后通过newInstance()
方法就可以创建一个Java
类的对象。
1 类的加载及ClassLoader
在 Java
中,ClassLoader
类的职责就是根据一个指定的类的名称,找到或生成其对应的字节码,然后从这些字节码中定义出一个 Class
类的实例,此外还负责加载应用所需的资源比如配置文件。ClassLoader
为了完成加载类的这个功能,提供了一些非常重要的方法,如下:
方法 | 作用 |
---|---|
getParent() | 获取该类加载器的父类加载器 |
loadClass(String name) | 加载名称为 name的类,返回Class类的实例。 |
findClass(String name) | 查找名称为 name的类,返回Class类的实例。 |
findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回Class类的实例 |
final defineClass(String name, byte[] b, int off, int len) | 将字节数组b里的内容转为Java的Class类 |
resolveClass(Class<?> c) | 链接指定的Java类 |
1.1 JDK提供的三个类加载器
如上面所说的那样,类加载器就是用来加载Java类到JVM中的,当JVM启动的时候,默认先启动三个类加载器:
BootStrapLoader(启动类加载器)
: 它是JVM内部实现的加载器,负责加载${JAVA_HOME}/lib/rt.jar
这个核心类库,JVM内部C++
语言实现,开发者不允许直接操作(它不是继承自ClassLoader的)。ExtClassLoader(扩展类加载器)
:负责把{JAVA_HOME}/lib/ext/*.jar
这个扩展包里的类加载到JVM
,JAVA 层面实现,开发者可以直接操作。AppClassLoader(系统类加载器)
:加载的是classpath
所指定的类,它也是Java程序默认的类加载器,Java层面实现,开发者可以直接操作。它们三个的结构图如下:
JVM类加载器.png
从上面三种Java提供的类加载器就可以看到,三种类加载器各有各的职责所在,但是这只是其中一个目的,另外一个目的就是实现 双亲委派模型 。基本上所有的类加载器都是
ClassLoader
类的一个实现,除了bootStrap
这个系统内部的加载器之外,也就是说除了它所有的类加载器都有一个父类加载器,可以通过上面的方法得到。当然,我们也可以自己在Java程序里通过继承ClassLoader
来实现自己的类加载器。
1.2 Demo:获取类加载器的父类加载器,理解双亲委派
public class ClassLoaderDemo {
public static void main(String[] args) {
/**
* 每一个Java类都维护一个指向定义它的类加载器的引用
* 可以通过getClassLoader() 就可以获取
* ExtClassLoader 它的父类加载器是Null
* bootStrap 是内部的C++实现,它在逻辑上是它的父类加载器
*/
ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
while (classLoader != null) {
System.out.println(classLoader.toString());
/**
* 获取上一级的类加载器
*/
classLoader = classLoader.getParent();
}
}
}
打印结果如下,第一个就是系统类加载器的实例,第二个输出的是扩展类的类加载器。这里并没有输出最顶级的引导类加载器,是因为有些JDK的类对于父类加载器是引导类加载器的情况下getParent()
返回的是 null
,因为引导类加载器是C++写的,并不存在这么一个java类的实体。
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2aaf7cc2
1.3 JVM 加载class文件的原理机制 及双亲委派模型
类的加载工作由 ClassLoader
和其子类负责,JVM在运行时会产生三个 ClassLoader
,就是上面说的三个,默认使用 AppClassLoader 状态应用程序的类。Java装载类使用 全盘委托机制
,全盘负责 是指一个 ClassLoader
装载一个类时,除非显式的使用另外一个 ClassLoader,否则该类所依赖的类也都是由这个 ClassLoader 装入。委托机制
是先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的子类装载器路径中查找并装载目标类。这个是为了一些不经意的情况影响运行的场景:比如在命名类的时候,可能你无意或者故意写了一个 HashMap
命名的类,这个直接加载到JVM中被其它地方使用了可能会出大问题,但是有了全盘负责机制,HashMap
永远都是由 BootStrap
根类加载器装载的。这就是双亲委派模型,先从根加载器开始找,逐级往下。双亲委派模型并不是多厉害的东西,只是把需要加载的类先让父类去加载,父类找不到的话再往下找。一方面这样可以避免重复加载,当父类已经加载了一个类的时候,就没必要在子类加载器ClassLoader中再去加载一次了,另一方面就是刚才提到的那个安全因素了,因为 HashMap 在JVM启动的时候已经加载了,所以用户就无法加载一个自定义的 HashMap
了。
1.4 自定义类加载器
既然JVM已经提供了默认的类加载器,但是有一些弊端,比如只能加载指定目录下的jar包或者class文件
,如果我们想加载其它位置的 jar或者class文件
时,比如网络上的某 class通过动态加载到内存来使用,这样的场景默认的类加载器就不能给我们提供帮助了,所以就要自己定义 ClassLoader
。自定义类的加载器继承 ClassLoader
然后重写父类的findClass方法,之所以只重写这个方法是因为JDK已经在loadClass中帮我们使用了ClassLoader搜索类的算法,当在loadClass方法中找不到类时,loadClass方法就会调用findClass方法来搜索类,所以只要重写它就好了。
2. 类的连接
类的连接有三个步骤,分别是验证、准备、解析
-
验证阶段主要检查一些执行的前置条件,简单的几个例子比如:
- 把已经读入到内存的二进制数据合并到JVM的运行环境
- Java 的一些语义规范检查
- 对加载到 JVM 中的字节码进行完整性验证
-
而准备阶段则是JVM 为类的静态变量分配内存并赋初始值,比如 int类型的静态变量 分配4字节空间,并赋初始值0
-
解析阶段主要是把类中定义的一些符号转化成引用的过程,比如,类中的一个方法引用了其它类的一个方法,那么就会找到这个其它类的方法的内存地址然后把符号引用替换成直接引用,也就是赋予了内存地址
3. 类的初始化
在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源。比如定义了静态属性a和b,分别赋值2 和 4,连接阶段的时候a和b都是等于0的,在初始化阶段才等于2和4.
在类的加载过程中,一次并不主动加载全部的类,有很多类是在使用的时候加载的,这样可以很高效的节约内存空间。主动初始化大致有以下几种时机(注:都是在没有被初始化的时候才会初始化的,如果已经初始化过了,即使发生了下面的事件也不需要初始化):
- 创建对象的实例,就是在我们
new
对象的时、通过class 文件反射
创建对象时 - 使用类的静态方法、静态属性时
- 初始化一个子类的时候,使用子类需要先初始化父类
- JVM 启动时被标记为启动类的类,也就是
main
方法所在的类
PS: 在类加载的过程中,在同一个类加载器下面只能初始化一次,这个时候再JVM中就会有一个表示该实例的 class对象的实例,所以初始化一次就行了。同时在编译期能够确定的静态变量和静态常量不会对类进行初始化,比如一个静态修饰的随机数、一个静态修饰的赋值变量。他们初始化的时机是不同的。
3.1 类的初始化步骤
当一个类含有父类的时候:(静态代码块和静态属性,层级一样的,按照编写顺序执行的)
- 父类的静态属性
- 父类的静态代码块
- 子类的静态属性
- 子类的静态代码块
- 父类的非静态属性
- 父类的非静态代码块
- 父类的构造方法
- 子类的非静态属性
- 子类的父静态代码块
- 子类的构造方法
再看没父类的类的加载
- 类的静态属性
- 类的静态代码块
- 类的非静态属性
- 类的非静态代码块
- 构造方法
4. Java 热部署的实现
热部署就是在不重启JVM的前提下能够自动侦测到 class
文件的变化,更新运行时的 class 文件。Java 类是通过 JVM
加载的,某个类的class文件在被 ClassLoader
加载后,会生成对应的 Class 对象。然后就可以创建该类的实例,这个过程上面说过了。默认的JVM只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的class文件JVM是不会管你的这个操作的。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变classLoader 的加载行为,让它自己去检测class文件是否发生变化,但是这样破坏性比较大的,不建议。另外一个比较友好的方法是创建自己的classLoader来加载需要监听的class,这样就能控制类的加载从而实现热部署。
热部署的步骤:
- 销毁自定义的
ClassLoader
,这是为了卸载之前的class
。让它被GC回收 - 更新class
- 使用新的
ClassLoader
去加载新的class
关于类加载器的一些思考和问题(这部分持续更新)
问题
- 能否自己定义一个类
java.lang.String
,然后自己拿来在应用程序中使用?
答:通常不可以的,依照类的双亲委派模型,总是会去父类加载器中去找Java.lang.String
看父类加载器能否加载的,这样能够保证父类加载器优先加载,如果父类加载完了子类就不需要再次加载了,而rt.jar
这个核心包是BootStrap
这个顶级的加载器加载的,所以自己写的根本就没用被加载的机会。
如果非要实现使用自己定义的java.lang.String
, 我们可以定义一个类加载器达到这个目的,当然这个流程肯定要避免双亲委派机制,所以这个类加载器也是特殊的,由于系统自带的那三个类加载器都是特定目录下的类,所以我们需要把自己的类加载器放在一个特殊的目录里不让那三个类加载器去加载,最终由我们自己定义的类加载器加载,流程是这样的一个流程。
思考
- JVM 的第一个类加载器是
BootStrap
,这个加载器很特殊,因为它是内部C++
实现的,不是 Java类,所以我们在ExtClassLoader
中获得父类加载器是个Null
值。 - 委托机制的意义:防止内存中出现多份同样的字节码,比如类A和类B都要加载
java.lang.String
,如果不用委托机制就用自己的加载机制,那么每个类都会加载一份java.lang.String
的字节码,这样内存里就有2份同样的字节码了。如果使用委托机制,子类加载器总是先看父类加载器能否加载,如果找到了那么就让父类去加载。 - 如果类A中引用了类B,那么JVM就会使用类A的类加载器去加载B。
网友评论