Java语言十五讲——第二讲 ClassLoader

第二讲 ClassLoader

1928年,狄拉克(Dirac)写下了电子的相对论量子力学方程即狄拉克方程,算出了有负能量,预言了正电子的存在,进而认为世界有反粒子反物质。大家都觉得荒唐,为什么没有观测到反物质呢?狄拉克说,那是因为反物质无处不在,就像鱼儿在水里面意识不到水的存在,只有跳出水面才会意识到水。这就是后来大家通称的“狄拉克之海”。

我们习以为常的无处不在的事物,会经常被我们所忽略,觉得它们不存在似的,比如空气,只有当雾霾发生的时候或者憋住了没有空气的时候我们才真切感受到空气也是一种真实存在的东西。

以前有学生问我ClassLoader的问题,我说其实你每天编程序都在不断地使用ClassLoader.他很吃惊。我没有瞎说话,真的就是我们每天都有用到。

先看一段类似于憋气的代码,在憋死的过程中感受一下ClassLoader的存在,代码如下:

package com.demo;

public class Test2 {
	public static void main(String[] args) {
		try {
			Class.forName("DummyClass");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

这段程序用到Class.forName(),Class.forName()调用了java.lang.ClassLoader.loadClass(),这个方法用于把类定义加载到运行环境中。

但是我们故意加载一个不存在的类DummyClass来憋死它。运行结果显示:

java.lang.ClassNotFoundException: DummyClass
 at java.net.URLClassLoader.findClass(Unknown Source)
 at java.lang.ClassLoader.loadClass(Unknown Source)
 at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
 at java.lang.ClassLoader.loadClass(Unknown Source)
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Unknown Source)
 at com.demo.Test2.main(Test2.java:7)

从上面的出错线索,我们可以看到ClassLoader使用了AppClassLoader,最后用了URLClassLoader.findClass(),这段出错线索明白地把Class Loader暴露出来了。

其实,我们应用程序员写的每一个Java类都是由ClassLoader加载的,每次运行时都会用到,只是这些由JVM自己控制,我们应用程序感觉不到而已。
那为什么我们还需要了解它呢?一是因为能帮助我们更好地理解Java,二是有些场景下还需要用到它。
好,我们先看定义:Class Loader is an object that is responsible for loading classes。

这个定义有两点要领会,class loader它是用于加载别的类的,并且,并且,并且,重要的事情说三遍,它自己也是一个对象。既然是一个对象,那就可以跟别的普通Java类一样操纵。

为了进一步往下钻研,我们得弄点预备知识,了解一下一个Java程序是如何在JVM里面运行起来的。一句简单的语句 new Test();大体经过下面的步骤:

Step 1:类级别的工作

1.1加载Loading:

加载阶段的工作是,把.class字节流就按照存储在方法区之中,然后在内存中创建一个java.lang.Class类的对象

从硬件里面(文件系统或者网络)把Test.class文件找到,然后放到JVM中的方法区,存放的是类的二进制表示如全名、静态代码、静态变量、方法定义、构造函数说明等等,有的时候我们把类的定义也说成是类的元数据(Meta Data)。

对每个加载的类,JVM会创建一个类对象(class object),对上面的例子,这是一个用于表示Test类的对象,这个对象是存放在堆区的。

注意:类对象是代表类的,所以无论程序员创建了多少个对象实例,调用多少次,JVM中总是只有这一个class object. 到这个时候,类的定义和内存表达出来了,但是并没有开始对象的创建。

1.2链接Linking:

这个阶段执行类的链接过程,给类分配内存。如果内存不够,报出OutOfMemoryError错误。它有下面的三个动作要做:

1.2.1验证Verification: 用于验证.class文件是否合规。按照字节码的规范,.class文件的格式是否正确,就是在这一步完成的。比如,文件的头四个字节都要求是CAFEBABE,这就是Java创造者对之的爱称。

1.2.2准备Preparation: 这个阶段给类里面的静态变量分配内存,赋予默认值。

1.2.3解析Resolution: 利用第一步的方法区,将符号引用转成直接内存引用。

1.3初始化Initialization:

这个阶段完成类加载,把所有静态变量赋初始值,执行静态代码块。实际JVM的实现时,这一部分要很小心,因为考虑多线程的环境,需要进行仔细的同步互锁。

至此,类级别的工作完成。(提醒一点,Java虚拟机规范规定的并不具体,具体要怎么做,不同的虚拟机的实现都不一样。)整个程序的生命周期中,类加载过程只会做一次

这样,一个用文件方式存储的java文件就加载到JVM中,有了关于类结构的内存表示。

这样的一个神奇的好处是,Java可以用任何方式存放程序,不一定是文件,只要修改与ClassLoader相关的代码就可以把程序用另外一种方式存储。在JVM中,只认规定的类内存结构,不用管具体的文件或者文件系统。一个Java程序,一旦所有的类都加载到JVM,就全部自由生活在JVM的美妙虚拟空间了。

Step 2:对象级别的工作

经过第一步,我们的类就准备好了,对象有了模子。创建对象(也叫类的一个实例)的事情就简单了。

2.1 为对象在堆中分配内存,注意的是,实例字段包括自身定义的和从父类继承下来的。

2.2 对实例内存进行零值初始化。

2.3 调用对象的构造函数。

好了,我们现在在JVM中创建好了对象。好简单。是的,有了女娲,造人就简单了。

扩展一下讨论,我们还可以写一个程序,看看程序执行顺序。

 

先准备一个Stub类,只是一个简单的对象:

public class Stub {
	public Stub(String s) {
		System.out.println(s + " object created.");
	}
}

再写一个Parent类,里面写几种不同的语句,我们看执行的先后顺序:

public class Parent {
	static Stub parentStaticObject = new Stub("Parent static object - ");
	static {
		System.out.println("parent static code execute.");
	}
	{
		System.out.println("parent code execute.");
	}
	Stub parentObject = new Stub("Parent code create object - ");
	Stub stub;
	public Parent() {
		System.out.println("parent constructor execute.");
		stub = new Stub("Parent constructor create object.");
	}
	public void sayHello() {
		System.out.println("hello from Parent");
	}
}

里面的语句有静态代码块,静态变量初始化,非静态代码块,实例变量初始化,构造函数和普通方法。

再写一个子类继承:

import javax.rmi.CORBA.Stub;

public class Child extends Parent {
	static Stub parentStaticObject = new Stub("Child static object - ");
	static {
		System.out.println("Child static code execute.");
	}
	{
		System.out.println("Child code execute.");
	}
	Stub parentObject = new Stub("Child code create object - ");
	Stub stub;

	public Child() {
		System.out.println("child constructor execute.");
		stub = new Stub("Child constructor create object.");
	}
	public void sayHello() {
		System.out.println("hello from Child.");
	}
}

同样的,里面的语句有静态代码块,静态变量初始化,非静态代码块,实例变量初始化,构造函数和普通方法,并且覆盖了父类的方法。

最后用一个测试程序看看输出结果:

import javax.rmi.CORBA.Stub;

public class StartTest {
	static {
		System.out.println("Tester static code execute.");
	}
	static Stub testerStaticObject = new Stub("Tester static object - ");
	{
		System.out.println("Tester code execute.");
	}
	Stub testerObject = new Stub("Tester code create object - ");
	public static void main(String[] args) {
		System.out.println("main() execute.");
		Child c = new Child();
		c.sayHello();
		((Parent) c).sayHello();
	}
}

运行一下,结果如下:

Tester static code execute.
Tester static object -  object created.
Parent static object -  object created.
parent static code execute.
Child static object -  object created.
Child static code execute.
main() execute.
parent code execute.
Parent code create object -  object created.
parent constructor execute.
Parent constructor create object. object created.
Child code execute.
Child code create object -  object created.
child constructor execute.
Child constructor create object. object created.
hello from Child.
hello from Child.

从这个执行顺序结果得出我们的结论:

  1. 静态代码最先执行,主程序静态代码 -> 父类静态代码 -> 子类静态代码。
  2. 执行主程序main()
  3. 父类非静态代码 -> 父类实例队形初始化 -> 父类构造函数
  4. 子类非静态代码 -> 子类实例队形初始化 -> 子类构造函数
  5. 普通方法

还有一个小地方要注意,子类覆盖的方法调用,无论是c.sayHello(); 还是((Parent)c).sayHello(); 结果都是调用的子类中的方法。这是面向对象编程中多态的体现。

有了上面的预备知识,接下来我们继续往前走。

在Java中,有三种Class Loader存在,应用层的,扩展层的,平台核心层的。

我们通过一个例子来了解,代码如下:

public class Test1 {
	public static void main(String[] args) {
		ClassLoader classLoader = Test1.class.getClassLoader();
		System.out.println(classLoader);

		ClassLoader classLoader1 = classLoader.getParent();
		System.out.println(classLoader1);

		ClassLoader classLoader2 = classLoader1.getParent();
		System.out.println(classLoader2);
	}
}

运行之后的结果是:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null

从这个结果可以看出,一个简单的运行都会涉及到三个class loader,application class loader,extension class loader,和bootstrap class loader(没有显示出来,因为bootstrap class loader是native代码,所以不在JVM中,于是返回的是null)。

Application class loader 加载我们自己写的类;

Extension class loaders 加载Java核心类的扩展部分,$JRE_HOME/lib/ext 目录下的类;

Bootstrap class loader加载Java平台核心类诸如 java.lang.Object, java.lang.Thread etc)以及rt.jar中的类。

注:上面的返回结果不是确定的,因为这不是语言本身规定的,不同的JVM厂商有不同的实现。我给的例子用到的是Sun公司提供的虚拟机。

几个类加载器之间是有层次关系的,称为Delegation Model

A ClassLoader instance will delegate the search of the class or resource to the parent class loader。

  1. 一个类加载器自己先不加载,而是交给它的Parent去处理,
  2. Parent又交给它的Parent去处理,
  3. 一层层委托上去一直到Bootstrap Class Loader,
  4. 如果Parent发现自己加载不了这个的class,
  5. 才会反交给下层加载。

一般情况下是这样的次序,

  1. 先是Application Class Loader出动加载客户程序,它自己不做,
  2. 交给上层的Extension Class Loader,
  3. 再交给Bootstrap Class Loader,
  4. 之后方向反过来,Bootstrap Class Loader发现不能加载,
  5. 就返给Extension Class Loader,
  6. 如果还是加载不了,最后再返给Application Class Loader

可能大家都听晕了。为什么Java采取这样一种初看起来莫名其妙的方式进行类加载呢?主要的原因在于安全性。我们想象如下一个场景:

JVM起来的时候,有一些系统级的最基本的类就会运作,这是虚机的核心。

大多数在rt.jar里面,像Java语言本身的基本类型,java.lang包里面的东西。

这是Java的基础,别的客户程序都是依赖这个核心的。但是没有一条规则去阻止你自己写同样的类,下面我们就非要写这么一个类:

package java.lang;

public class Integer {
	public Integer() {
	}

	public static void main(String[] args) {
		System.out.println("This is a fake Integer class. Hah Hah!");
	}
}

这是一个捣乱的类。包名和类名跟Java平台的一样。build是不会有问题的,没有语法规则阻止这样的事情。但是在运行的时候,我们希望出现提示:This is a fake Integer class. Hah Hah!但是实际上出现的却是:在类 java.lang.Integer 中找不到 main 方法

这就奇怪了,我们明明写了main()方法的。

这个时候,其实就是ClassLoader的委托机制在起作用,一层层委托到Bootstrap Class Loader的时候,它发现自己能加载(因为Bootstrap Class Loader负责加载rt.jar里面包含的所有类,java.lang.Integer这个名字在这些类里面),就加载它,事实上就是加载了rt.jar里 java.lang包之中的那个Java基础类Integer。我们自己写的这个类并没有加载,所以才会在运行时发现并没有main()方法

通过这个方式,Java做到了让自己系统的那些基础的类安全加载进来,而不会被别的程序偷梁换柱。保证Java虚拟宇宙的正常秩序,这样得以维持体系的安全。如果这些系统级别的基础类都被人替换掉了,整个JVM就失控了,大厦将倾。

了解了这些之后,我们就能理解它背后的设计者的匠心。

赞曰:虚机岂无缝?匠心独运成。

另外注意一点,这里类加载器之间的层次关系不是继承的关系来实现的,是通过组合关系(Composite)实现的。概念上,继承是is a,组合是as a。

编程实践中,Bruce Eckel(Java名著《Thinking in Java》作者)说过:能不用继承就不用继承,尽量优先使用组合。

这里引来了一个有趣的问题,ClassLoader本身也是一个类,它也需要被加载,那最早的那个怎么开始的呢?

Java设计了一个根加载器Bootstrap class loader,他是JVM核心的一部分,不是Java编写的,因为不是一个Java对象,我们的Java程序里面也看不到它。这个根加载器就是Java虚机的第一推动。

对Java程序来讲,JVM就是整个宇宙,Bootstrap Class Loader把原初对象放置到了宇宙的舞台,而James Gosling就是创造宇宙的上帝。
一切动都有因,难怪牛顿相信宇宙有第一推动。

好,介绍了这些。我们来看看怎么写自己的Class Loader。

首先要明白一个问题:为什么要写自己的Class Loader?不管那么多不是一样好好在用吗?

一般情况确实是这样,不过有些时候为了一些特殊需求,我们会用到自己定制的Class Loader。

比如1998年, Sun内部为完成JDK1.2忙得热火朝天,我也在里面打酱油,我们有一个小组提供一个工具,给Java生成的字节码加密,原因是字节码太规整,用一些工具很容易反编译,反编译之后的结果很容易供人看懂(比许多程序员手工编的程序还容易懂),导致知识产权保护不力。
于是就想着把.class文件加密,但是一个加密之后的.class文件肯定又不能被正确加载。怎么办呢? 就要自己做一个Class Loader,拿到.class文件,先进行一步解密,解密之后就成了正常的字节码,就可以用普通的application class loader的方式去继续加载了

还有一些别的场景,也需要我们自定义Class Loader,如动态生成的class文件,如多版本class运行。

我们先来看,从哪里下手做这个工作。

从大的过程,对象的创建分成两大步骤,一个是类级别的工作,一个是对象的实例化。显然,我们要在类级别工作着一个步骤动脑筋。而在类级别的工作中,分成加载Loading,链接Linking,初始化Initialization三步。根据上面讲解的一些知识,我们应该在Loading过程中搞一点名堂。

回到ClassLoader的定义,我们来看提供了哪些方法。

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException

这个loadClass()方法根据类的全名加载类。既然是这样,那就应该从这里下手了。

我们看看源码

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.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
} 

我们可以看到这个过程,先看这个类是不是已经加载了,如果没有就让上层的class loader去加载,最后由自己加载findClass(name)。

事情就追到findClass(name)方法了。看它的定义:

protected Class<?> findClass(String name) throws ClassNotFoundException

在ClassLoader里面,它只是一个空的方法,没有做实现。那么我们就可以实现它来进行我们自己的工作了。

好,我们在findClass()里面做什么呢?

自然先要从外部如文件系统或者网络获取.class字节流,然后呢?我们自己把它弄成类模型吗?理论上是,但是实际上我们不需要。接着找ClassLoader为我们提供了什么,我们可以看到有一个defineClass()方法,定义如下:

protected final Class<?> defineClass(  String name, byte[] b, int off, int len) throws ClassFormatError

这个方法就是用于将字节流转成类的。

并且这个方法是final的,我们用它并且只能用它。

有了这个方法,我们自己的工作就简单了,只需要对字节流进行处理后交给defineClass()就好了。

我们可以动手编程序了。

先做一个类,MyClass.java,代码如下:

package com.demo;

public class MyClass {
	public MyClass() {
	}

	public void show() {
		System.out.println("show test!");
	}
}

很简单,不解释了。

接着我们写自己的class loader,为了简单起见,我们的这个class loader不进行任何加密处理,只是简单地读取.class文件,生成类,模仿标准的AppClassLoader的行为。MyClassLoader.java代码如下:

package com.demo;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MyClassloader extends ClassLoader {
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		String classFileName = name.replace(".", "/") + ".class";
		byte[] cLassBytes = null;
		Path path = null;
		try {
			path = Paths.get(getResource(classFileName).toURI());
			cLassBytes = Files.readAllBytes(path);
		} catch (IOException | URISyntaxException e) {
			e.printStackTrace();
		}
		Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
		return clazz;
	}
}

正如上面根据原理分析的,我们自己写的class loader override了findClass(),

这里面我们做了几步:

一是根据class名字拼出文件名,

String classFileName = name.replace(".","/") + ".class";

二是根据名字定位到文件路径,

path = Paths.get(getResource(classFileName).toURI());

三是把文件读到字节数组中,

cLassBytes = Files.readAllBytes(path);

四是调用defineClass()生成类,

Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);

有了这个自定义的class loader之后,我们写一个测试类,MyClassTest.java,代码如下:

package com.demo;

import java.lang.reflect.Method;

public class MyClassTest {
	public static void main(String[] args) throws ClassNotFoundException {
		MyClassloader loader = new MyClassloader();
		Class<?> aClass = loader.findClass("com.demo.MyClass");

		try {
			Object obj = aClass.newInstance();
			Method method = aClass.getMethod("show");
			method.invoke(obj);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

在程序中,我们新建一个自定义的类加载器MyClassloader loader = new MyClassloader();

然后调用findClass()去加载类Class<?> aClass = loader.findClass("com.demo.MyClass");

之后根据类定义创建新的对象实例并调用方法

        Object obj = aClass.newInstance();
        Method method = aClass.getMethod("show");
        method.invoke(obj);

试着运行一下,程序正确出了结果。

到了这一步,恭喜你!你已经知道如何做Class Loader了,We Made It!

再回到加密的问题,这一讲不讲加密本身,但是我们要根据上面的代码知道如何处理加密。看我们自己的findClass()的实现,里面有两句话:

cLassBytes = Files.readAllBytes(path);
Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);

在defineClass之前,我们要准备出一个正常的字节数组,因此对于一个加密的.class文件,我们只需要在之前处理进行解密即可:

cLassBytes = decrypt(Files.readAllBytes(path));
Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);

假定有一个decrypt()方法,我们就可以了做到了。

以前在Sun公司的时候,一个同事曾经说过class loader就像是一个汤勺,用它来抓汤圆吃。这个比喻真有点像。

不过这个比喻不够,因为class loader不光是加载类,还规定了命名空间,不同的class loader加载的类是不能互相访问的(正因为这样才会有同一个类的多版本支持的技术)。

我们可以把class loader圈起来的这片空间理解为class园地。因此,class loader更加像是一个碗,把汤圆盛到里头了。

这个话题超越了进阶阶段,先按下不表。不过到了这里,我们应该能感受到Class Loader的强大了,从心底里对Java的发明人一次次投以敬佩的目光,他们二十几年前竟然有如此深刻的洞察。对我们Java程序员来讲,James Gosling就是我们的上帝。

在未来的讲座里,我还会跟大家一次次展示出造物者的匠心独运。

学习的过程,是一步步深入的。

很多人工作多年,并没有深入,有些时候也不是因为不努力,而是因为他们在的编程任务总是在无休止地对付客户需求的变更,没有坐下来好好整理。

而从应用的层面,学习技术就成了追逐新的热点,不断地换最新推出的环境框架工具。

而写应用程序,大体上用不到深入的内容,非专业的人突击学几个月之后也能一起做编程了,有的时候比本专业的人做得还要好。那么学习本专业的价值在哪里?这是许多年轻人曾经的疑惑。

或许与国家的发展阶段有关,前些年都是拿别人现成的平台框架工具,或开源或盗版,自己针对客户需要快速搭建应用系统,赚快钱,现在慢慢认识到基础的重要性了,真正愿意深入理解技术的人和公司也多起来了。这对于希望掌握和使用更深技术的程序员是一件好事情。

从事技术工作是一个良心活儿,学习技术则是一个慢工夫,急不来,就得要老老实实一个课题一个课题解决掉。只要方向对,扎扎实实,总能有成。

先贤语录:不积跬步,无以至千里。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值