JAVA虚拟机通过装载、连接和初始化一个JAVA类型,使该类型可以被正在运行的JAVA程序所使用。其中,装载就是把二进制形式的JAVA类型读入JAVA虚拟机中。连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接阶段分为三个步骤-验证、准备和解析。验证确保了JAVA类型数据格式正确并适于JAVA虚拟机使用。准备负责为该类分配它所需的内存,比如为它的类变量分配内存。解析把常量池中的符号引用转换为直接引用,如内存地址指针。在初始化期间,激活类的静态变量的初始化代码和静态代码块。
装载步骤的最终产品是一个被装载类型的Class类的实例对象,它成为JAVA程序与内部数据结构之间的接口。对于每一个被装载的类型,虚拟机都会相应地为它创建一个Class类的实例。
1 类装载器的安全作用
JAVA类装载器在JAVA安全体系结构中起着最关重要的作用,是JAVA安全沙箱的第一道防线。类装载器体系结构在三个方面对JAVA的沙箱起作用:
1) 它防止恶意代码去干涉善意的代码
2) 它守护了被信任的类库的边界
3) 它将代码归入某类(称为保护域),该类确定了代码可以进行哪些操作。
类装载器体系结构可以防止恶意代码去干涉善意的代码,这是通过为不同的类装载器装入的类提供不同的命名空间来实现的。
2双亲委派模型
JAVA虚拟机规范定义了两种类型的类装载器-启动类装载器和用户自定义类装载器,启动类装载器是JAVA虚拟机实现的一部分,通过继承ClassLoader类,用户可以创建自定义的类装载器来完成特定要求的加载。JAVA虚拟机已经创建了2个自定义类装载器-扩展类装载器和系统类装载器。
每一个用户自定义的类装载器在创建时被分配一个“双亲”parent类装载器。如果没有显示地传递一个双亲类装载器给用户自定义的类装载器的构造方法,系统类装载器就默认被指定为双亲。如果传递到构造方法的是一个已有的用户自定义类装载器的引用,该用户自定义类装载器就作为双亲;如果向构造方法传递了null,启动类装载器就是双亲。
启动类装载器Bootstrap Classloader:它是JAVA虚拟机实现的一部分,是c/c++实现的,它没有双亲。启动类装载器装载JAVA核心库代码。
扩展类装载器Extension Classloader:继承自URLClassLoader,初始化向构造方法传递了null,所以双亲是Bootstrap Classloaser。它从java.ext.dirs扩展目录中装载代码。
系统类装载器Application Classloader:继承自URLClassLoader,双亲是Extension Classloaser。它从CLASSPATH路径中装载应用程序代码。
其中,网络类装载器URLClassLoader是JAVA库提供的一个类装载器,用来从网络其他位置装载类。
双亲孩子类装载器委派链
在双亲委派模型下,当一个装载器被请求装载某个类时,它首先委托自己的双亲parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载。
现在假设要求Cindy去装载一个名为java.io.FileReader的类型。Cindy第一件事情就是去找Mom来装载那个类型;Mom所做的第一件事情就是去找Grandma来装载那个类型;而Grandma首先去找启动类装载器去装载。在这个例子中,启动类装载器可以装载那个类型,它就返回代表java.io.FileReader的Class实例给Grandma。Grandma传递该Class的引用 Mom,Mom再回传给Cindy,Cindy返回给程序。
在此模型下,启动类装载器可以抢在扩展类装载器之前去装载类,而扩展类装载器可以抢在系统类装载器之前去装载那个类,系统类装载器又可以抢在网络类装载器之前去装载它。这样,使用双亲-孩子委派链的方式,启动类装载器会在最可信的类库-核心JAVA API-中首先检查每个被装载的类型,然后,才依次到扩展路径、系统类路径中检查被装载的类型文件。用这种方法,类装载器的体系结构就可以防止不可靠的代码用它们自己的版本来替代可以信任的类。
3命名空间
由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间。命名空间由一系列唯一的名称组成,每一个被装载的类有一个名字。JAVA虚拟机为每一个类装载器维护一个名字空间。例如,一旦JAVA虚拟机将一个名为Volcano的类装入一个特定的命名空间,它就不能再装载名为Valcano的其他类到相同的命名空间了。可以把多个Valcano类装入一个JAVA虚拟机中,因为可以通过创建多个类装载器从而在一个JAVA应用程序中创建多个命名空间。
1) 初始类装载器/ 定义类装载器
命名空间有助于安全的实现,因为你可以有效地在装入了不同命名空间的类之间设置一个防护罩。在JAVA虚拟机中,在同一个命名空间内的类可以直接进行交互,而不同的命名空间中的类甚至不能觉察彼此的存在,除非显示地提供了允许它们进行交互的机制,如获取Class对象的引用后使用反射来访问。
如果要求某个类装载器去装载一个类型,但是却返回了其他类装载器装载的类型,这种装载器被称为是那个类型的初始类装载器 ;而实际装载那个类型的类装载器被称为该类型的定义类装载器。任何被要求装载类型,并且能够返回Class实例的引用代表这个类型的类装载器,都是这个类型的初始类装载器。在上面的一个例子中,java.io.FileReader定义类装载器是启动类装载器,Cindy、Mom、Grandma、启动类装载器都是初始类装载器。
虚拟机会为每一个类装载器维护一张列表,列表中是已经被请求过的类型的名字。这些列表包含了每一个类装载器被标记为初始类装载器的类型,它们代表了每一个类装载器的命名空间。虚拟机总是会在调用loadClass()之前检查这个内部列表,如果这个类装载器已经被标记为是这个具有该全限定名的类型的初始类装载器,就会返回表示这个类型的Class实例,这样,虚拟机永远不会自动在同一个用户自定义类装载器上调用同一个名字的类型两次。
2) 命名空间的类型共享
前面提到过只有同一个命名空间内的类才可以直接进行交互,但是我们经常在由用户自定义类装载器定义的类型中直接使用JAVA API类,这不是矛盾了吗?这是类型共享 原因-如果某个类装载器把类型装载的任务委派给另外一个类装载器,而后者定义了这个类型,那么被委派的类装载器装载的这个类型,在所有被标记为该类型的初始类装载器的命名空间中共享。
例如上面的例子中,Cindy可以共享Mon、Grandma、启动类装载器的命名空间中的类型,Kenny也可以共享 Mon、Grandma、启动类装载器的 命名空间中的 类型,但是Cindy和Kenny的命名空间不能共享。
3) 运行时包
每个类装载器都有自己的命名空间,其中维护着由它装载的类型。所以一个JAVA程序可以多次装载具有同一个全限定名的多个类型。这样一个类型的全限定名就不足以确定在一个JAVA虚拟机中的唯一性。因此,当多个类装载器都装载了同名的类型时,为了唯一表示该类型,还要在类型名称前加上装载该类型的类装载器来表示-[classloader class]。
在允许两个类型之间对包内可见的成员进行访问前,虚拟机不但要确定这个两个类型属于同一个包,还必须确认它们属于同一个运行时包-它们必须有同一个类装载器装载的。这样,java.lang.Virus和来自核心的java.lang的类不属于同一个运行时包,java.lang.Virus就不能访问JAVA API的java.lang包中的包内可见的成员。
4自定义类装载器
JAVA类型要么由启动类装载器装载,要么通过用户自定义的类装载器装载。启动类装载器是虚拟机实现的一部分,它以与实现无关的方式装载类型,JAVA提供了抽象类java.lang.ClassLoader,用户自定义的类装载器是类ClassLoader的子类实例,它以定制的方式装载类。所有用户自定义类装载器都实例化自ClassLoader的子类。
下面提供一个简单的用户自定义类装载器。
- import java.io.*;
- public class UserDefinedClassLoader extends ClassLoader
- {
- private String directory = "d:/classes/";
- private String extensionType = ".class";
- public UserDefinedClassLoader()
- {
- super(); // this set the parent as the AppClassLoader by default
- }
- public UserDefinedClassLoader( ClassLoader parent )
- {
- super( parent );
- }
- public Class findClass( String name )
- {
- byte[] data = loadClassData( name );
- return defineClass( name, data, 0, data.length );
- }
- private byte[] loadClassData( String name )
- {
- byte[] data = null;
- try
- {
- FileInputStream in = new FileInputStream( new File( directory + name.replace( '.', '/') + extensionType ) );
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- int ch = 0;
- while( ( ch = in.read() ) != -1 )
- {
- out.write( ch );
- }
- data = out.toByteArray();
- }
- catch ( IOException e )
- {
- e.printStackTrace();
- }
- return data;
- }
- }
- public class Valcano
- {
- static
- {
- System.out.println("Valcano Class Initialized");
- }
- public Valcano()
- {
- }
- }
- public class ClassLoaderTest
- {
- public static void main( String[] args )
- {
- try
- {
- UserDefinedClassLoader userLoader = new UserDefinedClassLoader();
- Class valcanoClass1 = userLoader.loadClass( "Valcano" );
- URL url = new URL("file:/d:/classes/" );
- ClassLoader urlLoader = new URLClassLoader( new URL[] { url } );
- Class valcanoClass2 = urlLoader.loadClass( "Valcano" );
- System.out.println( "valcanoClass1 classloaer = " + valcanoClass1.getClassLoader() );
- System.out.println( "valcanoClass2 classloaer = " + valcanoClass2.getClassLoader() );
- System.out.println( "valcanoClass1 = valcanoClass2 ? " + ( valcanoClass1 == valcanoClass2 ) );
- }
- catch( Exception e )
- {
- e.printStackTrace();
- }
- }
- }
输出结果:
valcanoClass1 classloaer = UserDefinedClassLoader@1fb8ee3
valcanoClass2 classloaer = java.net.URLClassLoader@14318bb
valcanoClass1 = valcanoClass2 ? false
我们可以看到,有两个不同的Valcano的Class实例被加载到同一个虚拟机中。
另外我们看到Valcano类静态初始化语句没有被执行,意味着类没有被初始化,这是因为JAVA中只有当类被主动使用时类型才会进行初始化。