深入Java虚拟机读书笔记第三章安全

时间:2022-04-03 23:06:32

为什么需要安全性

  Java的安全模型是其多个重要结构特点之一,它使Java成为适于网络环境的技术。Java安全模型侧重于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序(以及善于程序中的bug)的侵犯。为了达到这个目的,Java提供了一个用户可配置的“沙箱”,在沙箱中可以防止不可靠的Java程序。沙箱对不可靠程序的活动进行了限制,程序可以在沙箱的安全边界内做任何事,但是不能进行任何跨越这些便捷的举动。在Java 1.1中引入了基于代码签名和认证的信任模式。

基本沙箱

组成Java沙箱的基本组件:

1、类装载结构

2、class文件检验器

3、内置于Java虚拟机(及语言)的安全特性

4、安全管理器及Java API

  Java的沙箱安全模型,最重要的优点之一就是这些组件中的类装载器和安全管理器是可以由用户定制的。通过这些组件, 可以为Java程序创建个性化的安全策略。但是,这种可定制性是需要代价的,因为这种体系结构的灵活性也对它本身产生一定的危险。类装载器和安全管理器非常复杂,因此单纯的定制操作也可能潜在地产生错误,从而开启安全漏洞。

类装载器体系结构

  在Java沙箱中,类装载器体系结构是第一道防线。毕竟,是由类装载器将代码装入Java虚拟机中。类装载器体系结构在三个方面对Java的沙箱起作用:

1、它防止恶意代码去干涉善意的代码

2、它守护了被信任的类库的边界。

3、它将代码归入某类(保护域)

  类装载器体系结构可以防止恶意的代码去干涉善意的代码,这是通过为不同的类装载器装入的类提供不同的命名空间来实现的。命名空间由一系列唯一的名称组成,每一个被装载的类有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的。例如,一旦Java虚拟机将一个名为Volcano的类装入一个特定的命名空间,它就不能再装入名为Volcano的其他类到相同的命名空间了。可以把多个Volcano类装入一个Java虚拟机中,因为可以通过创建多个类装载器在一个Java应用程序中创建多个命名空间。

深入Java虚拟机读书笔记第三章安全

  命名空间有助于安全的实现,因为你可以有效地在装入了不同命名空间的类之间设置一个防护罩。在Java虚拟机中,在同一个命名空间内的类可以直接进行交互,而不同的命名空间中的类不能察觉彼此的存在,除非显示地提供了允许它们进行交互的机制。

  下图显示了和两个装载器有关的命名空间,它们都装载了一个名为Volcano的类。命名空间的每一个命名都被关联到方法区的一个类型数据,这个类型数据用哪个名字定义了一个类型。图中画出了从命名空间中的名字指向方法区中的类型的箭头,方法区定义了类型。

深入Java虚拟机读书笔记第三章安全

  类装载器体系结构守护了被信任的类库的边界,这是通过分别使用不同的类装载器装载可靠的包和不可靠的包来实现的。虽然通过赋给成员受保护(或包访问)的访问限制,可以在同一个包中的类型间授予彼此访问的特殊权限,但这种特殊的权限只能授给在同一个包中的运行时成员,而且它们必须是由同一个类装载器装载的。

  用户自定义类装载器经常依赖其他类装载器---至少依赖于虚拟机启动时创建的启动类装载器---来帮助它实现一些类装载请求。类装载器的装载实现参考ClassLoader类。在版本1.2以前,非启动类装载器必须显示地求助于其他类装载器,类装载器可以请求另一个用户自定义的类装载器来装载一个类,这个请求是通过对被请求的用户自定义类装载器调用loadClass()来实现的。除此以外,类装载器也可以通过调用findSystemClass()来请求启动类装载器类装载类,这是类ClassLoader中的一个静态方法。在版本1.2中,类装载器请求另一个类装载器来装载类型的过程被形式化,成为双亲委派模式。从版本1.2开始,除启动类装载器以外的每一个类装载器,都有一个“双亲”类装载器,在某个特定的类装载器试图以常用方式装载类型以前,它会先默认地将这个任务“委派”给它的双亲---请求它的双亲来装载这个类型。这个双亲再依次请求它自己的双亲来装载这个类型。这个委派的过程一直向上继续,直到达到启动类装载器,通常启动类装载器是委派链中的最后一个类装载器。如果一个类装载器的双亲类装载器有能力来装载这个类型,则这个类装载器返回这个类型。否则,这个类装载器视图自己来装载这个类型。

  在版本1.2以前的大多数虚拟机的实现中,内置的类装载器(原始类装载器)负责在本地装载可用的class文件,这些class文件通常包括那些要运行的Java应用程序的class文件,以及这个应用程序所需要的任何类库,这些类库中包含Java API的基本class文件。虽然如何找到这些被请求类型的class文件属于实现细节,但是许多实现都是按照类路径(class path)指明的顺序查找目录和JAR文件的。

  在版本1.2中,装载本地可用的class文件的工作被分配到多个类装载器中。原始类装载器的内置的类装载器被重新命名为启动类装载器,表示它现在只负责装载那些核心Java API的class文件,因为核心Java API的class文件是用于“启动”Java虚拟机的class文件,所有启动类装载器的名字也因此而得。

  在版本1.2中,由用户自定义类装载器来负责其他class文件的装载,例如用于应用程序运行的class文件,用于安装或下载标准扩展的class文件,在类路径中发现的类库的class文件等等。当1.2版本的Java虚拟机开始运行时,在应用程序启动以前,它至少创建一个用户自定义类装载器,也可能创建多个。所有这些类装载器被连接在一个双亲-孩子的关系链中,在这条链的顶端是启动类装载器,在这条链的末端是一个在版本1.2中被称为“系统类装载器”的装载器。在版本1.2中,系统类装载器这个名字有更正式的定义,它是指由Java应用程序创建的,新的用户自定义类装载器的默认委派双亲。这个默认的委派双亲通常是一个用户自定义的类装载器,它装载应用程序的初始类,但是它也可能是任何用户自定义类装载器,这是由实现Java平台的设计者决定的。

  例如,假设你写了一个Java应用程序,装载了一个类装载器,这个装载器是通过网络下载来装载class文件的。设想你在虚拟机上运行这个应用程序,在启动时实例化了两个用户自定义类装载器:一个“已安装扩展”的类装载器,一个“类路径”类装载器。这些类装载器和启动类装载器一起连入一个双亲-孩子关系链中,如图所示。类路径的类装载器的双亲是已安装了扩展的类装载器,而它的双亲是启动类装载器。在图中,类路径类装载器被设计成系统类装载器,新的用户自定义类装载器的默认委派双亲被应用程序实例化。假设当应用程序实例化它的网络类装载器时,它指明了系统类装载器作为它的双亲。

深入Java虚拟机读书笔记第三章安全

  设想在运行Java应用程序的过程中,类装载器发出一个装载Volcano类的请求,类装载器必领先询问它的双亲---类路径类装载器---来查找并装载这个类。这个类路径类装载器依次将向它自己的双亲发出同样的请求,它的双亲即为已安装扩展的类装载器。这个类装载器也是首先向它自己的双亲发出同样的请求,它的双亲即为已安装扩展的类装载器。这个类装载器也是首先将这个请求委派给它自己的双亲---启动类装载器。假设Volcano类不是Java API的一部分,也不是一个已安装扩展的一部分,也不在类路径上,所有这些类装载器将返回而不会提供一个名为Volcano的已安装类。当类路径类装载器回答,它和所有它的双亲都不能装载这个类时,你的类装载器可能试图用它自己特定的方式来装载Volcano类,它会通过网络下载Volcano。假设你的类装载器可以下载Volcano类,这样Volcano类就可以在应用程序以后的执行过程中发挥作用。我们继续讨论这个例子,假设以后的某一个时刻Volcano类的一个方法首次被调用,并且那个方法引用了Java API中的java.util.HashMap,因为这个引用是首次被运行的程序使用,所以虚拟机会请求你的类装载器(装载Volcano的类装载器)来装载java.util.HashMap。就像以前一样,你的类装载器首先将请求传递给它的双亲类装载器,然后这个请求一路委派直到委派给启动类装载器。但是这一次,启动类装载器可以将java.util.HashMap类返回给你的类装载器,因为启动类装载器可以找到这个类,所以已安装扩展的类装载器就不必在已安装扩展中查找这个类型,类路径类装载器也不必在类路径中查找这个类型,同样你的类装载器也不必从网上下载这个类。所有这些类装载器仅需要返回由启动类装载器返回的类java.util.HashMap。从这一时刻开始,不管何时Volcano类引用名为java.util.HashMap的类,虚拟机就可以直接使用这个java.util.HashMap类了。

  在有双亲委派模式的情况下,启动类装载器可以抢在标准扩展类装载器之前去装载类,而标准扩展类装载器可以抢在类路径类装载器之前去装载那个类,类路径类装载器又可以抢在网络类装载器之前去装载它。这样,在使用双亲-孩子委派链的方法中,启动类装载器会在最可信的类库---核心Java API---中首先检查每个被装载的类型,然后,才依次到标准扩展、类路径上的本地类文件中检查。所以,如果网络类装载器装载的某段移动代码发出指示,试图通过网络下载一个和Java API中某个类型同名的类型,例如java.lang.Integer,它将不能成功。如果java.lang.Integer的class文件在Java API中已存在,它将被启动类装载器抢先装载,而网络类装载器将没有机会下载并定义名为java.lang.Integer的类,它只能使用由它的双亲返回的类,这个类应该是由启动类装载器装载的。用这种方法,类装载器的体系结构就可以防止不可靠的代码用它们自己的版本来替代可信任的类。

  但是如果这个移动代码不是去试图替换一个被信任的类,而是想在一个被信任的包中插入一个全新的类型,情况会怎样呢?想象一下,如果刚才那个例子中,要求网络类装载器装载一个名为java.lang.Virus的类时,将会发生什么?像以前一样,这个请求将首先被一路向上委派给启动类装载器,虽然这个启动类装载器负责装载核心Java API的class文件,包括名为java.lang的包,但它无法在包java.lang中找到名为Virus的成员。假设这个类在已安装扩展以及本地类路径中也找不到,你的类装载器将试图从网络上下载这个类。

  假设你的类装载器成功地下载并定义了这个名为java.lang.Virus的类,Java允许在同一个包中的类拥有彼此访问的特殊权限,而这个包外的类则没有这个权限。你的类装载器装载了一个名为java.lang.Virus的类,暗示这个类是Java API的一部分,可想而知,它将得到访问java.lang中被信任类的特殊访问权限,并且可以使用这个特殊的访问权限达到不可告人的目的。类装载器机制可以防止这个代码得到访问java.lang包中被信任类的访问权限,因为Java虚拟机只把彼此访问的特殊权限授予由同一个类装载器装载到同一个包中的类型。因为Java API的java.lang包中的被信任类是由启动类装载器装载的,而恶意的类java.lang.Virus是由网络类装载类装载的,所以这些类型不属于同一个运行时包。运行时包这个名词是在Java虚拟机第2版规范中第一次出现的,它指由同一个类装载器装载的、属于同一个包的、多个类型的集合。在允许两个类型之间对包内可见的成员(声明为受保护的或包访问的成员)进行访问前,虚拟机不但要确定这两个类型属于同一个包,还必须确认它们属于同一个运行时包---它们必须是由同一个类装载器装载的。这样,因为java.lang.Virus和来自核心Java API的java.lang的成员不属于同一个运行时包,java.lang.Virus就不能访问Java API的java.lang包中的类型和包内可见的成员。

  之所以提出运行时包的概念,动机之一是使用不同的类装载器装载不同的类。类装载器可以用另一种方法来保护被信任的类库的边界,它只需通过简单地拒绝装载特定的禁止类型就可以了。例如,你可能已经安装了一些包,这些包中包含了应用程序需要装载的类,这些类必须是由网络类装载器的双亲---类路径类装载器---装载的,而不是由网络类装载器装载的。假设已经创建了一个名为absolutepower的包,并且将它安装在了本地类路径中的某个地方,在这里它可以被路径类装载器访问到。而且假设你不想让由自己的类装载器装载的类,能装载来自absolutepower包中的任何类。在这种情况下,必须编写自己的类装载器,让它做的第一件事就是确认被请求的类不是absolutepower包中的一个成员。如果这样的类被请求装载,你的类装载器将抛出一个安全异常,而不是将这个类的名字传递给双亲类装载器。

  类装载器要知道一个类是否来源于一个被禁止的包,例如absolutepower,只有一个方法,就是通过它的类名来检测。除了屏蔽不同命名空间中的类以及保护被信任的类库的边界外,类装载器还起到另外的安全作用。类装载器必须将每一个被装载的类放置在一个保护域中,一个保护域定义了这个代码在运行时将得到怎样的权限。

class文件检验器

  和类装载器一起,class文件检验器保证装载的class文件内容有正确的内部结构,并且这些class文件相互间协调一致。如果class文件检验器在class文件中发现了问题,它将抛出异常。好的Java编译器不应该产生畸形的class文件,但是Java虚拟机并不知道某个特定的class文件是如何被创建的。因为一个class文件实质上是一个字节序列,所以虚拟机无法分辨特定的class文件是由正常的Java编译器产生的,还是由黑客特制的。所以,所有的Java虚拟机的实现必须有一个class文件检验器,文件检验器可以调用class文件以确保这些定义的类型可以安全地使用。

   Class文件检验器实现的安全目标之一就是程序的健壮性。如果某个有漏洞的编译器,或是某个黑客,产生了一个class文件,而这个class文件中包含了一个方法,这个方法的字节码中含有一条跳转到方法之外的指令,那么一旦这个方法被调用,它将导致虚拟机的崩溃。所以,出于对健壮性的考虑,由虚拟机检验它装载的字节码的完整性是非常重要的。

  Java虚拟机的class文件检验器在字节码执行之前,必须完成大部分检验工作。它只在执行前而不是在执行中对字节码进行一个分析(并检验它的完整性),每一次遇到一个跳转指令时都进行检验。作为字节码确认工作的一部分,虚拟机将确认所有的跳转指令会到达另一条合法的指令,而且这条指令是在这个方法的字节码流中的。在大多数情况下,在执行前就对所有字节码进行一次检查,对于保证健壮性来说就足够了,而不必在它运行时每次都检验每一条字节码指令。

  Class文件检验器要进行四次独立的扫描来完成它的操作。第一次扫描是在类被装载时进行的,在这次扫描中,class文件检验器检查class文件的内部结构,以保证它可以被安全地编译。第二和第三次扫描是在连接过程中进行的,在这两次扫描中,class文件检验器确认类型数据遵从Java编程语言的语义,包括检验它所包含的所有字节码的完整性。第四次扫描是在进行动态连接的过程中解析符号引用时进行的,在这次扫描中,class文件检测器确认被引用的类、字段以及方法确实存在。

第一次:class文件的结构检查

  在第一次扫描中,对每一段将被当作类型导入的字节序列,class文件检验器都会确认它是否符合Java class文件的基本结构。在这次扫描中,检验器将进行许多检查,例如每个class文件必须以四个统一的字节开始:魔数0xCAFEBABE。这个魔数的用处是让class文件分析器很容易分辨出某个文件有明显问题而加以拒绝,这个文件可能是被破坏了的class文件,或者压根就不是class文件。这样class文件检验器所做的第一件事极可能就是检查导入的文件是否是以0xCAFEBABE开头的。检验器还必须确认在class文件中声明的主版本号和此版本号,这个版本号必须在这个Java虚拟机实现可以支持的范围之内。

  而且,在第一次扫描中,class文件检验器必须检验确认这个class文件没有被删节,尾部也没有附带其他的字节。虽然不同的class文件有不同的长度,但是在class文件中包含的每一个组成部分都声明了它的长度和类型。检验器可以使用组成部分的类型和长度来确定整个class文件的正确的总长度。用这种方法,它就可以检查一个转入的文件,其长度是否和它里面的内容相一致。

  第一次扫描的主要目的就是保证这个字节序列正确地定义了一个新类型,它必须遵从Java的class文件的固定格式,这样它才能被编译成在方法区中的内部数据结构。第二、第三、第四次扫描不是在符合class文件格式的二进制数据上进行的,而是在方法区中的,由实现决定的数据结构上进行的。

第二次:类型数据的语义检查

  在第二次扫描中,class文件检验器进行的检查不需要查看字节码,也不需要查看和装载任何其他类型。在这次扫描中,检验器查看每个组成部分,确认它们是否是其所属类型的实例,它们的结构是否正确。例如,方法描述符(它的返回类型,以及参数的类型和个数)在class文件中被存储为一个字符串,这个字符串必须符合特定的上下文无关文法。检验器对每个组成部件中被存储为一个字符串,这个字符串必须符合特定的上下文无关文法。检验器对每个组成部分进行检查的目的之一是为了确认每个方法描述符都是符合特定语法的、格式正确的字符串。

  另外,class文件检验器检查这个类本身是否符合特定的条件,它们是由Java编程语言规定的。例如,检验器强制规定除Object类以外的所有类,都必须有一个超类。在第二次扫描中,检验器还要检查final类没有被子类化,而且final方法没有被覆盖。还要检查常量池中的条目是合法,而且常量池的所有索引必须指向正确类型的常量池条目。也就是说,class文件检验器在运行时检查了一些Java语言应该在编译时遵守的强制规则。因为检验器并不能确定class文件是否是由一个善意的、没有漏洞的编译器产生的,所有它会检查每个class文件,以确保这些规则得到遵守。

第三次:字节码验证

  在class文件检验器成功地进行了两次检查后,它将把注意力放在字节码上,这一次扫描被称为“字节码验证”。在这次扫描中,Java虚拟机对字节流进行数据流分析,这些字节流代表的是类的方法。为了理解字节码检验器,必须对字节码和栈帧有一定的了解。

  字节码流代表了Java的方法,它是由被称为操作码的单字节指令组成的序列,每一个操作码后跟着一个或多个操作数。操作数用于在Java虚拟机执行操作码指令时所需要的额外的数据。执行字节码时,依次执行每个操作码,这就在Java虚拟机内构成了执行的线程。每一个线程被授予自己的Java栈,这个栈是由不同的栈帧构成的。每一个方法调用将获得一个自己的栈帧---栈帧其实就是一个内存片段,其中存储着局部变量和计算的中间结果。在栈帧中,用于存储方法的中间结果的部分称为该方法的操作数栈。操作码和它的(可选的)操作数可能指存储在操作数栈中的数据,或存储在方法栈帧中的局部变量中的数据。这样,在执行一个操作码时,除了可以使用紧随其后的操作数,虚拟机还可以使用操作数栈中的数据,或局部变量中的数据,或是两者都用。

  字节码检验器要进行大量的检查,以确保采用任何路径在字节码流中都得到一个确定的操作码,确保操作数栈总是包含正确的数值以及正确的类型。它必须保证局部变量在赋予合适的值以前不能被访问,而且类的字段中必须总是被赋予正确类型的值,类的方法被调用时总是传递正确数值和类型的参数。字节码检验器还必须保证每一个操作码都是合法的,即每一个操作码都有合法的操作数,以及对每一个操作码,合适类型的数值位于局部变量中或是在操作数栈中。这些仅仅是字节码检验器所做的大量检验工作中的一小部分,在整个检验过程通过后,它就能保证这个字节码流可以被Java虚拟机安全地执行。

  字节码检验器并不试图检测出所有的安全问题。如果这样做的话,它将会遇到“停机问题”。停机问题是计算机科学领域的一个著名论题:既不能写出一个程序,用它来判断作为其输入而读入的某个程序在执行时是否停机。一个程序是否会停机被称为是程序的“不可判定”特性,因为不可能写出一个程序,让它100%地告诉你任何一个给定的程序是否含有这种特性。停机问题的不可判断性可以扩展成计算机程序的许多特性,如一个Java字节码的集合是否能被虚拟机安全地执行。

  字节码检验器处理停机问题的方法是,不去试图精确地让每个安全的程序都通过检查。虽然不能写出一个程序来判定任何给定程序是否会停机,但是可以写出一个简单的程序,让它只是识别出某些一定会停机的程序。例如,如果一个程序的第一条指令就是停机,那么这个程序一定可以停机。如果一个程序内没有循环,它也一定可以停机,如此等等。同样,虽然不可能写出一个能扫描检查所有字节码流是否能够被虚拟机安全执行的检验器,但是可以写出一个能让其中一部分安全的字节码流通过的检验器。Java的字节码检验器就是这么做的。这个检验器检查确认读入的每一个字节码集合是否符合一个特定的规则集合。如果一个字节码集合能够遵从所有这些规则,那么检验器就知道它可以被虚拟机安全地执行。如果不是,那么这些字节码可能可以被虚拟机安全地执行,也可能不能安全地执行。这样,通过识别一些安全的字节码流,但不是全部,检验器就绕过了停机问题。由于字节码检验器强制检查的特性,只要定义好规则,任何程序只要可以用Java编程语言书写,编译器就可以确保编译出来的字节码可以被检验器通过。有些程序虽然不能用Java编程语言源代码表达出来,但仍可以通过检验器的检验。另外还有些程序(也不能用Java源代码表示),它们虽然实际上也能被Java虚拟机安全地执行,却不能通过检验器的检验。

  在第一、第二、第三次扫描中,class文件检验器可以保证导入的class文件构成合理,内在一致,符合Java编程语言的限制条件,并且包含的字节码可以被Java虚拟机安全地执行。如果class文件检验器发现其中任何一点不正确,它将会抛出一个错误,这个class文件将不会被程序使用。

第四次:符号引用的验证

  在动态连接的过程中,如果包含在一个class文件中的符号引用被解析时,class文件检验器将进行第四次检查。在这次检查中,Java虚拟机将追踪那些引用---从被验证的class文件到被引用的class文件,以确保这个引用是正确的。因为第四次扫描必须检查被检测的class文件以外的其他类,所以这次扫描可能需要装载新的类。大多数Java虚拟机的实现采用延迟装载的策略,直到类真正地被程序使用时才装载。即使一个实现确实预先装载了这些类,这是为了加快装载过程的速度,那它还是会表现为延迟加载。例如,如果Java虚拟机在预先装载中发现它不能找到某个特定的被引用类,它并不在当时抛出NoClassFoundError错误,而是直到(或者除非)这个被引用类首次被运行程序使用时才抛出。这样,如果Java虚拟机进行预先连接,第四次扫描可以紧随第三次扫描发生。但是如果Java虚拟机在某个符号引用第一次被使用时才进行解析,那么第四次扫描将在第三次扫描以后很久、当字节码被执行时才进行。

  Class文件检验器的第四次扫描仅仅是动态连接过程的一部分。当一个class文件被装载时,它包含了对其他类的符号引用以及它们的字段和方法。一个符号引用是一个字符串,它给出了名字,并且可能还包含了其他关于被引用项的信息---这些信息必须足以唯一地识别一个类、字段或方法。这样,对于其他类的符号引用必须给出这个类的全名;对于其他类的字段的符号引用必须给出类名、字段名以及字段描述符;对于其他类中的方法的引用必须给出类名、方法名以及方法的描述符。

  动态连接是一个将符号引用解析为直接引用的过程。当Java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用,那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务:

1、查找被引用的类(如果必要的话就装载它)

2、将符号引用替换为直接引用,例如一个指向类、字段或方法的指针或偏移量。

  虚拟机必须记住这个直接引用,这样当它以后再遇到相同的引用时,它就可以立即使用。当这个引用是非法引用时---例如,这个类不能被装载,或这个类的确存在,但是不包含被引用的字段或方法---class文件检验器将抛出一个错误。

  再以Volcano类为例。如果Volcano类中的某个方法调用了名为Lava的类中的某个方法,这个Lava中的方法的全名和描述符将包含在Volcano的class文件的二进制数据中。当Volcano的方法在执行过程中第一次调用Lava的方法时,Java虚拟机必须确认类Lava中存在这个方法,并且这个方法的名字和描述符与Volcano类中期待的相匹配。如果这个符号引用(类名、方法名和描述符)是正确的,那么虚拟机将把它替换为一个直接引用,例如一个指针,从那时开始将使用这个指针。但如果Volcano类中的符号引用不能匹配Lava类中的任何方法时,第四次扫描验证失败,Java虚拟机将抛出一个NoSuchMehodError。

Java虚拟机中内置的安全特性

  Java虚拟机装载了一个类,并且让它进行了第一到第三次的class文件检验,这些字节码就可以被运行了。除了对符号引用的检验(class文件检验的第四次扫描),Java虚拟机在执行字节码时还进行其他一些内置的安全机制的操作。这些机制大多数是Java的类型安全的基础,它们作为Java编程语言保证Java程序健壮性的特性,同样也是Java虚拟机的特性:

1、类型安全的引用转换

2、结构化的内存访问(无指针算法)

3、自动垃圾收集(不必显示地释放被分配的内存)

4、数组边界检查

5、空引用检查

  通过保证一个Java程序只能使用类型安全的、结构化的方法去访问内存,Java虚拟机使得Java程序更为健壮,也使得它们的运行更为安全。如果一个程序破坏内存、崩溃,或者可能导致其他程序崩溃,那么它就是一个安全裂口。

  内置在Java虚拟机中的另一个安全特性---作为内存的结构化访问的一个后备---就是并未指明运行时数据空间在Java虚拟机内部是怎样分布的。运行时数据空间是指一个内存空间,Java虚拟机用这些空间存储运行一个Java程序时所需要的数据:Java栈(每个线程一个)、一个存储字节码的方法区,以及一个垃圾收集堆(它用来存储由运行的程序创建的对象)。如果查看一个class文件的内部,将在找不到任何内存地址。当Java虚拟机装载一个class文件时,由它决定将这些字节码以及其他从class文件中解析得到的数据放置在内存的什么地方。在Java虚拟机规范中,并未说明Java虚拟机是怎样布局它的内存数据的。对于每个Java虚拟机的实现来说,由它的设计者来决定使用什么数据结构来表示运行时数据空间,并且将它们放在内存的哪个位置。

  禁止对内存进行非结构化访问,其实并不是Java虚拟机必须主动强制正在运行的程序这样做,这种禁止其实是字节码指令集本身的内在本质。就像在Java编程语言中,没有办法表达一个非结构化的内存访问,在字节码中也没有办法表达非结构化的内存访问---即使你自己书写字节码。因此,禁止对内存的结构化访问是防止对内存恶意破坏的一种固有阻碍。

  但是,对于由支持Java虚拟机的类型安全机制所建立的安全屏障,还是有办法可以突破的。虽然字节码指令集没有向用户提供不安全、非结构化的内存访问方法,但是可以绕过字节码,即调用本地方法。当调用本地方法时,Java安全沙箱完全不起作用。首先,健壮性的保证对于本地方法并不适用,虽然不能通过Java方法去破坏内存,但是可以通过本地方法达到这个目的。这样,一旦某个线程进入一个本地方法,不管Java虚拟机内置了何种安全策略,只要这个线程运行这个本地方法,安全策略将不再对这个线程使用。因此,安全管理器中包含了一个方法,该方法用来确定一个程序是否能装载动态链接库,因为在调用本地方法时动态连接库是必须的。

  为了保证安全而内置于Java虚拟机的最后一个机制,就是异常的结构化错误处理,因为Java虚拟机支持异常,所以当一些违反安全的行为发生时,它就会做一些结构化处理,Java虚拟机将抛出一个异常或一个错误,而不是崩溃。

安全管理器和Java API

  Java安全模型的前三个组成部分---类装载器体系结构、class文件检验器以及Java中内置的安全特性---一起达到一个共同的目的:保持Java虚拟机的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意或有漏洞的代码侵犯。相反,这个安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。这个安全管理器是一个单独的对象,在运行的Java虚拟机中,它在访问控制---对于外部资源的访问控制---中起到中枢作用。

  安全管理器定义了沙箱的外部外界。因为它是可定制的,所以它允许为程序建立自定义的安全策略。当Java API进行任何可能不安全的操作时,它都会向安全管理器请求许可,从而强制执行自定义的安全策略。要向安全管理器请求许可,Java API将调用安全管理器对象的“check”方法。因为这些方法的名都以“check”开头,所以它们都被称为“check”方法。例如,安全管理器的checkRead()方法决定了线程是否可以读取一个特定的文件。

  因为Java API在进行一个可能不安全的操作前,总是检查安全管理器,所以Java API不会在安全管理器建立的安全策略之下执行被禁止的操作。如果安全管理器禁止这个操作,Java API就不会执行这个操作。

当Java应用程序启动时,它还没有安全管理器,但是应用程序通过将一个指向java.lang.SecurityManager或是其子类的实例传给setSecurityManager(),以此来安装安全管理器,这个动作是可选的。如果应用程序没有安装安全管理器,那么它将不会对请求Java API的任何动作做限制---Java API将做任何被请求的事。