一、Java类加载机制中的类
在讲Java类加载机制前,我想问问大家知道Java类加载机制中的这个类指的是什么吗?肯定有人会想“这不开玩笑吗,不就是我们写代码时创建的类吗”,严格上来讲,其实不是的,我们写代码所创建的“类”只是java文件,记得刚学Java时在终端用Java命令跑Java程序的时候吗?
javac HelloWorld.java
java HelloWorld
javac和java这两条命令总是形影不离,如果你直接执行java命令,终端会提示
错误: 找不到或无法加载主类 HelloWorld
原因: java.lang.ClassNotFoundException: HelloWorld
奇怪,我的“类”明明就在那里,为什么说找不到?
Java之所以可以跨平台,就是因为它可以一次编译,处处运行,当然处处运行的前提是机器装有jvm的Java环境,在运行Java程序前我们需要把带有java后缀的Java源程序通过Java编译器编译成带有class后缀的Java字节码文件,因为jvm只认识它。这也解释了终端的报错信息和为什么我们在终端执行javac命令后会生成一个与Java源程序同名的带class后缀的文件。
所以,Java类加载机制中的类指的就是这个编译后的class文件,要加载的也是它。
二、类加载机制概念
Java虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。class文件由Java类加载器加载后,在JVM中将形成一份描述类结构的Class对象,通过该Class对象可以获知类的结构信息:如构造函数,属性和方法等,我们使用反射时获取的Class对象就是这个。
三、类加载流程
类加载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:加载、验证、准备、解析、初始化、使用和卸载。其中,验证、准备和解析这三个部分统称为连接(linking)。
3.1、加载(查找和导入Class文件)
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
3.2、验证(检查载入Class文件数据的正确性)
当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:
- JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以
0x cafe bene
开头,主次版本号是否在当前虚拟机处理范围之内等。 - 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。
3.3、准备(给类的静态变量分配存储空间并初始化)
为类的静态变量分配内存并将其初始化为数据类型默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
//在准备阶段初始化value为0,在初始化阶段才会变为123
public static int value = 123;
//在准备阶段初始化value为null,在初始化阶段才会变为"abc"
public static String str = "abc"
但是如果静态变量被final修饰变成静态常量,那么在准备阶段,它会直接被赋值。因为final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。
//直接对value赋值123
public static final int value = 123;
3.4、解析(将符号引用转成直接引用)
当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。
3.5、初始化(对类的静态变量,静态代码块执行初始化操作)
类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行Class对象类构造器 <clinit>()
方法的过程。在class文件编译之后,会有两个方法:<clinit>方法和<inti>方法,<clinit>方法负责静态变量和静态块的初始化,是在类加载过程中执行的,而<init>是负责普通代码块、普通方法和属性以及构造方法的初始化,是在对象实例化执行的。因此,<clinit>方法永远先于<init>方法执行,虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
<clinit>()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
四、何时进行类的初始化
什么情况下需要开始对类进行加载过程的第一个阶段"加载",虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。
- 创建类的实例
- 访问类的静态变量(被 final 修辞的静态变量除外)
- 访问类的静态方法
- Java反射操作,如 (Class.forName(“xx.yyy.zzz”))
- 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
- 虚拟机启动时,定义了main()方法的那个类先初始化
注意:
- 被 final 修辞的静态变量除外原因:常量一种特殊的变量,因为编译器把他们当作值 (value) 而不是域 (field) 来对待。如果你的代码中用到了常变量 (constant variable),编译器并不会生成字节码,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变 final 域的值那么每一块用到那个域的代码都需要重新编译。
- 子类调用父类的静态变量,子类不会被初始化。只有父类会被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
只有以上六种情况会触发类的初始化,其他情况均不会触发。
五、类初始化顺序
用个例子证明一下这张图
class Grandpa {
static {
System.out.println("爷爷的静态代码块");
}
{
System.out.println("爷爷的普通代码块");
}
public Grandpa() {
System.out.println("爷爷的构造方法");
}
}
class Father extends Grandpa {
static {
System.out.println("爸爸的静态代码块");
}
{
System.out.println("爸爸的普通代码块");
}
public Father() {
System.out.println(str);
}
public String str = "爸爸的构造方法";
}
class Son extends Father {
static {
System.out.println("儿子的静态代码块");
}
{
System.out.println("儿子的普通代码块");
}
public Son() {
System.out.println("儿子的构造方法");
}
}
public class InitializationDemo {
public static void main(String[] args) {
new Son();
}
}
运行输出的结果:
爷爷的静态代码块
爸爸的静态代码块
儿子的静态代码块
爷爷的普通代码块
爷爷的构造方法
爸爸的普通代码块
爸爸的构造方法
儿子的普通代码块
儿子的构造方法
初始化的顺序确实如图中所讲的一样
六、类加载器
类的加载就是虚拟机通过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动作的就是类加载器。
类与类加载器
JVM 在判定两个 Class 是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的,这是因为每一个类加载器都拥有一个独立的类名称空间。只有两者同时满足的情况下,JVM 才认为这两个 Class 是相同的。
类加载器分类
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,有三种系统提供的类加载器一种自定义的类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
- 扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。
- 应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器:继承自ClassLoader类实现我们的类加载需要,常见文件类加载器、网络类加载器等等。
双亲委派模型
每个ClassLoader实例都有一个父类加载器的引用(非继承,是包含关系),JVM内置的类加载器(Bootstrap ClassLoader)没有父类加载器,它可作为其他Classloader实例的父类,这样会保证当某个ClassLoader实例加载某个类时,总会自底向上检查类是否加载,然后自顶向下尝试加载类。即类加载是一个从上往下的过程,其总会从ClassLoader实例的顶层父类去尝试加载,也就是总会从JVM内置的类加载器开始,当它加载不了时,在给其子类加载,直至加载完成生成返回类对象或抛出ClassNotFindException。
通过源码也可看出这点
public abstract class ClassLoader {
protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
//检查指定类是否被加载过
Class c = findLoadedClass(name);
if( c == null ){//如果没被加载过,委派给父加载器加载
try{
// 调用JVM类加载器以外的类加载器加载
if( parent != null )
c = parent.loadClass(name,resolve);
else
// 调用JVM类加载器BootstrapClassLoader进行加载
c = findBootstrapClassOrNull(name);
}catch ( ClassNotFoundException e ){
}
if( c == null ){
如果仍然找不到,按顺序调用findClass尝试从自定义类加载器中加载
c = findClass(name);
}
}
if( resolve ){//如果要求立即链接,那么加载完类直接链接
resolveClass();
}
//将加载的类对象直接返回
return c;
}
}
使用这种模式的好处是使得Java类之间随着它类加载器的不同具有一种优先级的层次关系,简单点说就是维护了基础类的安全统一,例如Object类,它是所有Java类的基础类,如果我们自己编写一个java.lang.Object类,程序可以编译通过,但是双亲委派模型会保证加载的永远是系统的Object类而不是我们的,会这样正是因为系统的Object类的优先级比我们自己编写的Object类优先级高,因为系统的Object类是JVM内置的类加载器负责加载的。
七、自定义类加载器
自定义文件系统类加载器从指定路径加载类,继承自ClassLoader类,为了保证双亲委派模型,只重写findClass方法。
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
尝试加载桌面的HelloWorld.class类,并执行sayHello()方法
public static void main(String[] args) {
FileSystemClassLoader loader = new FileSystemClassLoader("/Users/kjt/Desktop/");
try {
Class<?> helloWorld = loader.findClass("HelloWorld");
Object o = helloWorld.getConstructor().newInstance();
Method method = helloWorld.getMethod("sayHello");
method.invoke(o);
} catch (Exception e) {
e.printStackTrace();
}
}
控制台输出
Hello World!