JAVA类加载原理与机制剖析
——————致更好的自己!
一、类的加载、链接、初始化(俗称类加载)
*在讲解类的加载、链接、初始化之前先讲讲JAVA程序的基本运行过程,一个JAVA程序的运行过程是:当运行java程序的时候,系统会启动一个JVM进程,而java程序不管启动多少个线程,拥有多少个变量,都是处于JVM这个进程中,他们都共享JVM的进程内存区域。*只有如下几种情况,JVM进程将会终止:
- 程序正常运行结束,进程自动退出
- 手动利用
System.exit()
或者Runtime.getRuntime().exit()
手动退出进程 - JVM依赖的平台的强制退出或者关闭
- 程序执行过程因为遇到错误或者未捕获的异常而退出
JVM进程结束之后,该进程的内存空间就会被回收。好了,接下来进入正题,类的加载。
1、加载
当我们的JAVA程序主动调用、使用某个类或者某个类中的方法、变量时,如果这个类还没有被加载到内存,那么系统就会通过 加载、链接、初始化对该类进行加载,这三个步骤有时候也会被简称为类加载或者 类初始化。
JAVA中习惯把对象统称为实例,而类则是实例的抽象;其实,java的类,也是实例,它们都是java.lang.Class的实例。所以, 类加载其实就是:将.class文件加载进内存,并且创建一个对应的java.lang.Class实例代表它。
类加载的工作:
-
类加载是由类加载器完成,而类加载器一般由JVM提供
-
我们可以自定义类加载器,通过继承
ClassLoader
,通过不同的类加载器,我们可以加载程序外的指定路径.class;也可以加载jar包中的.class文件。类加载的时机:
-
JVM规范允许预先加载,并非所有的类都在首次调用时进行加载
2、链接
链接的过程比较简单,就是将类的二进制数据合并到 JRE,进行验证、准备、解析:
- 验证:检验上一步加载的类是否有正确的内部结构
- 准备:为类变量分配内存,设置默认值(这个不要误解成static int a=6,而是static int a ;会为a预留一个空间并且赋值为0)
- 解析:将类的二进制数据中的符号引用替换成直接引用
完成链接之后,就会进行初始化。
3、初始化
初始化阶段是由JVM负责的,主要做的事情就是对类变量进行初始化以及static代码块的执行,就像 :
static int a = 6;
//会分为链接阶段的分配空间,默认a=0;而初始化阶段进行 a = 6的赋值。
JVM初始化一个类包含的步骤大致如下:
- 如果该类没有被加载和链接,那么程序先进行加载和链接
- 如果该类的父类还没有被初始化,就会先初始化其父类(如果父类的父类未初始化,爷爷类调用这三个步骤)
- 如果该类中有初始化语句,就会依次执行这些语句
类的初始化时机:
- 通过new创建这个类的对象、通过反射创建实例、通过反序列化创建实例
- 访问这个类或者接口的类变量,或者为类变量赋值
- 通过反射强制创建java.lang.Class对象,例如
Class.forName("MyClass");
- 初始化某个类的子类,或者通过java.exe运行某个主类等等。
特别注意一点,就是 final
定义的类变量,如果在编译时候可以确定下来,就类似与C/C++中的宏定义,会在编译时将出现这个类变量的地方全部进行常量替换,例如:
public class TestDefine {
public static final String IS_DEFINE="已经确定";
public static void main(String[] args) {
System.out.println(IS_DEFINE);
}
}
//以上这个类,编译后的.class文件是下面这样:
public class TestDefine {
public static final String IS_DEFINE = "已经确定";
public TestDefine() {
}
public static void main(String[] args) {
System.out.println("已经确定");
}
}
当然,如果在编译时候无法确定,那么就在运行时调用的时候也会初始化。在使用类加载器的时候,初始化的时机:
package com.flyingstars.www.expand.reflect;
class Test {
static {
System.out.println("Test类的初始化");
}
}
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
//这里要加包名,不然会出现classNotFound错误
classLoader.loadClass("com.flyingstars.www.expand.reflect.Test");
System.out.println("*********************************");
Class.forName("com.flyingstars.www.expand.reflect.Test");
}
}
//输出的结果如下:
*********************************
Test类的初始化
因此,调用loadClass只是进行第一步的加载,只有在调用Class.forName()才会强制初始化该类。
总结
- 类的加载、链接、初始化是依次执行的流程
- 初始化时候要注意的特殊点就是final指定类变量的可预知结果的编译、使用类加载器如果只是调用loadClass方法,就只是进行加载,调用Class.forName才进行强制的初始化。
- 特别注意的地方,就是向上加载机制。
二、类加载器
在了解了类的加载、链接、初始化之后,我们有必要深入学习类加载器,从而对JAVA的运作机制的理解、或者为开发出框架之类的东西提供帮助。
1、类加载机制
首先,类加载负责加载所有的类,系统(JVM)会为加载到内存中的类生成一一对应的Class实例,一个类加载进入 JVM之后,同一个类就不会再加载进去,这里涉及一个问题,JAVA 是如何定义同一个类的?
- 同一个类:由一个唯一的marker标志,该marker=类名+包名+加载器 ,因此,两个不同的类加载器加载相同的.class文件得到的结果是不同。
在JVM中,类的加载机制大致有三种:
- 全盘负责:所谓全盘负责,就是当一个类加载器在加载某个类时,这个类所依赖的、所引用的其它类都会由该类加载器全权负责加载,除非显式使用另一个类加载器来加载那些依赖、引用到的类。
- 父类委托,也称双亲委派机制:除了顶层的启动类加载器外,其他的类加载器都有自己的父类加载器.父类委托机制工作过程是:如果一个类加载器收到了类加载的请求的话,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是这样,因此所有的请求最终都应该传送到顶层的启动类加载器中,只有当父加载器无法完成这个请求时,会把这个请求发送给子类加载器,子类加载器才会自己去加载。
- 缓存机制:缓存机制就是保证所有加载过的Class都被缓存,当需要使用某个Class时,会先从缓存中寻找已经加载了的Class,如果找不到,才会调用类加载器进行加载转换成.Class实例,存入缓存。这也能很好的解释: 为什么删除.class文件后,已经运行的程序仍然在运行;或者替换掉.class文件之后必须重启才能生效。
以下列出类加载器的层次与层次图:
- bootstrap:jre/lib/rt.jar
- ExtClassLoader :jre/lib/ext/*.jar
- AppClassLoader: CLASSPATH指定的所有jar或目录
以上的AppClassLoader
在很多地方解析为:SystemClassLoader
,其实不然,如下代码:
//SystemClassLoader只是封装了AppClassLoader
//通过如下代码就可以看到差别
System.err.println(ClassLoader.getSystemClassLoader().getClass().getName());
System.err.println(ClassLoader.getSystemClassLoader().getParent().getClass().getName());
层次图:
类加载机制的总结
-
类加载机制大致分为三种:全盘、父类委托、缓存
-
父类委托的好处:职责细分,提高安全性,假设我自己定义一个类加载器,然后随便伪造一个类,这个类不符合jvm规范,里面有不安全的代码,如果不适用父类委托机制,那么这个类就会被直接加载到内存里面了。
-
Tomcat的类加载机制,其实本质是一样的,只不过Tomcat修改了类加载目录,从自定义目录中去加载罢了。
-
在自定义目录(非classpath)下找不到这个你这个类名指定的class文件,就会抛ClassNotFoundException异常,所以像java.*包里的类,自定义加载器是加载不到的(就算有这个文件,也加载不到),所以这时就需要父加载器去加载了,ClassNotFoundException也由父加载器去抛出。
2、自定义类加载器
*在讲完类加载机制之后,可能有不少的人想要实现自己的类加载器甚至重写类加载的一些方法,但是定义自己的类加载器一般不会在实际应用中使用,况且抽象类加载器留给我们重写的方法也不多。*如果实在对重写类加载器有兴趣,可以上网搜一搜
三、总结
类加载的内容包括:类加载的过程、类加载机制、类加载器,以下列出要点:
- 类加载过程可分为:加载、链接、初始化
- 类初始化会出现向上初始化的特征,即初始化某个类时,会先看看父类、爷爷类是否初始化,再由上向下初始化,是一个递归过程
- 类加载机制有三种:全盘加载、父类委托、缓存机制。