JVM的ClassLoader过程分析

时间:2022-02-20 17:24:50

本文来自网络:深入分析Java ClassLoader原理

http://my.oschina.net/zhengjian/blog/133836

一、 JVM的ClassLoader过程以及装载原理

ClassLoader就是寻找类或是接口的字节码文件(.class)并通过解析字节码文件来构造类或接口对象的过程。在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:寻找文件、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

导入class字节码:查找class文件,导入class或者接口的字节码;

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

校验:检查导入类或接口的二进制数据的正确性;

准备:给类的静态变量分配并初始化存储空间;

解析:将符号引用转成直接引用;

初始化:激活类的静态变量的初始化Java代码和静态Java代码块。

二、classpath的作用:查找class文件

classpath的作用是指定查找类的路径:当使用java命令执行一个类(类中的main方法)时,会从classpath中进行查找这个类。
   设置classpath方式一:
  设置环境变量CLASSPATH,多个路径之间使用英文的分号隔开,也可以指定为jar包路径。
  示例:CLASSPATH=c:/myclasses/;c/mylib/aa.jar;c:/mylib/bb.jar;.
  注意:在Windows中不区分大小写,所以指定的环境变量名为classpath或是ClassPath都一样。
   设置classpath方式二:
  执行java命令时通过-classpath参数指定。
  示例:java -classpath c:/myclasses/;c:/mylib/aa.jar cn.itcast.MainApp
  注意:这样就只用这个参数指定的classpath,找不到类就报错,不会使用CLASSPATH环境变量!

结论:按classpath中指定的顺序,先从前面的路径中查找,如果找不到,在从下一个路径中查找,直到找到类字节码或是报NoClassDefFoundError。
另外一种指定class路径方式:将class类字节码文件打成jar包,并放到JRE的lib/ext/目录下,这样在执行时就可以直接找到这个类而不需要指定classpath。

如何将class类打包为jar文件?java的安装目录下面bin目录下有一个工具:jar.exe、eclipse 自带的export 和Ant, Maven之类的构建都可以轻松打包。

命令:jar cvf jar包的名字.jar 文件 文件 文件

例如:jar cvf a.jar HelloWorld.class Test.class

怎样打开jar包呢?

使用一般的解压工具就可以了。

问题如何使用java的工具执行MyEclipseKeyGen.jar呢?

比如MyEclipse6.0的注册机就是一个jar文件。

操作如下:java -jar MyEclipseKeyGen.jar。

三、Java环境中ClassLoader

java应用环境中不同的class分别由不同的ClassLoader负责加载。一个jvm中默认的classloader有Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader:

1)  Bootstrap ClassLoader称启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,主要是 %JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件。Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。可以通过sun.boot.class.path系统属性查询本机JDK环境:

System.out.println(System.getProperty("sun.boot.class.path"));

2)  Extension ClassLoader 称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

3)  App ClassLoader系统类加载器,负责加载当前java应用的classpath中的所有类。

4)  自定义ClassLoader(),自定义的ClassLoader都必须继承自抽象类java.lang.ClassLoader,重写findClass方法,这个方法定义了ClassLoader查找class的方式。

主要可以扩展的方法有:

findClass 定义查找Class的方式

defineClass 将类文件字节码加载为jvm中的class

findResource 定义查找资源的方式

可以直接使用或继承已有的ClassLoader实现,比如java.net.URLClassLoader、java.security.SecureClassLoader、 java.rmi.server.RMIClassLoader。

Extension ClassLoader 和 App ClassLoader都是java.net.URLClassLoader的子类。

这个是URLClassLoader的构造方法:

public URLClassLoader(URL[] urls, ClassLoader parent)

public URLClassLoader(URL[] urls)urls参数是需要加载的ClassPath url数组,可以指定parent ClassLoader,不指定的话默认以当前调用类的ClassLoader为parent。

代码如下:

ClassLoader classLoader = new URLClassLoader(urls);

Thread.currentThread().setContextClassLoader(classLoader);

Class clazz=classLoader.loadClass("com.company.MyClass");//使用loadClass方法加载class,这个class是在urls参数指定的classpath下边。

Method taskMethod = clazz.getMethod("doTask", String.class, String.class);//然后我们就可以用反射做些事情了

taskMethod.invoke(clazz.newInstance(),"hello","world");

四、ClassLoader加载类的原理

1、  原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

2、  为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载。当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

3、  JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。

JVM的ClassLoader过程分析

五、ClassLoader加载类的流程

Bootstrap ClassLoader是JVM级别的,由C++撰写;Extension ClassLoader、App ClassLoader都是java类,都继承自URLClassLoader超类。Bootstrap ClassLoader由JVM启动,然后初始化sun.misc.Launcher ,sun.misc.Launcher初始化Extension ClassLoader、App ClassLoader。

JVM的ClassLoader过程分析

在JAVA中,一个类通常有着一个.class文件,但也有例外。在JAVA运行时环境中(Java runtime),每一个类都有一个以第一类(first-class)的Java对象所表现出现的代码,其是java.lang.Class的实例。编译一个JAVA文件,编译器都会嵌入一个public, static, final修饰的类型为java.lang.Class,名称为class的域变量在其字节码文件中。因为使用了public修饰,可以采用如下的形式对其访问:

java.lang.Class klass = Myclass.class;

一旦一个类被载入JVM中,同一个类就不会被再次载入了(切记,同一个类)。这里存在一个问题就是什么是“同一个类”?正如一个对象有一个具体的状态,即标识,一个对象始终和其代码(类)相关联。同理,载入JVM的类也有一个具体的标识,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识。因此,如果一个名为Pg的包中,有一个名为Cl的类,被类加载器KlassLoader的一个实例kl1加载,Cl的实例,即C1.class在JVM中表示为(Cl, Pg, kl1)。这意味着两个类加载器的实例(Cl, Pg, kl1) 和 (Cl, Pg, kl2)是不同的,被它们所加载的类也因此完全不同,互不兼容的。

  例1,测试你所使用的JVM的ClassLoader

import java.net.URL;
public class Sample {
public static void main(String[] args)
{
ClassLoader cloader;
cloader = ClassLoader.getSystemClassLoader();
System.out.println(cloader);
while (cloader != null )
{
cloader = cloader.getParent();
System.out.println(cloader);
}
try
{
Class<Object> c1 = (Class<Object>)Class.forName("java.lang.Object" );
cloader = c1.getClassLoader();
System.out.println( "java.lang.Object's loader is " + cloader);
Class<Sample> c2 = (Class<Sample>) Class.forName("Sample" );
cloader = c2.getClassLoader();
System.out.println( " Sample's loader is " + cloader);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

在我的机器上( Java 1.7.2)的运行结果

sun.misc.Launcher$AppClassLoader@4d905742

sun.misc.Launcher$ExtClassLoader@3f50d5d6

null

java.lang.Object's loader is null

Sample's loader is sun.misc.Launcher$AppClassLoader@4d905742

第一行表示,系统类装载器实例化自类sun.misc.Launcher$AppClassLoader

  第二行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader

  第三行表示,系统类装载器parent的parent为bootstrap

  第四行表示,核心类java.lang.Object是由bootstrap装载的

  第五行表示,用户类Sample是由系统类装载器装载的

  注意,我们清晰的看见这个三个ClassLoader类之间的父子关系(不是继承关系),父子关系在ClassLoader的实现中有一个ClassLoader类型的属性,我们可以在自己实现自定义的ClassLoader的时候初始化定义,而这三个系统定义的ClassLoader的父子关系分别是

  AppClassLoader——————》(Parent)ExtClassLoader——————————》(parent)BootClassLoader(null c++实现)

  系统为什么要分别指定这么多的ClassLoader类呢?

  答案在于因为java是动态加载类的,这样的话,可以节省内存,用到什么加载什么,就是这个道理,然而系统在运行的时候并不知道我们这个应用与需要加载些什么类,那么,就采用这种逐级加载的方式

  (1)首先加载核心API,让系统最基本的运行起来

  (2)加载扩展类  (3)加载用户自定义的类

import java.net.URL;
public class Sample {
public static void main(String[] args)
{
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}

在上面的结果中,你可以清晰看见三个ClassLoader分别加载类的路径;也知道为什么我们在编写程序的时候,要把用到的jar包放在工程的classpath下面啦,也知道我们为什么可以不加载java.lang.*包啦!其中java.lang.*就在rt.jar包中。

六 总结:

classloader有Bootstrap ClassLoader(JVM默认 C++编写)、Extension ClassLoader和App ClassLoader

也可有自定义classloader

通过classloader 装载类的流程:

导入class字节码:查找class文件,导入class或者接口的字节码;

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

        校验:检查导入类或接口的二进制数据的正确性;

        准备:给类的静态变量分配并初始化存储空间;

        解析:将符号引用转成直接引用;

初始化:激活类的静态变量的初始化Java代码和静态Java代码块。