类加载器体系
类加载器是沙箱的第一道防线,毕竟代码都是由它装入jvm中的,其中也包括有危险的代码。它的安全作用有三点:
一 保护善意代码不受恶意代码的干扰
二 保护已验证的类库
三 代码放入有不同的行为限制的各个保护域中
类加载体系通过使用不同的类加载器把类放入不同的名字空间中从而保护善意代码不受恶意代码的干扰。
JVM为每个类加载器维护一个名字空间。例如,如果jvm在某个名字空间中加载了一个称为volcano的类,就不能再在这个名字空间中加载另一个也称为volcano的类,除非你再创建另一个名字空间。也就是说,如果jvm有三个名字空间,你就可以加载三个叫做volcano的类,一个名字空间一个。
在jvm中,同一个名字空间中的类是可以直接交互的,但在不同名字空间中的类就不行,除非提供另外的机制。这样,名字空间就起到了一个屏障的作用。
在图3-1中,显示了两个名字空间,各有一个类加载器,各加载了两个类型,两个名字空间都有一个叫做volcano的类型。左边颜色较深的类加载器加载了类型climber和volcano,右边颜色较浅的类加载器加载了类型bakingsoda和volcano。图中的箭头表示名字空间中的名字对在方法区(method area)中对应类型定义数据的引用。因为名字空间的屏障作用,当climber引用volcano时,是指的同一个名字空间中的volcano。尽管都在同一个jvm,它也无法知道另一个名字空间中的volcano,。如果想知道如何达到名字空间的分隔的,可以看第八章“链接模式”
类加载器体系可以通过使用不同的类加载器加载可信包(trusted packages)和不可信包(untrusted packages)从而保护可信包的安全。尽管你可以对同一包中的类型制定访问控制,但这种控制只有在被同一个加载器加载的前提下才起作用。
通常,一个用户定义的类加载器需要依赖其他类加载器来完成任务,至少需要一个在jvm启动时创建的类加载器,这个类加载器称为启动类加载器。在1.2版本以前,类加载器必须显示的调用其他类加载器,如调用其他用户定义的类加载器的loadClass方法,或者调用启动类加载器(bootstrap class loader)的静态函数findSystemClass()。
在1.2版本中,一个类加载器要求另一个类加载器加载某个类型的过程被规范化为代理模式(parent-delegation model 译者:就是Chain of Responsibility模式)
在某个类加载器试图以自己的方式加载一个类时,它首先缺省把这个工作交给自己的父对象。而这个父对象又会首先把这个任务交给自己的父对象处理,这样这个任务会一直传到启动类加载器,因为启动类加载器通常是代理链的最后一个类。如果父类加载器能够加载这个类型,就会返回此类型,否则由子类加载器处理。
在1.2版本以前的多数jvm的实现中,内建类加载器负责加载本地可用的类文件,通常包括java应用的类文件和所有这个应用需要的库,尽管加载所需类文件的方式根据应用不同而不同,但许多应用都以class path定义的路径来搜寻所需类文件。
在1.2版本中,加载本地可用类文件的任务被分解给了多个类加载器。以前称为原始类加载器(primordial class loader)的类加载器被改称为启动类加载器,用来表示它只用来加载核心java api的类文件,因为核心java api类文件是用来启动jvm的。
而负责加载其他类文件的任务都给了用户定义的类加载器(译者:这里指广义用户,包括虚拟机的实现者),这些类文件包括应用的类文件,用来进行安装和下载的标准扩展类文件,用来在class path中查找库的类文件等等。
因此当1.2的JVM开始运行时,它会创建至少一个用户定义的类加载器,所有的这些类加载器串成一个链,在链的头部是启动类加载器,在链的尾部是系统类加载器(system class loader)。在1.2之前,有时称内建类加载器为系统类加载器,在1.2,这个名字被更正式的用于称呼java应用所创建的新的类加载器的父亲。
这个缺省父代理通常来加载应用的初始类,但任何用户定义的类加载器都可能被java平台的设计者所改变。
例如,假如你写了一个应用,此应用需要安装一个类加载器,用来加载从网络上下载的类文件。这个应用运行在一个jvm上,而这个jvm有两个用户定义的类加载器,一个是安装扩展类加载器,另一个是类路径类加载器。它们和启动类加载器串成一个链,依次为:启动类加载器,安装扩展类加载器,类路径加载器。
如图3-2,类路径加载器被设计成了系统类加载器,它将是java应用新的类加载器的父亲。当你的应用的网络类加载器被安装时,它将这个系统类加载器设为它的父亲。
假如在java应用运行中需要加载一个称为volcano的类,你的类加载器会首先把这一任务交给它的父亲,类路径类加载器,去查找和加载这个类文件。而类路径类加载器同样首先交给自己的父亲,安装扩展类加载器,去完成任
务。这样,这个任务最后交给启动类加载器去首先尝试处理。
假设类volcano不是java api的一部分,也不是安装扩展和类路径的一部分,所有对应的类加载器都没有返回这个类型,这样就轮到你自己的类加载器了。它将会从网络上下载此类文件,这样这个类就称为你应用中的一部分了。
我们继续这个例子,假如以后某个时候第一次调用了类volcano的一个方法,这个方法中引用了java api中的类java.util.HashMap,而这个类是这个应用第一次引用,这样jvm就要求你的类加载器去加载这个类。象以前一样,这个请求最终到达了启动类加载器。
但这次不同,启动类加载器能够加载java.util.Hashmap并把它返回给了你的类加载器。这样安装扩展类加载器和类路径类加载器只起到了一个传递的作用,而你的类加载器也不用从网络下载这个类文件了。从此,在类volcano中,所有对java.util.Hashmap的引用都会使用这个类。
有了这个背景知识,我们就可以看看类加载器是如何被用来保护可信库(trusted libraries)的了。类加载器体系通过防止不可信类冒充可信类保护了可信类的边界,防止了对java runtime安全的潜在威胁。
有了这个链状的代理关系,我们知道,要加载一个类,需要链上的类加载器按特定顺序逐次检查,这样自己定义的类加载器始终处于一个较低优先级的状态,如果你自己的类加载器想要从网络上下载一个叫做java.lang.Integer的类是不可能的。它只能使用从父类加载器传来的类型。通过这种方法,防止了用不可信代码替换可信代码的发生。
但假如代码不准备去替换一个可信类型,而只想在可信包中插入一个新类型呢?假如在前面例子中,你的网络类加载器想加载一个叫做java.lang.Virus的类。象以前一样,加载类的要求在链内传递,直到启动类加载器,尽管启动类加载器负责加载核心java api的类,其中也包括一个叫java.lang的包名,但找不到Virus,我们假设这个类同样在安装扩展类加载器和类路径类加载器中也没有找到。这样你的网络类加载器就从网络上下载了这个类。
假设你成功下载了类java.lang.Virus,Java对在同一个包中的类的相互访问有一定的特权。因此,因为你的类加载器加载了一个无耻的宣称自己是java api的一部分的类java.lang.Virus,你肯定希望能够享受到某种特权,从而干一些罪恶的勾当。但类加载机制制止了这种事情的发生,因为类加载机制限制这种特权只有在同一个类加载器加载的前提下。
因为java api的java.lang包中的可信类都由启动类加载器加载,而邪恶的java.lang.Virus由你的网络类加载器加载,他们并不属于同一个运行包(runtime package)。
运行包这个术语首次在jvm第二版的规范中引入,指由同一个类加载器加载的同一个包中的所以类型。
在允许同一个包中的两个类型访问之前,jvm还有确信此两个类型是由同一个类加载器加载的。
因此,jvm不允许java.lang.Virus去访问java api中java.lang 包中的其他类型,因为他们不是由同一个类加载器加载的。
引入运行包的目的之一就是使用不同的类加载器加载不同类型的类文件。启动类加载器用来加载最值得信赖的核心java api。安装扩展类加载器用来加载安装扩展的任何类文件。虽然安装扩展也是可以信赖的,但还没有到可以向java api添加新类型的程度。同样,类路径类加载器加载的类也不能访问安装扩展和java api中的类型。
类加载器也可以简单的禁止加载某些类型来保护可信代码。
例如,你可能安装了某些包,其中有一些类你想由类路径类加载器来加载,而不是你的网络类加载器。
假设你创建一个叫做absolutepower的包,并把它安装在了类路径类加载器的访问范围内。同时你希望由你的类加载器加载的类不能加载absolutepower包中任何类。这样在你的类加载器中的第一件事就是检查需要加载的类是不是把自己声明为absolutepower中的类,如果是,则抛出一个异常,并不是交给父类加载器来处理。
类加载器机制除了屏蔽不同名字空间,保护可信类库外,还把每个加载的类放到了一个保护域(protection domain)中,保护域对类的活动范围也有一个定义。