第一章:Java体系结构介绍
1、Java为什么重要?
Java是为网络而设计的,而Java这种适合网络环境的能力又是由其体系结构决定的,可以保证安全健壮和平台无关的程序通过网络传播。
2、网络带来的机遇和挑战
平台无关性、安全性、网络移动性,Java体系的这三个方面共同使得Java和发展中的网络计算环境相得益彰。
3、Java体系结构
Java程序设计语言
Java class文件格式
Java应用编程接口(API)
Java 虚拟机
【这个其实比较重要,对Java的理解不要仅仅局限于它是一种语言!】
4、Java虚拟机
Java面向网络的核心就是Java虚拟机,Java虚拟机就是一台抽象的计算机,其规范规定了每个Java虚拟机都必须实现的特性,但是每个特定的实现都有很多选择。而规范本身也很灵活,允许用纯软件的方式来实现,也可以很大部分由硬件来实现。
Java虚拟机的主要任务是装载class文件并且执行其中的字节码。
Java有两种方法,Java方法和本地方法,Java 方法是有Java语言编写,编译成字节码,存储在class文件中的。本地方法是由其他语言编写的,编译成和处理器相关的机器代码。本地方法保存在动态链 接库中,格式是各个平台专有的。Java方法是与平台无关的,但是本地方法却不是。通过本地方法,Java程序可以直接访问底层操作系统的资源。
Java本地接口(JNI)使本地方法可以在特定主机系统的任何一个Java平台实现上运行。
5、类装载器的体系结构
类装载器的体系结构是Java虚拟机在完全性和网络移动性上发挥重要作用的一个方面。在Java虚拟机中,存在着多个类装载器。Java虚拟机拥有灵活的类装载器体系结构,从而使Java应用程序得以用自定义的方式来实现类的装载。
一个Java应用程序可以使用两种类装载器:启动(BOOTSTRAP)类装载器和用户定义的类装载器。启动类装载器系统唯一,是Java虚拟机实现的一部分。
每一个类被装载的时候,Java虚拟机都要监视这个类,看它到底是被启动类装载器还是被用户定义类装载器装载。当被装载的类引用了另一个类时,虚拟机就会使用装载第一个类的类装载器装载被引用的类。由于Java虚拟机采取这种方式进行类的装载,所以被装载的类默认情况下只能看到被同一个类装载器装载别的类,通过这种方法,Java体系结构运行一个Java应用程序中建立多个命名空间,运行时的Java程序中的每一个类装载器都有他自己的命名空间。
6、Java Class文件
为Java程序提供独立于底层主机平台的二进制形式的服务。当编译和链接一个C++程序的时候,所得的可执行二进制文件只能在指定的硬件平台和操作系统上运行,因为这个二进制文件包含了目标处理器的机器语言。而Java编译器把Java源文件的指令翻译成字节码,这种字节码就是Java虚拟机的“机器语言”。
除了特定处理器的机器语言之外,传统二进制可执行文件的另一个依赖于具体平台的属性石整数的字节顺序。在Java Class文件中字节顺序是高位在前,这与使用何种平台产生这个文件和在何种平台上使用这个文件没有关系。
除了对于平台无关性的支持,Java Class文件还在支持网络移动性的Java体系结构中担当了至关重要的角色。首先,class文件设计的紧凑,可以快速在网络上传送,其次,由于 Java程序师动态链接和动态扩展的,class文件可以在需要的时候才下载。
7、Java API
JavaAPI 通过支持平台无关性和安全性,使得Java适应于网络应用。
运行Java程序时,虚拟机装载程序的class文件所使用的JavaAPI class文件,所有被装载的class文件(包括从应用程序中和从JavaAPI中提取的)和所有的已经装载的动态库(包含本地方法)共同组成了在 Java虚拟机上运行的整个程序。
Java API 的class文件天生就与主机平台密切相关,在一个平台能够支持Java程序以前,必须在这个特定平台上明确地实现API功能。对于Java程序而言,无论平台内部如何,JavaAPI都会有同样的表现和可预测的行为,正是由于在每个特定的主机平台上都明确地实现了Java虚拟机和JavaAPI,因此,Java程序自身就能够具有平台无关性的程序。
JavaAPI在Java安全性模型方面也做出了贡献,当JavaAPI的方法进行任何有潜在危险的操作之前,都会通过查询安全管理器来检验是否得到了授 权。安全管理器是一个为应用程序提供自定义安全策略的特殊对象。通过强制执行安全管理器和访问控制器建立的安全策略,JavaAPI促进了安全环境的建 立,在这种安全环境中,可以运行具有潜在危险的代码。
8、Java程序设计语言
Java是一门面向对象的语言,面向对象的技术的承诺之一就是提高代码的重用率,提高开发者的效率,但是这并不意味着Java比c++更有效率。不过和c++相比,Java还是有一些可以提高开发者效率的十分重要的差别,这种效率的提升主要来自于Java对直接内存操作的约束。
由于Java在运行时强制执行严格的类型规则,根本无法以可能导致内存冲突的方式直接管理内存,因此,Java程序中不会出现那些常使C++程序员降低效率的特定的BUG。
Java避免无意间破坏内存的另一个办法是自动垃圾收集,Java 和c++一样,有一个new操作符,可以通过它来为新对象在堆中分配内存,但是和c++不同的是,Java并没有与new相对应的delete的操作符。 垃圾收集器禁止Java程序员显示指明哪个对象应该被释放,当c++项目中的大小和复杂性逐渐上升时,程序员决定哪个对象应该被释放或者判断一个对象是否 已经被释放的难度也随之增加。如果不在使用的对象没有被释放,会导致内存泄露;多次释放一个对象会导致内存冲突,这两种内存问题都会导致c++程序崩溃。 而c++使用的内存管理方式使得确定问题所在相当困难。Java比c++更有效率的原因在于Java中不用再内存冲突的问题上纠缠不清(开发而非执行上的效率)。而且当不再为显示释放内存担心的时候,效率会更高,程序设计业会更加容易。
Java在运行时保护内存完整性的第三个办法是数组边界检查,在c++中,数组操作实际上就是指针运算,这会带来潜在的内存冲突。Java中绝对不允许数组操作超出边界,从而导致 内存冲突。
最后一个关于Java确保程序健壮性的例子是对对象引用的检查,每次使用引用的时候,Java都会确保这些引用不为空值。
9、Java体系结构的代价
Java程序的执行速度可能比较低,这是Java面向网络特性上所付出的最主要的代价之一。Java面向网络的体系结构所付出的另一个代价是内存管理和线程调度上的缺陷。垃圾收集器可以使得许多程序更加健壮,这也是网络环境中很有价值的安全性保障措施,但是垃圾收集器也给程序运行时性能加入了一些不确定性,你无法确认垃圾收集器什么时候开始收集垃圾,是否开始收集垃圾,也无法确认垃圾收集到底要持续多长时间。
Java为了实现平台无关性,也要付出代价,即最小公分母问题,这是任何尝试提供跨平台功能的API都会出现的固有困难。
当把Java Class文件与Java编程语言之间的紧密联系和Java天生的动态链接性联系到一起的时候,还要付出一些代价。
第二章 平台无关
1、Java体系结构对平台无关性的支持
对平台无关性的支持,是分布在整个Java体系结构中的,所有的组成部分,包括语言、class文件、API及虚拟机,都在对平台无关性的支持方面扮演着重要角色。
Java平台扮演一个运行Java程序与其下的硬件和操作系统间的缓冲角色。Java程序被编译为可运行于Java虚拟机的二进制程序,并且假定 JavaAPI的class文件在运行时都是可用的,接着虚拟机运行程序,那些API则给与程序访问底层计算机的能力,无论Java程序被部署到何处,它 只需要与Java平台交互, 担心底层的硬件和操作系统,因此它能够运行于任何拥有Java平台的计算机。
Java编程语言主要通过基本数据类型的值域和行为都是由语言自己定义的(在C和c++中,基本整数类型中的int的值域是它的占位宽度来决定,而它的占 位宽度则是由目标平台决定,这就意味着针对不同的平台编译的同一个C++程序在运行时可能会有不同的行为,而这仅仅是因为基本数据类型在不同平台上值域的 不同)。通过确保基本数据类型在所有平台上的一致性,Java语言本身为Java程序的平台无关性提供了强有力的支持。
Class文件定义了一个特定于Java虚拟机的二进制格式,Java class文件可以在任何平台上创建,也可以被任何平台的Java虚拟机装入并运行。
Java 的可伸缩性:Java支持平台无关性,一个方面就是它的可伸缩性,Java平台可以在各种各样不同平台的计算机上实现。具体表现在有三个基础的API集合(J2EE,J2SE,J2ME).
2、影响平台无关性的因素
Java程序的平台无关性依赖于多种因素,其中有些因素不在开发人员的控制范围之内,但是大多数是由开发人员控制的(从根本上说,任何Java程序的平台无关程度都依赖于作者怎样编写它)。
1) java平台的部署:决定Java程序其平台无关性的最主要因素就是Java平台在不同的平台上被部署的程度。
2) Java平台的版本,Java平台的版本始终在动态变化中。
3) 本地方法:决定Java程序的平台无关程度的另一个主要因素就是是否调用了本地方法。(当编写一个平台独立的Java程序时,必须遵守的一条最重要的原则就是:不要直接或者间接的调用不属于JavaAPI的本地方法。)
3、对虚拟机的依赖
在编写平台独立的Java程序时,必须遵循两条原则:
1)不要依赖及时终结(finalization)来达到程序的正确性。
2)不要依赖线程的优先级(thread prioritization)来达到程序的可正确性。(为了保证多线程Java程序的平台独立,必须依赖同步Sychronization而不是优先级来在线程之间协调相互间的动作。
这两条原则可以防止Java虚拟机规范中允许的垃圾收集和线程在不同实现中的变化所带来的不利影响。
4、对用户界面的依赖
在不同的Java平台之间,另一个主要的变化就是用户接口。
第三章 安全性
1、为什么需要安全性
因为Java是为网络而生,而网络提供了一条攻入连入网络计算机的潜在途径,因为安全性非常重要。
2、基本沙箱
组成Java沙箱的基本组件如下:
类装载器结构
Class文件检查器
内置于Java虚拟机(及语言)的安全性
安全管理器及JavaAPI。
Java的沙箱安全模型,最重要的优点之一就是这些组件中的类装载器和安全管理器是可以由用户定制的。
3、类装载器体系结构
在Java沙箱中,类装载器体系结构式第一道防线,毕竟是由类装载器将代码装入java虚拟机中的。它起的作用如下:
1) 它防止恶意代码区干涉善意的代码。
2) 它守护了被信任的类库边界。
3) 它将代码归入某类(称为保护域),该类确定了代码可以进行哪些操作。
类装载器体系结构可以防止恶意代码区干涉善意代码,是通过为不同的类装载器装入的类提供不同的命名空间来实现的。命名空间是由一系列唯一的名称组成,每一个被装载的类有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的。命名空间有助于安全的实现,因为你可以有效地在装入了不同的命名空间的类之间设置一个防护罩。在Java虚拟机中,在同一个命名空间内的类可以直接进行交互,而不同的命名空间中的类甚至不能察觉彼此的存在,除非显示的提供了允许它们进行交互的机制。
类装载器体系结构守护了被信任的类库边界,是通过分别使用不同的类装载器装载可靠的包河不可靠的包来实现的。虽然通过赋给成员受保护(或包访问)的访问限制,可以在同一个包中的类型间授予彼此访问的特殊权限,但是这种特殊的权限只能授给在同一个包中的运行时成员,而且它们必须是由同一个类装载器封装的。
用户自定义类装载器经常依赖其他类装载器,至少依赖于虚拟机启动时创建的启动类装载器来帮助它实现一些类装载请求。在版本1.2之前,非启动类装载器必 须显式地求助于其他类装载器,类装载器可以请求另一个用户自定义的类装载器来装载一个类,这个请求是通过对被请求的用户自定义类装载器调用 loadClass()来实现的。除此以外,类装载器也可以通过调用findSystemClass()来请求启动类装载器来装载类,这是 ClassLoader中的一个静态方法。
从版本1.2之后,类装载器请求另一个类装载器来装载类型的过程被形式化,称为双亲委派模式。除启动类装载器之外的每一个类装载器,都一个“双 亲”类装载器,在某一个特定的类装载器试图以常用的方式装载类型以前,它会先默认的将这个任务委派给他的双亲,请求他的双亲来装载这个类型。然后这个双亲 依次请求他自己的双亲来装载这个类型,这个委派的过程一直向上继续,直到达到启动类装载器,通常启动类装载器是委派链中的最后一个雷装载器,如果一个雷装 载器的双亲类装载器有能力来装载这个类型,则这个类型装载器返回这个类型。否则,这个类装载器试图自己来装载这个类型。
在版本1.2之前的大多数虚拟机实现中,内置类装载器(原始类装载器)负责在本地装载可用的class文件。这些class文件通常包括哪些要运行的 Java应用程序的class文件,以及这个应用程序所需的任何类库,这些类库中包含JavaAPI的基本class文件。从版本1.2以后,装载本地可 以class文件的工作被分配到多个类装载器中,刚才称为原始类装载器的内置类装载器被重新命名为启动类装载器,表示它现在只负责装载哪些核心JavaAPI class文件,因为核心JavaAPI的class文件是用于启动Java虚拟机的class文件,所以启动类装载器也因此得名。
从版本1.2之后,由用户自定义类装载器来负责其他class文件的封装。当Java虚拟机开始运行时,在应用程序启动之前,它至少创建一个用户自定义类 装载器,也肯能创建多个。所有这些类装载器被链接在一个双亲-孩子的关系链中,在这条链的顶端是启动类装载器,在这条链的末端是一个被称为“系统类装载器”的类装载器。系统类装载器是指由Java应用程序创建的,新的用户自定义类装载器的默认委派双亲,这个默认的委派双亲通常是一个用户自定义类装载器(被称为用户自定义的类装载器,和启动类装载器相对而言,实际上有Java虚拟机实现提供)。
如何使用类装载器来保护可信任类库?类装载器体系结构是通过剔除装作被信任的不可靠类, 来保护可信任类库的边界的。在有双亲委派模式的情况下,启动类装载器可以抢在标准扩展类装载器之前图装载类,而标准扩展类装载器可以抢在路径类装载器之前 去装载那个类,类路径类装载器又可以抢在网络类装载器之前去装载它。这样,在使用双亲-孩子委派链的方法中,启动类装载器会在最可信的类库—核心 JavaAPI中首先检查每个被装载的类型,然后才依次到标准扩展,类路劲上的本地文件中检查。
运行时包是在Java虚拟机第二版的规范中第一次出现的,它指由同一个类装载器装载的。属于同一个包的,多个类型的集合。在允许两个类型之间对包内可见的成员进行访问前,虚拟机不但要确定这两个类型属于同一个包,还必须确认它们属于同一个运行时包—它们必须是由同一个类装载器装载的。之所以提出运行时包的概念,动机之一是使用不同的类装载器装载不同的类。
类装载器可以用另一种方法来保护被信任的类库的边界,,它只需要通过简单地拒绝装载特定的禁止类型就可以了。
4、class文件检查器
和类装载器一起,class文件检验器保证装载的class文件内容有正确的内部结构,并且这些class文件相互协调一致。如果class文件检验器在class发现了问题,将抛出异样。
Class文件检查器实现的安全目标之一就是程序的健壮性。Java虚拟机的Class文件检验器在字节码执行之前,必须完成大部分检查工作。他只在执行 前而不是在执行中对字节码进行一次分析(并检验它的完整性),每一次遇到一个跳转指令时都进行检验。作为字节码确认工作的一部分,虚拟机将确认所有的跳转 指令会到达另一条合法的指令,而且这条指令是在这个方法的字节流中的。
Class文件检验器要进行四趟独立的扫描来完成他的操作:
1)第一趟扫描,对每一段将被作为类型导入的字节序列,class文件检查 器都会确认它是否符合Java class文件的基本结构。 在这次扫描中,检验器将进行许多检查:以魔数0XCAFEBABE开头?确定class文件中声明的主版本号和次版本号,这 个版本号必须在这个Java虚拟机实现的可支持范围之内。确定这个class文件没有被删节,尾部也没有附带其他字节。
第一趟扫描的主要目的就是保证这个字节序列正确的定义了一个新的类型,它必须遵从Java的class文件固有格式,这样才能被编译成方法中的内部数据结 构。第二、第三和第四趟扫描不是在符合class文件格式的二进制数据上进行的,而是在方法区中的,由实现决定的数据结构上进行的。
2)第二趟:类型数据的语义检查。在第二趟扫描中,class文件检查器进行的检查不需要查看字节码,也不需要查看和装载任何其他类型:在这趟扫描中,检验器查看每个组成部分,确认它们是否是其所属类型的实例,它们的结构是否正确。检验器对每个组成部分进行检查的目的之一是,为了确认每个方法描述都是符合特定语法的、格式正确的字符串。另外,class文件检验器检查这个类本身是否符合特定的条件,它们是由Java编程语言规定的。
3) 第三趟:字节码验证。在class文件检验器成功地进行了两趟检查后,它将注意力放在字节码上,这一趟扫描被称为“字节码验证“。在这趟扫描中,Java虚拟机对字节流进行数据流分析,这些字节流代表的是类的方法。
字节码流代表了Java的方法,它是由被称为操作码的单字节指令组成的序 列,每一个操作码后都跟着一个或多个操作数。操作数用于在Java虚拟机执行操作码指令时提供的额外的数据。执行字节码时,依次执行每个操作码,这就在 Java虚拟机内构成了执行的线程。每一个线程被授予自己的Java栈,这个栈是由不同的栈帧构成的,每一个方法调用将获得一个自己的栈帧,栈帧其实就是一个内存片段,其中存储着局部变量和计算的中间结果。 在栈帧中用于存储方法的中间结果的部分被称为该方法的操作数栈。操作码和它的(可选的)操作数可能指存储在操作数栈中的数据,或存储在方法栈帧中的局部变 量中的数据。这样,在执行一个操作码时,除了可以使用紧随其后的操作数,虚拟机还可以使用操作数栈中的数据,或局部变量中的数据,或者两者都用。
字节码检查器要进行大量的检查,以确保采用任何路径在字节码流中都得到一个确定的操作码,确保操作数栈总是包含正确的数值以及正确的类型。它 必须保证局部变量在赋予合适的值以前都不能被访问,而且类的字段中必须总是被赋予正确类型的值,类的方法被调用时总是传递正确的数值和类型的参数。字节码 还必须保证每个操作码都是合法的,即每一个操作码都有合法的操作数,以及对每一个操作码,合适类型的数值位于局部变量中或是在操作数栈中。这些仅仅是字节 码检验器所做的大量检查工作中的一个小部分,在整个检验过程中,它就能保证这个字节码流可以Java虚拟机安全的执行。
字节码检验器并不试图检测出所有的安全问题。因为会遭遇“停机问题”,停机问题是计算机科学领域的一个著名论题:即不肯能写出一个程序,用它来判断作为其输入而读入的某个程序在执行时是否停机。
字节码检验器处理停机问题的方法是,不去试图精确地让每个安全的程序都通过检查,虽然不能写出一个程序来判定任何给定程序是否会停机,但是可以写出一个简单的程序,让它只是识别出某些一定会停机的程序。Java字节码检验器就是这么做的,这个检验器检查确认读入的每一个字节码集合是否符合一个特定的规则集合。如果一个字节码集合能够遵从所有这些规则,那么检验器就知道它可以被虚拟机安全地执行。这样,通过识别一些安全的字节码流,但不是全部,检验器就绕过了停机问题。
在第一、第二、第三趟扫描中,class文件检验器可以保证导入的class文件构成合理,内在一致,符合Java编程语言的限制条件,并且包含的字节码可以被Java虚拟机安全地执行。
4) 第四趟:符号引用的验证。在动态链接的过程中,如果包含在一个 class文件中的符号引用被解析时,class文件检验器将进行第四趟检查。在这趟检查中,Java虚拟机将追踪那些引用,从被验证的class文件到 被引用的class文件,以确保这个引用是正确的。因为第四趟扫描必须检查被检测的class文件以外的其他类,所以这次扫描可能需要装载新的类。大多数 Java虚拟机的实现采用延迟装载类的策略,直到类真正地被程序使用时才装载。即使一个实现确实需要预先装载了这些类,这是为了加快装载过程的速度,那它还是会表现为延迟装载。
如果Java虚拟机在预先装载中发现它不能找到某个特定的被引用类,它并不在当时抛出NoClassDefFoundError错误,而是直到(或者除 非)这个被引用的类首次被运行程序使用时才抛出。这样,如果Java虚拟机进行预先链接,第四趟扫描可以紧随第三趟扫描发生。但是如果Java虚拟机在某 个符号引用第一次被使用时才进行解析,那么第四趟扫描将在第三趟以后很久,当字节码被执行时才进行。
Class文件检验器的第四趟扫描仅仅是动态链接过程的一部分, 当一个class文件被装载时,它包含了对其他类的符号引用以及它们的字段和方法。一个符号引用是一个字符串,它给出了名字,并且可能还包含了其他关于这 个被引用项的信息,这些信息必须足以惟一地标识一个类,字段或方法。这样,对于其他类的符号引用必须给出这个类的全名,对于其他类的字段符号引用,必须给 出类名、字段名以及字段描述符。对于其他类中的方法的引用必须给出类名、方法名以及方法的描述符。
动态链接是一个将符号引用解析为直接引用的过程。当Java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引 用,那么虚拟机必须解析这个符号引用,在解析时,虚拟机执行两个基本任务:一个是查找被引用的类,另一个是将符号引用替换为直接引用。
5) 二进制兼容
因为Java程序是动态连接的,所以class文件检查器在第四次扫描中,必须检查相互引用的类之间是否兼容。
5、Java虚拟机中内置的安全特性
Java虚拟机在执行字节码时还进行一些内置的安全机制的操作,这些机制大多数十Java类型安全的基础,这些机制如下:
1)类型安全的引用转换
2)结构化的内存访问
3)自动垃圾收集
4)数组边界检查
5)空引用检查
通过保证一个Java程序只能使用类型安全的、结构化的方法去访问内存,Java虚拟机使得Java程序更为健壮,可以阻挠那些黑客,是他们不能为了达到某些目的而破坏Java虚拟机的内在存储,也使得它们运行更为安全。
作为内存结构化访问的一个后备,Java虚拟机并未指明运行时数据空间在Java虚拟机内部是怎么分布的。运行时数据空间是指一些内存空间,Java虚拟机用这些空间来存储运行一个Java程序时所需要的数据:Java栈,一个存储字节码的方法区,以及一个垃圾收集堆(它用来存储由运行的程序创建的对象)。
访问一个class文件内部,将找不到任何内存地址。当Java虚拟机装载一个class文件时,由它决定将这些字节码以及其他从class文件中解析得 到的数据放在内存的什么地方。当Java虚拟机启动一个线程的时,由它决定将为这个线程创建Java栈放到哪里。当它创建一个对象时,也是由它决定将这个 对象放到内存中的什么地方。
禁止对内存进行非结构化的访问,其实并不是Java虚拟机必须主动强制正在运行的程序这样做,这种禁止其实是字节码指令集本身的内在本质(因为在字节码中,没有办法表达非结构化的内存访问,即使你自己书写了字节码)。
但是,对于由支持Java虚拟机的类型安全机制所建立的安全屏障,还是有办法可以突破的,虽然字节码指令集没有向用户提供不安全的,非结构化的内存方法,但是可以绕过字节码,即调用本地方法。在调用本地方法时,Java安 全沙箱完全不起作用。首先健壮性的保证对本地方法并不适用,虽然不能通过Java方法去破坏内存,但是可以通过本地方法可以达到这个目的。最重要的是,本 地方法没有经过JavaAPI(绕过JavaAPI),所以当一个本地方法试图做一些具有破坏性的动作时,安全检查器并未被检查。这样,一旦某个线程进入 一个本地方法,不管Java虚拟机内置了何种安全策略,只要这个线程运行这个本地方法,安全策略将不再对这个线程适用。因此,安全管理器中包含了一个方 法,该方法用来确定一个程序是否能装载动态链接库,因为在调用本地方法时动态链接库是必须的。在调用本地方法前必须确认它是可信任的。
为了保证安全而内置的Java虚拟机的最后一个机制,就是异常的机构化错误处理,因为Java虚拟机支持异常,所以当一些违反安全的行为发生时,它会做一 些结构化的处理,Java虚拟机将抛出一个异常或者一个错误,而不是崩溃,这个异常或者错误将导致这个错误线程的死亡,而不是使整个系统陷入瘫痪。(抛出 一个错误总是导致抛出错误的线程死亡。)
6、安全管理器和JavaAPI
Java安全模型的前三个部分---类装载体系结构,class文件检查器以及Java中内置的安全特性---一起达到一个共同的目的:保持Java虚拟机的实例它正在运行的应用程序内部完整性,使得它们不被下载的恶意或有漏洞的代码侵犯。
Java安全模型的第四个部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或者有漏洞的代码侵犯,这个安全管理器是一个单独的对象,在运行Java的虚拟机中,它在访问控制—对于外部资源的访问控制—中起中枢作用。
安全管理器定义了沙箱的外部边界,是可以定制的,运行为程序建立自定义的安全策略。当JavaAPI进行任何可能不安全的操作时,它都会向安全管理器请求 许可,从而强制执行自定义的安全策略。要向安全管理器请求许可,JavaAPI将调用安全管理器对象的“check”方法。因为在JavaAPI在进行一 个可能不安全的操作前,总是检查安全管理器,所以JavaAPI不会在安全管理器建立的安全策略下执行被禁止的操作。
当一个JavaAPI即将进行一个潜在不安全的动作时,它将遵循以下两个步骤:
首先,JavaAPI的代码检查有没有安装安全管理器,如果没有安装,则跳过第二步直接执行这个潜在不安全操作。
否则,在第二步中,它将调用安全管理器中的合适的“check”方法,如果这个操作被禁止,那么这个”check”方法会抛出一个安全异常,这将导致该JavaAPI方法立即中止,这个潜在不安全的操作将不会被执行。
安全管理器负责两个方面的工作:说明一个安全策略以及执行这个安全策略。
7、代码签名和认证
Java安全模型很重要的一点是它支持认证,认证功能加强了用户的能力,使用户能通过实现一个沙箱来建立多种安全策略,这个沙箱可以依赖于为这个代码提供担保的对象来改变。
要对一段代码作担保或者签名,必须首先生成一个公钥/私钥对,用户保管那把私钥,而把公钥公开。一旦拥有了公钥/私钥对,就必须将要签名的class文件 和其他文件放到一个JAR文件中,然后使用一个工具对整个JAR文件签名,这个签名工具将首先对JAR文件的内容进行单向散列计算,以产生一个散列,然后 这个工具将用私钥对这个散列进行签名,并且将经过签名后的散列加到JAR文件的末尾,这个签名后的散列代表了你对这个JAR文件内容的数字签名。那些持有 你的公钥的人将对JAR文件验证两件事:这个JAR文件确实是你签名的,并且你签名后这个JAR文件没有做过任何改动。
数字签名过程的第一步是一个单向散列计算,它输入大量数据但是产生少量数据,称为散列。
虽然认证技术并没有消除所有和放宽沙箱限制相关的风险,但是他可以减小这些风险。安全性是一种代码和安全之间的折中,安全风险越小,安全的代价就越大。
8、策略
Java安全体系结构的真正好处在于,它可以对代码授予不同层次的信任度来部分地访问系统。Java安全体系结构的主要目标之一就是使建立细粒度的访问拦截策略过程更为简单而且更少出错。
9、保护域
当类装载器将类型装入Java虚拟机时,它们将为每个类型指派一个保护域。
11、访问控制器
类java.security.AccessController提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许,这个访问控制器不能被实例化,它不是一个对象,而是集合在单个类中的多个静态方法。
第五章 Java虚拟机
1、Java虚拟机是什么?
1)抽象规范
2)一个具体的实现
3)一个运行中的虚拟机实例
2、Java虚拟机的生命周期
一个运行时的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。每个Java程序都运行于它自己的Java虚拟机实例中。
在Java虚拟机内部有两种线程:守护线程和非守护线程。守护线程是由虚拟机自己使用的,比如执行垃圾收集任务的线程(但是,Java程序也可以把它创建 的任何线程标记为守护线程)。只要还有任何非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序中所有的非守护线程都终止 时,虚拟机实例将自动退出。
3、Java虚拟机的体系结构
每个Java虚拟机都有一个类装载子系统,它根据给定的权限定名来装入类型。
每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。
当Java虚拟机运行一个程序时,它需要内存来存储许多东西,例如字节码,从已装载的class文件中得到其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等等,Java把这些东西都组织到几个“运行时数据区”中,以便于管理。
方法区:某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有,每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有的线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后它把这些类型放到方法区中,当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。
当每一个新的线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈,如果线程正在执行一个Java方法(非本地方法),那么PC 寄存器的值将总是指示下一条被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态,包括它的局部变量,被调用时传进来的参数,它的返 回值,以及运算的中间结果等等。而本地方法的调用则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存 区中。
1)数据类型:Java虚拟机通过数据类型来执行计算,数据类型及其运算都是由Java虚拟机规范严格定义的,数据类型可以分为两种:基本类型和引用类型。基本类型的变量持有原始值,而引用类型的变量只有引用值。
Java语言中所有的基本类型同样也都是Java虚拟机中的基本类型,但是Boolean有点特别,虽然Java虚拟机也把boolean看做基本类型,但是指令集对boolean只有有限的支持,当编译器把Java编译为字节码时,它会用int或byte来表示boolean.在Java虚拟机中,false是由整数零来表示的,所有非零整数都表示true,涉及boolean值的操作会用int,另外,boolean数组是当做byte数组来访问的。Java虚拟机中还有一个只在内部使用的基本类型:returnAddress,但是程序员不能使用这个类型。
Java虚拟机的引用类型被称为“引用”(reference),有三种引用类型:类类型,接口类型以及数组类型,它们的值都是对动态创建对象的引用。类类型的值是对类实例的引用,数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象,而接口类型的值,则是对实现了该接口的某个类实例的引用。还有一类特殊的引用值是Null,表示该引用变量没有引用任何对象。
2)字长:Java虚拟机中,最基本的数据单元就是字(Word)。Java程序无法侦测到底层虚拟机的字长大小,同样,虚拟机的字长大小也不会影响程序的行为,它仅仅是虚拟机实现的内部属性。
3)类装载子系统:在Java虚拟机中,负责查找并装载类型的那部分被称为类装载器子系统。Java虚拟机有两种类装载器,启动类转载器和用户自定义类装载器,前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。
装载、连接以及初始化类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。以上动作严格按照以下顺序进行:
装载,查找并装载类型的二进制数据。
连接,执行验证,准备,以及解析。验证确保被导入类型的正确性;准备,为类变量分配内存,并将其初始化为默认值;把类型中的符号引用转换为直接引用。
初始化,把类变量初始化为正确初始值。
4) 堆
Java程序在运行时创建的所有类实例或者数组都放在同一个堆中,而一个Java虚拟机实例中只存放一个堆空间,因此所有线程都将共享这个堆。又由于一个Java程序独占一个Java虚拟机实例,因而每个Java程序都有自己的堆空间—它们不会彼此干扰。但是同一个Java程序的多个线程却共享着同一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题。
垃圾收集器的主要工作就是自动回收不再被运行的程序引用的对象所占用的内存,此外,它也可能去移动那些还在使用的对象,以此减少堆碎片。
Java虚拟机规范并没有规定具体的实现必须为Java程序准备多少内存,也没有说它必须怎么管理自己的堆空间,它仅仅告诉实现的设计者:Java程序需要从堆中为对象分配空间,并且程序本身不会主动释放它。
对象的内部表示:Java虚拟机规范并没有规定Java对象在堆中是如何表示的,对象的内部表示也影响着 整个堆以及垃圾收集器的设计,它是由虚拟机的实现者决定。Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成,只要有一个对象引 用,虚拟机就必须能够快速定位对象实例的数据,另外它也必须能够通过该对象引用访问相应的类数据(存储于方法区的类型信息),因此在对象中通常会有一个指 向方法区的指针。
一种可能的堆空间设计是:把堆分为两部分,一个是句柄池,一个是对象池,一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分,一个指向 对象实例变量的指针,一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需要更改一下指针对象的新 地址就可以了。缺点是每次访问对象的实例变量都要经过两次指针传递。
另一种设计方式是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。这样设计的优缺点正好与前面的方法相反,它只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。
不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表,因为方法表加快了调用实例方法时的效率,从而对Java虚拟机实现的整体性能起着非常重要的正面作用。
如下一种方案:每个对象的数据都包含一个指向特殊数据结构的指针,这个数据结构位于方法区,包括两部分:一个指向方法区对应数据的指针和次对象的方法表。
方法表是个指针数组,其中每一项都是一个指向“实例方法数据”的指针,实例方法可以被那类的对象调用,方法表指向的实例方法数据包括以下信息:此方法的操作数栈和局部变量区的大小,此方法的字节码,异常表。
堆上的对象数据中还有一个逻辑部分,那就是对象锁,这是一个互斥对象。虚拟机中的每个对象都有一个对象锁,它被用于协调多个线程访问同一个对 象时的同步。在任何时刻,只能有一个线程拥有这个对象锁,因此只有这个线程才能访问该对象的数据,此时其他希望访问这个对象的线程只能等待,直到拥有对象 锁的线程释放锁。
很多对象在其整个生命周期内部都没有被任何线程加锁,在线程实际请求某个对象锁之前,实现对象锁所需要的数据是不必要的。
除了实现锁所需要的数据外,每个Java对象逻辑上还与实现等待集合(waitset)的数据相关联。锁是用来实现多个线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成一个共同目标而协调工作的。等待集合由等待方法和通知方法联合使用。
最后一种数据类型,可以作为堆中某个对象映像的一部分,是与垃圾收集器有关的数据。垃圾收集器必须跟踪程序引用的每个对象,这个任务不可避免地要附加一些 数据给这些对象,数据的类型要视垃圾收集使用的算法而定。除了标记对象的引用情况外,垃圾收集器还要区分对象是否调用了终结方法。
数组的内部表示,在Java中,数组时真正的对象,和其他对象一样,数组总是存储在堆中,同样和普通对象一样,实现的设计者将决定数组在堆中的表示形式。
数组和其他对象一样,数组也拥有一个与它们的类相关联的Class的实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度(多维数组中每一维的长度)是多少。
数组类的名称由两部分组成:每一维用一个方括号,“[”表示,用字符或字符串表示元素类型。
在堆中的每个数组对象还必须保存的数据是数组的长度、数组数据以及某些指向数组的类数据的引用。虚拟机必须能够通过一个数组对象的引用得到此数组的长度, 通过索引访问其元素(其间检查数组边界是否越界),调用所有数组的直接超类Object声明的方法等等。
6)程序计数器
对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器)寄存器,它是在该线程启动时创建的。PC寄存器的大小事一个字长,因此它既能够持有一个本地指针,也能够持有一个returnAddress
7) Java栈
每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈,以帧为单位保存线程的运行状态。虚拟机只会对直接对Java栈执行两种操作:以帧为单位的压栈或出栈。
某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,它会跟踪当前类和当前常量池,当虚拟机遇到栈内操作指令时,它对当前帧内数据执行操作。
Java方法可以以两种方式完成。一种通过return返回的,称为正常返回,一种是通过抛出异常而异常中止的。不管以哪种方法返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。
Java栈上的所有数据都是此线程私有的,任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用 一个方法时,方法的局部变量保存在调用线程Java栈的帧中,只有一个线程能总是访问那些局部变量,即调用方法的线程。
8) 栈帧
栈帧由三部分组成:局部变量区,操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,它们是按字长计算的。当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。
局部变量区,Java栈帧的局部变量区被组织为一个字长为单位,从0开始计数的数组。字节码指令通过从0开始的索引来使用其中的数据。类型为 int、float、reference和returnAddress的值在数组中只占据一项,而类型byte、short和char的值在存入数组前都 将被传为int值,因而同样占据一项,但是类型为long和double的值在项目中却占据连续的两项(在访问局部变量中的long和double的值的 时候,指令只需要指出连续两项中第一项的索引值)。
局部变量区包含对应方法的参数和局部变量,编译器首先按声明的顺序把这些参数放入局部变量数组。
参数this对于任何一个实例方法都是隐含加入的,它用来表示调用该方法的对象本身。而静态类方法中没有this变量,因为它是一个类方法,类方法只与类 相关,没有与具体的对象相关系。不能通过类方法访问类实例的变量,因为在方法调用时没有关联到一个具体实例。
在Java中,所以的对象都是按引用传递,并且存储在堆中,永远不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象引用。
操作数栈:和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组,但是和前面不同的是,它不是通过索引来访问,而是通过标准的栈操作(压栈和出 栈)来访问的。不同于程序计数器,Java虚拟机没有寄存器,程序计数器也无法被程序指令直接访问,Java虚拟机的指令时从操作数栈中而不是从寄存器中 取得操作数的,因此它的运行方式是基于栈的而不是基于寄存器的。虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压 回操作数栈。
帧数据区:除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池的解析,正常方法返回以及异常派发机制,这些信息都保存在Java栈帧的帧数据区中。
Java虚拟机中的大多数指令都涉及到常量池入口,有些指令仅仅是从常量池中取出数据然后压入Java栈,还有些指令使用常量池的数据来指示要实例化的类 或者数组、要访问的字段或者要调用的方法,还有些指令需要常量池中的数据才能确定某个对象是否属于某个类或实现了某个接口。
每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
帧数据区还帮助虚拟机处理Java方法的正常结束或者异常中止,为了处理Java方法执行期间的异常退出情况,帧数据区还必须保存一个对此方法异常表的引用。
Java栈的可能实现方式,实现的设计者可以任意按自己的想法设计Java栈,一个可能的方式就是从堆中分配每一个帧。
9) 本地方法栈
当线程调用一个本地方法时,就进入了一个全新并且不再受到虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但是不止于此,它还可以做任何它想做的事情。
任何本地方法接口都会使用某种本地方法栈,当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会 保持Java栈不变,不再线程的Java栈中压入新的栈,虚拟机只是简单地动态链接并直接调用指定的本地方法,可以把这看做是虚拟机利用本地方法来动态扩 展自己。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈,或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
10) 执行引擎
任何Java虚拟机实现的核心都是它的执行引擎,在Java虚拟机规范中,执行引擎的行为使用指令集来定义。“执行引擎”这个术语也有三种理 解:一个是抽象的规范,一个是具体的实现,另一个是正在运行的实例,抽象规范使用指令集规定了执行引擎的行为。运行中Java程序的每一个线程都是一个独 立虚拟机执行引擎的实例,从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。Java虚拟机的实现可能用一些对用户程序不可见的线 程,比如垃圾收集器,这样的线程不需要实现的执行引擎实例,所有属于用户运行程序的线程,都是在实际工作的执行引擎。
指令集 方法的字节码流是由Java虚拟机的指令序列构成的,每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码表明需要执行的操作;操作数向 Java虚拟机提供执行操作码需要的额外信息。操作码本身就已经规定了它是否需要跟随操作数,以及如果有操作数的话,它是什么形式的。很多Java虚拟机 的指令不包含操作数,仅仅是由一个操作码字节构成的。
Java虚拟机指令集关注的中心是操作数栈,一般是把将要使用的值会压入栈中,虽然Java虚拟机没有保存任意值的寄存器,但是每个方法都有一个局部变量集合,指令集实际的工作方式就是把局部变量当做寄存器,用索引来访问。