前言:虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(java.lang.Class对象),这就是java虚拟机的类加载机制。(摘抄自《深入java虚拟机》,周志明著。)

一、类加载的过程

        类的生命周期从加载进内存开始直到卸载出内存为止,期间经历了加载、验证、准备、解析、初始化、使用和卸载七个过程,其中验证、准备、解析三个过程统称为连接。除了使用和卸载之外,java虚拟机在类加载的过程中,会按部就班的“开始”这些过程,注意这里用的是按部就班的“开始”而不是按部就班的“运行”或“完成”,简而言之,这五个过程会顺序开始,但无需等到上一个过程结束下一个过程才能开始运行,实际上他们通常都是相互交叉混合着运行,但这并不妨碍java虚拟机的顺序“开始”。


  • 加载

        加载就是虚拟机将java字节码文件加载进内存的方法区转化成运行时数据结构,并在内存中创建相应的java.lang.Class对象作为方法区中这个类的数据访问入口。也即是文件静态的存储结构变成运行时数据结构。

        注意:数组类本身不通过类加载器创建,而是由java虚拟机直接创建,但是数组的元素类型还是需要类加载器来创建,特别注意的是,如果数据类型是基本数据类型,则将会由根加载器(bootstrap loadClass)来加载。

  • 验证

        验证的目的是为了确保class文件符合虚拟机规范,且不会危害虚拟机安全。主要包含以下四个方面:

        a. 文件格式验证:验证class文件是否符合虚拟机格式,例如前四个字节是否是以魔数0xCAFEBABE开头,第五六个字节是否是次版本,第七八个字节是否是主版本等信息;

        b. 元数据验证:从语义级别分析、检查文件内容是否符合java语言规范;

        c. 字节码验证:从数据流和控制流级别分析、检查该文件内容是否符合java虚拟机规范;

        d. 符号引用验证:这一阶段的验证真正发生的时间是在解析过程中,目的是为了确保解析动作能够正常执行,例如验证根据某一个类的全限定名是否能找到该类。

  • 准备

        准备阶段是正式为“类变量”(static field)分配内存,并依次赋予初始默认值,“依次”的含义是根据程序声明变量的顺序来赋予默认值。

  • 解析

        将经过验证阶段验证的符号引用替换成直接引用,即替换成引用的地址值。

  • 初始化

        初始化是类加载过程的最后一步,到了该阶段,才开始真正的执行类定义中的java程序代码。在准备阶段,将会给静态变量赋予初始值,而在初始化阶段,将会根据程序员的代码内容来初始化类变量以及其他资源的值,即是赋予给定值。

        首先明确一点:此处涉及的是类加载的初始化,而不是实例对象的初始化(注意:类初始化和对象初始化不一样),对应着的是clinit()类构造器和init()实例构造器。        

        该阶段是执行类构造器clinit()方法的过程(注意:不是实例构造器init()),类构造器和实例构造器不同,类构造器是由编译器自动收集类中所有类成员的赋值动作和静态代码块中的语句合并产生,收集顺序和类文件中定义顺序一致。所以,类变量随着类初始化的完成而完成,先于对象存在。

        至此,类加载的过程全部完毕,类变量和方法均可以被使用;接下来就是对类进行实例化,生成并使用、销毁对象。

二、类加载器——原理

  •  什么是类加载器

        类加载器是一个实现特定动作的代码模块,该特定动作是指”通过一个类的全限定名称来获取描述此类的二进制字节流,在方法区中为之建立起运行时数据结构并建立Class对象作为其数据访问的入口“。需要注意的是同一个类一旦被载入虚拟机中,就不会被再次载入。这句话涉及两个关键点,一个是类载入的虚拟机应是指同一批次,另一个是虚拟机怎么判断载入的是否是同一个类。
        a. 举一个例子来说明同一批次的虚拟机问题。如以下代码,结果是B输出2,C输出1,而不是B输出2,C也输出2,虽然B类和C都引用了A类实例,但是B、C不是同一批次的虚拟机。
package vm;

/**
 * 
 * question: 先运行B类,在运行C类,请问B和C中输出的结果是?
 *
 */
public class A {
	public static int a = 1;
}

package vm;

public class B {
	public static void main(String[] args) {
		A.a++;
		System.out.println(A.a);
	}
}


package vm;

public class C {
	public static void main(String[] args) {
		System.out.println(A.a);
	}
}
        b. 虚拟机如何避免对同一个类进行重复加载
        程序中调用两个同名类的时候,是通过全限定类名(包+类)来判断是否是同一个类;虚拟机通过类加载的实例对象把字节码文件加载进内存中并创建相应的java.lang.Class对象,因此虚拟机通过全限定类名和将其加载进内存的类加载器实例来判断。例如,相同的字节码文件,如果是不同的类加载器实例加载进虚拟机中,也会被判断成不同的对象,而分别被虚拟机缓存。当然,缓存之后,相同的类加载器就不需要重复加载了。

  • 类加载器简介

        虚拟机启动的时候,会形成三个类加载器组成的初始类加载器层次结构。
        a. Bootstrap ClassLoader:根类加载器,直属于java虚拟机(除了BootStrap ClassLoader之外,所有的类加载器都是ClassLoader类或子类),负责加载java核心类库(例如rt.jar等),即加载安装jdk目录下jre/lib中的jar包;如以下代码所示,根加载器通常加载的文件如下。
package class_loader;

import java.net.URL;

public class BootStrapDemo {
	public static void main(String[] args) {
		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
		for (URL i : urls)
			System.out.println(i);
	}
	/*
	 * 打印结果: file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/resources.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/rt.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/sunrsasign.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/jsse.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/jce.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/charsets.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/jfr.jar
	 * file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/classes
	 */
}
        b. Extension ClassLoader:扩展类加载器,和System CLassLoader一样,都是UrlClassLoaderl类中子类,但Extension ClassLoader是System ClassLoader的父类,负责加载安装jdk目录下jre/lib/ext中的jar包,用于扩展;
package class_loader;

public class ExtensionDemo {
	public static void main(String[] args) {
		ClassLoader extensionClassLoader = ClassLoader.getSystemClassLoader().getParent();
		System.out.println(extensionClassLoader);
		System.out.println("扩展类加载器路径: " + System.getProperty("java.ext.dirs"));
	}
	/*
	 * 输出结果: sun.misc.Launcher$ExtClassLoader@7852e922 
	 * <span style="font-family: Arial, Helvetica, sans-serif;">扩展类加载器路径:</span><span style="font-family: Arial, Helvetica, sans-serif;">C:\Program Files\Java\jdk1.8.0_11\jre\lib\ext;C:\Windows\Sun\Java\lib\ext</span>
	 */
}
        c. Application ClassLoader:应用程序加载器,加载classpath路径中的类的字节码文件,通俗一点,就是加载我们写的程序的字节码。
package class_loader;

public class AppClassLoader {
	public static void main(String[] args) {
		ClassLoader cl = ClassLoader.getSystemClassLoader();
		System.out.println(cl);
	}
	/*
	 * 输出结果:
	 * sun.misc.Launcher$AppClassLoader@2a139a55
	 */
}

  • 类加载机制

        a. 缓存机制:同批次虚拟机中,被相同类加载器加载过的类将不会再次被加载进内存。
        b. 全盘负责:类加载器加载某一个类时,该类中所依赖和引用的其他类,也将由此类加载器加载。
package overall;

/**
 * 全盘负责:类加载器加载某一个类时,该类中所依赖和引用的其他类,也将由此类加载器加载。
 * @author reliveIT
 *
 */
public class A {
	public A(){
		System.out.println("A: "+A.class.getClassLoader());
	}
}

package overall;

public class B {
	private A a;

	public B(A a) {
		System.out.println("B: "+B.class.getClassLoader());
		this.a = a;
	}

	public static void main(String[] args) {
		B b = new B(new A());
	}

	/*
	 * 输出结果: 
	 * A: sun.misc.Launcher$AppClassLoader@73d16e93
	 * B: sun.misc.Launcher$AppClassLoader@73d16e93
	 */
}
        c. 双亲委托:(重点、重要,摘自《深入理解java虚拟机》,周志明著)一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都应该传送到顶层的根类加载器中,只有当父类加载器自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。整个向上向下的过程类似双向链表一样。
        双亲:根加载器是扩展加载器的父类,扩展加载器是应用程序加载器的父类,应用程序加载器是自定义加载器的父类(此处涉及的父类并非java的三大特性之一,而是利用组合实现的)。
        如以下测试代码,接下来便验证一下双亲委托模型在加载过程中是否真的是向上加载优先。
package test;

/**
 * @author reliveIT
 *
 */
public class Demo {
	public static void main(String[] args) {
		System.out.println(Demo.class.getClassLoader().getClass().getName());
	}
}
        场景一:文件存放于classpath路径下,打印的类加载器为AppClassLoader。
        场景二:将该文件打成jar包存放到jdk/jre/lib/ext目录下,打印的类加载器是ExtClassLoader.
        场景三:将该文件打成jar包存放到jdk/jre/lib目录下,打印的类加载器依旧是ExtClassLoader。
        似乎场景三并没有如想象中那样向上传递到根加载器来加载该类,这是由于根类加载器直属于虚拟机,加载的是JDK中的核心类,因此在实现的时候,根加载器加载该目录下的文件是按照文件名和一定的校验(你可以想象成MD5一类的校验)来加载的,而不是存在于这个目录下的文件都加载(这里可以做测试,自己做jar包覆盖jdk/jre/lib下的jar文件,你会发现编译器启动的时候就出错了)!
        再做一个测试来验证双亲委托中的跟类加载器是否满足这一加载模型,如以下代码所示。
        场景四:自定义一个java.lang.Object类,代码和执行结果如下。你会发现明明定义了main方法,虚拟机却报出没有在Object类中找到main方法的错误。这说明了虚拟机加载的Object类是rt.jar包中的Object类,而不是用户自定义存在classpath下的Object。如果根加载器不满足双亲加载模型,则会输出AppClassLoader才对!
package java.lang;

public class Object {
	public static void main(String[] args) {
		System.out.println(Object.class.getClassLoader());
	}
	/*
	 * 输出结果: 
	 * 错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
	 * public static void main(String[] args) 否则 JavaFX应用程序类必须扩展javafx.application.Application
	 */
}
        至此,完全可以证明双亲委托机制是向上加载优先,且全部传递到根类加载器。

  • 双亲委托模型的优点

        从场景四就可以窥见双亲委托模型的优点:保证java核心类库的纯净和安全!在该模型下,java核心类库随着根加载器一起具备了具有优先级的层级关系,如果没有该模型,那么在场景四中就会导致出现多个Object,虚拟机不知道应该去加载哪一个Object才是正确的,应用程序也将出现混乱。

  • 双亲委托模型的实现

        查看java.lang.ClassLoader的实现代码,思想如下:如果该类已经被加载过了并且在虚拟机中存在缓存,则立刻返回;否则查看其是否有父类,如果有父类则让父类来加载(父类中的双亲委托模型实现代码也一致,最终会传递给跟加载器),否则直接让跟加载器来加载(没有父类,说明该加载器要么是扩展类加载器,要么就直接是根类加载器);要是这样加载也失败,说明父类加载器路径下不存在这个文件,此时交还给该类自己的加载器,调用自己的findClass()方法进行加载。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    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();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

三、类加载器——实践(自定义类加载器)

  • 自定义类加载器

        当我们在网络通信中需要传输相关的字节码的时候,往往会对传输的内容进行加密。这时候接收到的内容是经过加密的,就需要自定义一个类加载器来实现解码。

        从类加载机制的双亲委托模型原理来看,可以重写父类的loadClass(String name)来实现自定义类加载器,也可以通过重写findClass(String name)方法来实现自定义类加载器。二者实现方式的区别在于,如果重写loadClass方法,则有可能破坏双亲委托模型中向上加载优先的机制,否则就需要实现更多的代码从而增加编码负担。因此建议尽量重写findClass方法,而且在JDK1.2以后也首推重写findClass方法来实现自定义类加载器。

  • 代码示例

        场景:通过自定义的类加载器加载Test.class文件,如果发现Test类还未编译,则先编译后加载。

package myClassLoader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 自定义类加载器:根据全限定类名来加载Demo类的字节码文件,如果该类还未被编译,则先编译后加载字节码
 * 附注:仅供参考,如果错误,烦请指正,不胜感激!
 * @author reliveIT
 *
 */
public class MyAppClassLoader extends ClassLoader {
		
	//编译文件
	private boolean compileFile(String name){
		Process p = null;
		name = name.replaceAll("\\.", File.separator+File.separator)+".java";
		try {
			p = Runtime.getRuntime().exec("javac " + name);
			p.waitFor();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("编译完成!");
		return p.exitValue() == 0;
	}
	
	//加载字节码
	private byte[] getClassData(String name) throws FileNotFoundException {
		String fileName = name.replaceAll("\\.", File.separator+File.separator)+".class";
		File classFile = new File(fileName);
		if(!classFile.exists()){
			if(!compileFile(name))
				throw new FileNotFoundException("没有发现文件!");
		}
		
		FileInputStream fis = new FileInputStream(classFile);
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		byte[] bArray = null;
		try {
			bArray = new byte[fis.available()];
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		int hasRead = -1;
		try {
			while((hasRead = fis.read(bArray)) != -1){
				baos.write(bArray, 0, hasRead);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			if(fis != null){
				try {
					fis.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		System.out.println("已经将字节码转换成二进制数据流写入内存!");
		return baos.toByteArray();
	}
	
	@Override
	protected Class<?> findClass(String name) {
		// TODO Auto-generated method stub
		byte[] classData = null;
		try {
			classData = getClassData(name);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
		
		if(classData == null){
			throw new RuntimeException("类加载失败!");
		}
		
		return defineClass(name.substring(name.indexOf(".")+1), classData, 0, classData.length);
	}
	
}
package myClassLoader;

public class Main {
	public static void main(String[] args) {
		MyAppClassLoader app = new MyAppClassLoader();
		Class<?> c = null;
		try {
			c = app.loadClass("src.myClassLoader.Test");
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(c.getName());
	}
}
package myClassLoader;

public class Test {
	
}

        结果:在存放Test.java文件的文件夹下生成字节码文件Test.class。

附注:

        本文遗漏、错误之处,烦请指正,不胜感激!


Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐