Java(JVM)虚拟机结构基础,和JAR文件包及jar命令详解
前段时间由于研究原来广为传播的String和StringBuffer的性能问题,自己做了几个小实验并得出一些结论,但是从网友的反应来看那个研究并没有起到应有的目的,而且网友也很中肯的提出了自己的意见并对实验中的一些内容指出了其缺陷,针对他们的反应我又反编译了代码来进行对比,但是几位网友仍然不是很信服,而且上次实验的结果和反编译得到的结论并不能完全吻合,因为反编译代码的对比基本上是基于语句的多少,因此这个这个对比也确实不能使人信服,但是这给我的下一步行动指引了方向:研究JVM指令和JVM结构,在对反编译后的代码有完全的理解才能给出可能使人信服的结论。
本文以及以后将要写的一些文章就是我研究JVM规范的一些心得,我希望在和大家共同理解的基础上进行我们下一轮的深入研究。
好,闲话少说,开始我们的正文。
JVM执行的对象就是大家非常熟悉的class文件,我们也称为类文件,JVM规范定义的这个编译完成的代码文件(虽然并非强制要求是实际的文件)的格式非常的详实,但是我们这里只说一些宏观的内容,以后有机会再研究细节的内容吧。JVM要求的类文件的格式是和硬件和操作系统无关的一种二进制格式,它精确定义了类或者接口的表示,它甚至包含了字节顺序这样的细节,而字节顺序在特定平台的目标文件格式中一般都是固定的,不会进行说明。
JVM所支持的数据类型和Java语言规范中定义的几乎一样,请注意是几乎一样!也就是原始类型和引用类型,他们可以被存储在变量表中,也可以作为参数传递、被方法返回,更通常的就是成为操作的对象。为什么和Java语言规范中定义的不完全一样呢?因为JVM中有一种Java语言所没有的原始类型:返回地址类型(returnAddress type)。该类型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作码的指针,并且它的值是不能被运行中的程序所修改的。
另外需要提到的就是布尔类型的值,虽然在Java语言中它是完全独立的值,但是在JVM中只提供了对它的有限支持,表现在:
没有单独的操作布尔类型的指令,源代码中的布尔类型的操作在编译以后是作为int类型的值进行操作的。
JVM直接支持布尔数组,newarray指令可以创建布尔数组,而它的访问和修改操作却是使用byte类型的数组的操作指令进行的:baload,bastore。(在JDK1.0,1,1以及1.2中,布尔数组被编码为byte数组,每个元素是8位)JVM用1代表true,用0代表false,编译器将源代码中的布尔类型映射为JVM中的int类型,而且必须和JVM的要求一致。
另外JVM规范中对于浮点类型的数据有大段的说明,我没有怎么看,主要是讨论JVM的浮点型和IEEE 754的关系的。
关于类型的另外一个需要提一下的是类型检查。JVM期望几乎所有的类型检查已经在运行之前完成了(通常是由编译器进行检查的)而不用JVM自己来检查。原始类型的值不需要被标记或者在运行时被检查以确定他们的类型,同样他们也不用和引用类型的值进行区分,区分工作是由JVM的指令集来完成的,JVM的指令集使用不同指令来区分它要操作的值的类型,例如iadd, ladd, fadd以及dadd是用于将两个数字相加并产生数字类型结果的所有JVM指令,但是每个指令都是针对特定类型的,分别对应int, long, float以及double。
JVM包含对对象的显式支持。类是动态分配的类实例或者是一个数组,JVM中的引用类型就是对一个对象的引用,引用类型的值可以想象为对象的指针,一个对象同时可能存在多个对它的引用,对象总是通过引用被操作、传递或者测试的。
对于引用类型,需要提及的一点就是关于null,它最初是没有运行时类型的,但是它可以被转换为任何类型,而且对于null,JVM并没有要求任何具体的值与之对应。
说完上面这些,我们就开始进入我学习JVM时最想了解的部分了,大家可要打起精神哦。
JVM为运行一个程序定义了几种数据区(Data Area),包括:pc寄存器、JVM堆栈、堆、方法区(Method Area)、运行时常量池(Runtime Constant Pool)以及本机方法堆栈(Native Method Stacks),这些数据区根据其生存期可以分为两种,一种就是和JVM的生存期相同(包括堆和方法区),一种和线程的生存期相同(其它的),和JVM生存期相同的数据区在JVM启动的时候被创建并在JVM退出的时候被销毁,而和线程生存期相同的数据区是每个线程一个的,他们在线程创建的时候被创建,在线程被销毁的时候被销毁。
由于JVM可以同时支持运行多个线程,因此每个线程必然需要各自的PC(program counter)寄存器,无论从什么角度讲,每个JVM线程只能在一个时间只能执行一个方法,该方法也就是线程的当前方法,如果该方法不是本机方法,那么PC寄存器保存的就是当前指令(JVM的指令)的地址,如果是当前方法是本机方法,PC寄存器的值就没有被定义。JVM的PC寄存器的大小足够大,可以容纳一个returnAddress类型或者特定平台的本机指针。
每个JVM线程还拥有一个私有的JVM堆栈,它存储帧(下一篇文章会讲到)。JVM堆栈和像C这样的传统编程语言中的堆栈是类似的,它保存局部变量和部分结果,并且在方法调用和返回中也担任一些职责。因为除了对帧的压入和弹出操作外,对JVM堆栈不能直接进行操作,因此帧可能是在堆上分配的。如果一个线程中计算所需的JVM堆栈大于允许的大小,JVM会抛出StackOverflowError错误,如果JVM堆栈是可以动态伸缩的,如果需要扩展,但是又没有足够的内存可用或者没有足够的内存为一个新线程创建JVM堆栈,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有线程所共享的堆,所有的类实例和数组都是在堆中创建的。堆所存储的对象被一个自动存储管理系统回收(也就是我们所熟知的垃圾收集器(gc))。对象不能被显式的释放,JVM假设没有特定类型的自动存储管理系统,存储管理技术可以根据实现者的系统需求进行选择。如果计算所需的内存堆大于自动存储管理系统可以使用的大小,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有的线程所共享的方法区,方法区类似传统语言的已编译代码的存储区或者UNIX进程的“文本”段。它存储类结构,例如运行时常量池,成员和方法数据以及方法、构造方法的代码(包括用于类和实例的初始化以及接口类型初始化的特定方法(这些特定方法以后会讲到))。虽然从逻辑上讲方法区是堆的一部分,但是JVM的简单实现可以选择不对方法区进行垃圾收集或者压缩(以笔者的理解就是类不能进行卸载)。最新版本(第二版)的JVM规范没有要求方法区的位置或者管理已编译代码的策略。如果方法区的内存不能满足一个分配请求,JVM会抛出OutOfMemoryError。
运行时常量池是类文件中的常量池表的运行时表示,它包含几种常量,范围从编译时就已知的数字常量到运行时必须进行解析的方法和成员引用。运行时常量池扮演的功能类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比典型的符号表更多。
每个运行时常量池时从JVM的方法区中分配的,对于特定方法或者接口的运行时常量池是JVM在创建类或者接口的时候创建的。
当创建一个类或者接口时,如果创建运行时常量池需要的内存比方法区中的可用内容更多的内存,JVM会抛出OutOfMemoryError。
关于常量池创建的更多内容以后可能会更详细的讲解。
JVM的实现可能使用传统的堆栈(更通常的讲就是C栈)以支持本机方法(不是使用JAVA语言编写的方法),本机方法堆栈也可以用于在像C语言这样的语言中为JVM指令集实现解析器,对于不能加载本机方法以及自身不依赖传统堆栈的JVM实现而言,它可以不提供本机方法堆栈,如果提供,本机方法堆栈通常在线程创建的时候为每个线程分配(以笔者的理解应该是需要使用本机方法的线程)。如果线程计算所需的内存比本机方法堆栈所允许的大,JVM会抛出StackOverflowError错误,如果本机方法堆栈可以动态伸缩,而当需要扩展的时候又没有足够的内存时,或者没有足够的内容用于创建一个本机方法堆栈,JVM会抛出OutOfMemoryError。
对于上面的这些数据区,JVM规范允许它们的大小是固定尺寸的,也可以是根据计算的需要动态伸缩的,如果是固定尺寸的,其尺寸可以在创建时自主选择。JVM的实现可以给程序员或者用户提供控制JVM堆栈的初始大小的方法,同样,在动态伸缩的情况下可以控制最大大小和最小大小,并且它们所使用的内存空间可以不是连续的。
下面通过对JVM堆栈的帧的详细介绍了解方法执行的一些内幕。
帧通常用于存储数据和部分结果,同时还用于执行动态链接、返回方法的返回值以及分发异常。
帧在方法调用的时候被创建,在方法完成的时候销毁。它是在创建它的线程的JVM堆栈中分配到空间的,每个帧都有它自己的局部变量数组、操作数堆栈和一个当前方法所在的类的运行时常量池的引用。
它的局部变量数组和操作数堆栈的大小是在编译的时候就确定了的,而且它是和它所联系的方法的代码一起提供的,因此它的数据结构的尺寸仅仅依赖于JVM的实现和方法调用时同时可以分配的内存。
对于正在执行的方法而言只有一个帧是活动的,这个帧就是所谓的当前帧,它的方法就是当前方法,当前方法所在的类被定义为当前类。局部变量和操作数堆栈的操作通常和当前帧有关。
如果一个帧所在的方法调用了另外的方法或者方法结束,那么该帧不再是当前帧。如果是调用另外的方法,那么一个新的帧会被创建并且在控制权转换到新方法时成为当前帧;如果是方法结束,如果有方法返回,当前帧将它的方法调用的结果传递给前一个帧,当前一个帧成为当前帧时当前帧被丢弃。
需要注意的是由一个线程创建的帧是局部于该线程的,其它的线程不能引用它。
每个帧都包含变量数组,也就是我们所熟知的局部变量数组。一个局部变量可以保存一个boolean、 byte、char、short、int、float、引用或者returnAddress值,一对局部变量才能保存一个long或者double值。
局部变量是根据索引进行寻址的,第一个局部变量的索引是0。如果一个整型值介于0和局部变量数组的长度之间并且也只有在这个区间的时候它才会被作为局部变量数组的索引。
long型或者double型的值占用两个连续的局部变量,这样的值可能只能使用较小的那个索引值进行寻址,例如,局部变量数组中索引为n的double变量值实际上占用n和n+1,但是局部变量n+1是不能读取的,它可以被写入,但是这样做会使得局部变量n的内容无效。JVM没有要求n是偶数,这就意味着double和long型值在局部变量数组中不必是64位对齐的,JVM的实现者可以决定使用适当的方式表示那样的值。
JVM使用局部变量传递方法调用的参数,对于类方法调用(也就是static方法),所有的参数都是连续的存储在局部变量表中并且是从0开始的,对于实例方法调用,所有的参数也是连续的但是是从1开始的,局部变量0存储的是实例方法所在的类实例的引用。
每个帧都包含一个后进先出的堆栈,也就是它的操作数堆栈。
操作数堆栈在刚刚被创建的时候是空的,JVM提供指令从局部变量或者成员加载常量或者值到堆栈,其它的JVM指令从操作数堆栈提取操作数,操作它们并将结果放回操作数堆栈。操作数堆栈也用于准备传递给方法的参数以及接收方法的结果。
例如一个iadd指令将两个int值相加,该指令要求它的前一条指令将它要相加的两个值压入操作数堆栈的最上面,它从操作数堆栈取出那两个值进行相加并将结果放回操作数堆栈。
子计算可能是嵌套在操作数堆栈中的,产生的值可以被嵌入的计算使用。
操作数堆栈的每一项都可以保存JVM的任何类型的值,包括long和double型的。
操作数堆栈中的值必须根据其类型进行操作。下面的这些情况都是不可能的:压入两个int值而后续的操作将它们作为long型或者压入两个float值而后续的操作是iadd指令(该指令的操作对象是两个int型)。有一小部分JVM指令(例如dup和swap)将运行时数据区的值作为原始的值(raw value)进行操作而不考虑其类型,这些指令是以一种不能用于修改或者分解单独的值的方式定义的,这些对操作数堆栈操作的限制通过类文件验证进行了强制。
在任何时候操作数堆栈都有其相应的深度,long或者double型的值是两个单位而其它的值是一个单位。
每个帧都包含一个相应于当前方法的类型的运行时常量池的引用以支持方法代码的动态链接。类文件代码中的方法代码指的是被调用的方法以及通过符号引用可以访问的变量,动态链接将这些符号方法引用翻译为具体的方法引用、在必要的时候加载类以解析未定义的符号以及将变量访问翻译为那些变量的运行时位置在存储结构中的适当的偏移。方法和变量的晚期绑定使得方法使用到的其它类的变化可以破坏该代码的可能性更小。
如果方法调用没有导致一个异常(无论是JVM抛出的还是代码显式抛出的)就被认为是方法调用正常结束。如果当前方法调用正常结束,那么一个值可能被返回给调用它的方法。
在这种情况下,当前帧被用于恢复调用者的状态,包括它的局部变量和操作数堆栈以及适当增加程序计数器以跳过方法调用指令。方法调用者所在的帧的程序的执行正常的继续,如果有方法返回,返回值被压入帧的操作数堆栈。
如果方法里面的一个JVM指令的执行引起JVM抛出一个异常并且那个异常在方法里面没有被处理就会导致方法调用突然结束,执行一个athrow指令也可以导致一个异常被显式的抛出并且如果那个异常没有被当前方法捕获也可以导致方法调用突然结束,一个突然结束的方法调用永远也不会向它的调用者返回一个值。
一个帧可能会被像调试信息这样的与实现相关的特定信息扩展。
常常在网上看到有人询问:如何把 java 程序编译成 .exe 文件。通常回答只有两种,一种是制作一个可执行的 JAR 文件包,然后就可以像.chm 文档一样双击运行了;而另一种是使用 JET 来进行 编译但是 JET 是要用钱买的,而且据说 JET 也不是能把所有的 Java 程序都编译成执行文件,性能也要打些折扣。所以,使用制作可执行 JAR 文件包的方法就是最佳选择了,何况它还能保持 Java 的跨平台特性。
下面就来看看什么是 JAR 文件包吧:
1. JAR 文件包
JAR 文件就是 Java Archive File,顾名思意,它的应用是与 Java 息息相关的,是 Java 的一种文档格式。JAR 文件非常类似 ZIP 文件——准确的说,它就是 ZIP 文件,所以叫它文件包。JAR 文件与 ZIP 文件唯一的区别就是在 JAR 文件的内容中,包含了一个 META-INF/MANIFEST.MF 文件,这个文件是在生成 JAR 文件的时候自动创建的。举个例子,如果我们具有如下目录结构的一些文件:
==
`-- test
`-- Test.class
把它压缩成 ZIP 文件 test.zip,则这个 ZIP 文件的内部目录结构为:
test.zip
`-- test
`-- Test.class
如果我们使用 JDK 的 jar 命令把它打成 JAR 文件包 test.jar,则这个 JAR 文件的内部目录结构为:
test.jar
|-- META-INF
| `-- MANIFEST.MF
`-- test
`--Test.class
2. 创建可执行的 JAR 文件包
制作一个可执行的 JAR 文件包来发布你的程序是 JAR 文件包最典型的用法。
Java 程序是由若干个 .class 文件组成的。这些 .class 文件必须根据它们所属的包不同而分级分目录存放;运行前需要把所有用到的包的根目录指定给 CLASSPATH 环境变量或者 java 命令的 -cp 参数;运行时还要到控制台下去使用 java 命令来运行,如果需要直接双击运行必须写 Windows 的批处理文件 (.bat) 或者 Linux 的 Shell 程序。因此,许多人说,Java 是一种方便开发者苦了用户的程序设计语言。
其实不然,如果开发者能够制作一个可执行的 JAR 文件包交给用户,那么用户使用起来就方便了。在 Windows 下安装 JRE (Java Runtime Environment) 的时候,安装文件会将 .jar 文件映射给 javaw.exe 打开。那么,对于一个可执行的 JAR 文件包,用户只需要双击它就可以运行程序了,和阅读 .chm 文档一样方便 (.chm 文档默认是由 hh.exe 打开的)。那么,现在的关键,就是如何来创建这个可执行的 JAR 文件包。
创建可执行的 JAR 文件包,需要使用带 cvfm 参数的 jar 命令,同样以上述 test 目录为例,命令如下:
jar cvfm test.jar manifest.mf test
这里 test.jar 和 manifest.mf 两个文件,分别是对应的参数 f 和 m,其重头戏在 manifest.mf。因为要创建可执行的 JAR 文件包,光靠指定一个 manifest.mf 文件是不够的,因为 MANIFEST 是 JAR 文件包的特征,可执行的 JAR 文件包和不可执行的 JAR 文件包都包含 MANIFEST。关键在于可执行 JAR 文件包的 MANIFEST,其内容包含了 Main-Class 一项。这在 MANIFEST 中书写格式如下:
Main-Class: 可执行主类全名(包含包名)
例如,假设上例中的 Test.class 是属于 test 包的,而且是可执行的类 (定义了 public static void main(String[]) 方法),那么这个 manifest.mf 可以编辑如下:
Main-Class: test.Test <回车>
这个 manifest.mf 可以放在任何位置,也可以是其它的文件名,只需要有 Main-Class: test.Test 一行,且该行以一个回车符结束即可。创建了 manifest.mf 文件之后,我们的目录结构变为:
==
|-- test
| `-- Test.class
`-- manifest.mf
这时候,需要到 test 目录的上级目录中去使用 jar 命令来创建 JAR 文件包。也就是在目录树中使用“==”表示的那个目录中,使用如下命令:
jar cvfm test.jar manifest.mf test
之后在“==”目录中创建了 test.jar,这个 test.jar 就是执行的 JAR 文件包。运行时只需要使用 java -jar test.jar 命令即可。
需要注意的是,创建的 JAR 文件包中需要包含完整的、与 Java 程序的包结构对应的目录结构,就像上例一样。而 Main-Class 指定的类,也必须是完整的、包含包路径的类名,如上例的 test.Test;而且在没有打成 JAR 文件包之前可以使用 java <类名> 来运行这个类,即在上例中 java test.Test 是可以正确运行的 (当然要在 CLASSPATH 正确的情况下)。
3. jar 命令详解
jar 是随 JDK 安装的,在 JDK 安装目录下的 bin 目录中,Windows 下文件名为 jar.exe,Linux 下文件名为 jar。它的运行需要用到 JDK 安装目录下 lib 目录中的 tools.jar 文件。不过我们除了安装 JDK 什么也不需要做,因为 SUN 已经帮我们做好了。我们甚至不需要将 tools.jar 放到 CLASSPATH 中。
使用不带任何的 jar 命令我们可以看到 jar 命令的用法如下:
jar {ctxu}[vfm0M] [jar-文件] [manifest-文件] [-C 目录] 文件名 ...
其中 {ctxu} 是 jar 命令的子命令,每次 jar 命令只能包含 ctxu 中的一个,它们分别表示:
-c 创建新的 JAR 文件包
-t 列出 JAR 文件包的内容列表
-x 展开 JAR 文件包的指定文件或者所有文件
-u 更新已存在的 JAR 文件包 (添加文件到 JAR 文件包中)
[vfm0M] 中的选项可以任选,也可以不选,它们是 jar 命令的选项参数
-v 生成详细报告并打印到标准输出
-f 指定 JAR 文件名,通常这个参数是必须的
-m 指定需要包含的 MANIFEST 清单文件
-0 只存储,不压缩,这样产生的 JAR 文件包会比不用该参数产生的体积大,但速度更快
-M 不产生所有项的清单(MANIFEST〕文件,此参数会忽略 -m 参数
[jar-文件] 即需要生成、查看、更新或者解开的 JAR 文件包,它是 -f 参数的附属参数
[manifest-文件] 即 MANIFEST 清单文件,它是 -m 参数的附属参数
[-C 目录] 表示转到指定目录下去执行这个 jar 命令的操作。它相当于先使用 cd 命令转该目录下再执行不带 -C 参数的 jar 命令,它只能在创建和更新 JAR 文件包的时候可用。
文件名 ... 指定一个文件/目录列表,这些文件/目录就是要添加到 JAR 文件包中的文件/目录。如果指定了目录,那么 jar 命令打包的时候会自动把该目录中的所有文件和子目录打入包中。
下面举一些例子来说明 jar 命令的用法:
1) jar cf test.jar test
该命令没有执行过程的显示,执行结果是在当前目录生成了 test.jar 文件。如果当前目录已经存在 test.jar,那么该文件将被覆盖。
2) jar cvf test.jar test
该命令与上例中的结果相同,但是由于 v 参数的作用,显示出了打包过程,如下:
标明清单(manifest)
增加:test/(读入= 0) (写出= 0)(存储了 0%)
增加:test/Test.class(读入= 7) (写出= 6)(压缩了 14%)
3) jar cvfM test.jar test
该命令与 2) 结果类似,但在生成的 test.jar 中没有包含 META-INF/MANIFEST 文件,打包过程的信息也略有差别:
增加:test/(读入= 0) (写出= 0)(存储了 0%)
增加:test/Test.class(读入= 7) (写出= 6)(压缩了 14%)
4) jar cvfm test.jar manifest.mf test
运行结果与 2) 相似,显示信息也相同,只是生成 JAR 包中的 META-INF/MANIFEST 内容不同,是包含了 manifest.mf 的内容
5) jar tf test.jar
在 test.jar 已经存在的情况下,可以查看 test.jar 中的内容,如对于 2) 和 3) 生成的 test.jar 分别应该此命令,结果如下;
对于 2)
META-INF/
META-INF/MANIFEST.MF
test/
test/Test.class
对于 3)
test/
test/Test.class
6) jar tvf test.jar
除显示 5) 中显示的内容外,还包括包内文件的详细信息,如:
0 Wed Jun 19 15:39:06 GMT 2002 META-INF/
86 Wed Jun 19 15:39:06 GMT 2002 META-INF/MANIFEST.MF
0 Wed Jun 19 15:33:04 GMT 2002 test/
7 Wed Jun 19 15:33:04 GMT 2002 test/Test.class
7) jar xf test.jar
解开 test.jar 到当前目录,不显示任何信息,对于 2) 生成的 test.jar,解开后的目录结构如下:
==
|-- META-INF
| `-- MANIFEST
`-- test
`--Test.class
jar xvf test.jar
运行结果与 7) 相同,对于解压过程有详细信息显示,如:
创建:META-INF/
展开:META-INF/MANIFEST.MF
创建:test/
展开:test/Test.class
9) jar uf test.jar manifest.mf
在 test.jar 中添加了文件 manifest.mf,此使用 jar tf 来查看 test.jar 可以发现 test.jar 中比原来多了一个 manifest。这里顺便提一下,如果使用 -m 参数并指定 manifest.mf 文件,那么 manifest.mf 是作为清单文件 MANIFEST 来使用的,它的内容会被添加到 MANIFEST 中;但是,如果作为一般文件添加到 JAR 文件包中,它跟一般文件无异。
10) jar uvf test.jar manifest.mf
与 9) 结果相同,同时有详细信息显示,如:
增加:manifest.mf(读入= 17) (写出= 19)(压缩了 -11%)
4. 关于 JAR 文件包的一些技巧
1) 使用 unzip 来解压 JAR 文件
在介绍 JAR 文件的时候就已经说过了,JAR 文件实际上就是 ZIP 文件,所以可以使用常见的一些解压 ZIP 文件的工具来解压 JAR 文件,如 Windows 下的 WinZip、WinRAR 等和 Linux 下的 unzip 等。使用 WinZip 和 WinRAR 等来解压是因为它们解压比较直观,方便。而使用 unzip,则是因为它解压时可以使用 -d 参数指定目标目录。
在解压一个 JAR 文件的时候是不能使用 jar 的 -C 参数来指定解压的目标的,因为 -C 参数只在创建或者更新包的时候可用。那么需要将文件解压到某个指定目录下的时候就需要先将这具 JAR 文件拷贝到目标目录下,再进行解压,比较麻烦。如果使用 unzip,就不需要这么麻烦了,只需要指定一个 -d 参数即可。如:
unzip test.jar -d dest/
2) 使用 WinZip 或者 WinRAR 等工具创建 JAR 文件
上面提到 JAR 文件就是包含了 META-INF/MANIFEST 的 ZIP 文件,所以,只需要使用 WinZip、WinRAR 等工具创建所需要 ZIP 压缩包,再往这个 ZIP 压缩包中添加一个包含 MANIFEST 文件的 META-INF 目录即可。对于使用 jar 命令的 -m 参数指定清单文件的情况,只需要将这个 MANIFEST 按需要修改即可。
3) 使用 jar 命令创建 ZIP 文件
有些 Linux 下提供了 unzip 命令,但没有 zip 命令,所以需要可以对 ZIP 文件进行解压,即不能创建 ZIP 文件。如要创建一个 ZIP 文件,使用带 -M 参数的 jar 命令即可,因为 -M 参数表示制作 JAR 包的时候不添加 MANIFEST 清单,那么只需要在指定目标 JAR 文件的地方将 .jar 扩展名改为 .zip 扩展名,创建的就是一个不折不扣的 ZIP 文件了,如将上一节的第 3) 个例子略作改动:
jar cvfM test.zip test
本文以及以后将要写的一些文章就是我研究JVM规范的一些心得,我希望在和大家共同理解的基础上进行我们下一轮的深入研究。
好,闲话少说,开始我们的正文。
JVM执行的对象就是大家非常熟悉的class文件,我们也称为类文件,JVM规范定义的这个编译完成的代码文件(虽然并非强制要求是实际的文件)的格式非常的详实,但是我们这里只说一些宏观的内容,以后有机会再研究细节的内容吧。JVM要求的类文件的格式是和硬件和操作系统无关的一种二进制格式,它精确定义了类或者接口的表示,它甚至包含了字节顺序这样的细节,而字节顺序在特定平台的目标文件格式中一般都是固定的,不会进行说明。
JVM所支持的数据类型和Java语言规范中定义的几乎一样,请注意是几乎一样!也就是原始类型和引用类型,他们可以被存储在变量表中,也可以作为参数传递、被方法返回,更通常的就是成为操作的对象。为什么和Java语言规范中定义的不完全一样呢?因为JVM中有一种Java语言所没有的原始类型:返回地址类型(returnAddress type)。该类型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作码的指针,并且它的值是不能被运行中的程序所修改的。
另外需要提到的就是布尔类型的值,虽然在Java语言中它是完全独立的值,但是在JVM中只提供了对它的有限支持,表现在:
没有单独的操作布尔类型的指令,源代码中的布尔类型的操作在编译以后是作为int类型的值进行操作的。
JVM直接支持布尔数组,newarray指令可以创建布尔数组,而它的访问和修改操作却是使用byte类型的数组的操作指令进行的:baload,bastore。(在JDK1.0,1,1以及1.2中,布尔数组被编码为byte数组,每个元素是8位)JVM用1代表true,用0代表false,编译器将源代码中的布尔类型映射为JVM中的int类型,而且必须和JVM的要求一致。
另外JVM规范中对于浮点类型的数据有大段的说明,我没有怎么看,主要是讨论JVM的浮点型和IEEE 754的关系的。
关于类型的另外一个需要提一下的是类型检查。JVM期望几乎所有的类型检查已经在运行之前完成了(通常是由编译器进行检查的)而不用JVM自己来检查。原始类型的值不需要被标记或者在运行时被检查以确定他们的类型,同样他们也不用和引用类型的值进行区分,区分工作是由JVM的指令集来完成的,JVM的指令集使用不同指令来区分它要操作的值的类型,例如iadd, ladd, fadd以及dadd是用于将两个数字相加并产生数字类型结果的所有JVM指令,但是每个指令都是针对特定类型的,分别对应int, long, float以及double。
JVM包含对对象的显式支持。类是动态分配的类实例或者是一个数组,JVM中的引用类型就是对一个对象的引用,引用类型的值可以想象为对象的指针,一个对象同时可能存在多个对它的引用,对象总是通过引用被操作、传递或者测试的。
对于引用类型,需要提及的一点就是关于null,它最初是没有运行时类型的,但是它可以被转换为任何类型,而且对于null,JVM并没有要求任何具体的值与之对应。
说完上面这些,我们就开始进入我学习JVM时最想了解的部分了,大家可要打起精神哦。
JVM为运行一个程序定义了几种数据区(Data Area),包括:pc寄存器、JVM堆栈、堆、方法区(Method Area)、运行时常量池(Runtime Constant Pool)以及本机方法堆栈(Native Method Stacks),这些数据区根据其生存期可以分为两种,一种就是和JVM的生存期相同(包括堆和方法区),一种和线程的生存期相同(其它的),和JVM生存期相同的数据区在JVM启动的时候被创建并在JVM退出的时候被销毁,而和线程生存期相同的数据区是每个线程一个的,他们在线程创建的时候被创建,在线程被销毁的时候被销毁。
由于JVM可以同时支持运行多个线程,因此每个线程必然需要各自的PC(program counter)寄存器,无论从什么角度讲,每个JVM线程只能在一个时间只能执行一个方法,该方法也就是线程的当前方法,如果该方法不是本机方法,那么PC寄存器保存的就是当前指令(JVM的指令)的地址,如果是当前方法是本机方法,PC寄存器的值就没有被定义。JVM的PC寄存器的大小足够大,可以容纳一个returnAddress类型或者特定平台的本机指针。
每个JVM线程还拥有一个私有的JVM堆栈,它存储帧(下一篇文章会讲到)。JVM堆栈和像C这样的传统编程语言中的堆栈是类似的,它保存局部变量和部分结果,并且在方法调用和返回中也担任一些职责。因为除了对帧的压入和弹出操作外,对JVM堆栈不能直接进行操作,因此帧可能是在堆上分配的。如果一个线程中计算所需的JVM堆栈大于允许的大小,JVM会抛出StackOverflowError错误,如果JVM堆栈是可以动态伸缩的,如果需要扩展,但是又没有足够的内存可用或者没有足够的内存为一个新线程创建JVM堆栈,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有线程所共享的堆,所有的类实例和数组都是在堆中创建的。堆所存储的对象被一个自动存储管理系统回收(也就是我们所熟知的垃圾收集器(gc))。对象不能被显式的释放,JVM假设没有特定类型的自动存储管理系统,存储管理技术可以根据实现者的系统需求进行选择。如果计算所需的内存堆大于自动存储管理系统可以使用的大小,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有的线程所共享的方法区,方法区类似传统语言的已编译代码的存储区或者UNIX进程的“文本”段。它存储类结构,例如运行时常量池,成员和方法数据以及方法、构造方法的代码(包括用于类和实例的初始化以及接口类型初始化的特定方法(这些特定方法以后会讲到))。虽然从逻辑上讲方法区是堆的一部分,但是JVM的简单实现可以选择不对方法区进行垃圾收集或者压缩(以笔者的理解就是类不能进行卸载)。最新版本(第二版)的JVM规范没有要求方法区的位置或者管理已编译代码的策略。如果方法区的内存不能满足一个分配请求,JVM会抛出OutOfMemoryError。
运行时常量池是类文件中的常量池表的运行时表示,它包含几种常量,范围从编译时就已知的数字常量到运行时必须进行解析的方法和成员引用。运行时常量池扮演的功能类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比典型的符号表更多。
每个运行时常量池时从JVM的方法区中分配的,对于特定方法或者接口的运行时常量池是JVM在创建类或者接口的时候创建的。
当创建一个类或者接口时,如果创建运行时常量池需要的内存比方法区中的可用内容更多的内存,JVM会抛出OutOfMemoryError。
关于常量池创建的更多内容以后可能会更详细的讲解。
JVM的实现可能使用传统的堆栈(更通常的讲就是C栈)以支持本机方法(不是使用JAVA语言编写的方法),本机方法堆栈也可以用于在像C语言这样的语言中为JVM指令集实现解析器,对于不能加载本机方法以及自身不依赖传统堆栈的JVM实现而言,它可以不提供本机方法堆栈,如果提供,本机方法堆栈通常在线程创建的时候为每个线程分配(以笔者的理解应该是需要使用本机方法的线程)。如果线程计算所需的内存比本机方法堆栈所允许的大,JVM会抛出StackOverflowError错误,如果本机方法堆栈可以动态伸缩,而当需要扩展的时候又没有足够的内存时,或者没有足够的内容用于创建一个本机方法堆栈,JVM会抛出OutOfMemoryError。
对于上面的这些数据区,JVM规范允许它们的大小是固定尺寸的,也可以是根据计算的需要动态伸缩的,如果是固定尺寸的,其尺寸可以在创建时自主选择。JVM的实现可以给程序员或者用户提供控制JVM堆栈的初始大小的方法,同样,在动态伸缩的情况下可以控制最大大小和最小大小,并且它们所使用的内存空间可以不是连续的。
下面通过对JVM堆栈的帧的详细介绍了解方法执行的一些内幕。
帧通常用于存储数据和部分结果,同时还用于执行动态链接、返回方法的返回值以及分发异常。
帧在方法调用的时候被创建,在方法完成的时候销毁。它是在创建它的线程的JVM堆栈中分配到空间的,每个帧都有它自己的局部变量数组、操作数堆栈和一个当前方法所在的类的运行时常量池的引用。
它的局部变量数组和操作数堆栈的大小是在编译的时候就确定了的,而且它是和它所联系的方法的代码一起提供的,因此它的数据结构的尺寸仅仅依赖于JVM的实现和方法调用时同时可以分配的内存。
对于正在执行的方法而言只有一个帧是活动的,这个帧就是所谓的当前帧,它的方法就是当前方法,当前方法所在的类被定义为当前类。局部变量和操作数堆栈的操作通常和当前帧有关。
如果一个帧所在的方法调用了另外的方法或者方法结束,那么该帧不再是当前帧。如果是调用另外的方法,那么一个新的帧会被创建并且在控制权转换到新方法时成为当前帧;如果是方法结束,如果有方法返回,当前帧将它的方法调用的结果传递给前一个帧,当前一个帧成为当前帧时当前帧被丢弃。
需要注意的是由一个线程创建的帧是局部于该线程的,其它的线程不能引用它。
每个帧都包含变量数组,也就是我们所熟知的局部变量数组。一个局部变量可以保存一个boolean、 byte、char、short、int、float、引用或者returnAddress值,一对局部变量才能保存一个long或者double值。
局部变量是根据索引进行寻址的,第一个局部变量的索引是0。如果一个整型值介于0和局部变量数组的长度之间并且也只有在这个区间的时候它才会被作为局部变量数组的索引。
long型或者double型的值占用两个连续的局部变量,这样的值可能只能使用较小的那个索引值进行寻址,例如,局部变量数组中索引为n的double变量值实际上占用n和n+1,但是局部变量n+1是不能读取的,它可以被写入,但是这样做会使得局部变量n的内容无效。JVM没有要求n是偶数,这就意味着double和long型值在局部变量数组中不必是64位对齐的,JVM的实现者可以决定使用适当的方式表示那样的值。
JVM使用局部变量传递方法调用的参数,对于类方法调用(也就是static方法),所有的参数都是连续的存储在局部变量表中并且是从0开始的,对于实例方法调用,所有的参数也是连续的但是是从1开始的,局部变量0存储的是实例方法所在的类实例的引用。
每个帧都包含一个后进先出的堆栈,也就是它的操作数堆栈。
操作数堆栈在刚刚被创建的时候是空的,JVM提供指令从局部变量或者成员加载常量或者值到堆栈,其它的JVM指令从操作数堆栈提取操作数,操作它们并将结果放回操作数堆栈。操作数堆栈也用于准备传递给方法的参数以及接收方法的结果。
例如一个iadd指令将两个int值相加,该指令要求它的前一条指令将它要相加的两个值压入操作数堆栈的最上面,它从操作数堆栈取出那两个值进行相加并将结果放回操作数堆栈。
子计算可能是嵌套在操作数堆栈中的,产生的值可以被嵌入的计算使用。
操作数堆栈的每一项都可以保存JVM的任何类型的值,包括long和double型的。
操作数堆栈中的值必须根据其类型进行操作。下面的这些情况都是不可能的:压入两个int值而后续的操作将它们作为long型或者压入两个float值而后续的操作是iadd指令(该指令的操作对象是两个int型)。有一小部分JVM指令(例如dup和swap)将运行时数据区的值作为原始的值(raw value)进行操作而不考虑其类型,这些指令是以一种不能用于修改或者分解单独的值的方式定义的,这些对操作数堆栈操作的限制通过类文件验证进行了强制。
在任何时候操作数堆栈都有其相应的深度,long或者double型的值是两个单位而其它的值是一个单位。
每个帧都包含一个相应于当前方法的类型的运行时常量池的引用以支持方法代码的动态链接。类文件代码中的方法代码指的是被调用的方法以及通过符号引用可以访问的变量,动态链接将这些符号方法引用翻译为具体的方法引用、在必要的时候加载类以解析未定义的符号以及将变量访问翻译为那些变量的运行时位置在存储结构中的适当的偏移。方法和变量的晚期绑定使得方法使用到的其它类的变化可以破坏该代码的可能性更小。
如果方法调用没有导致一个异常(无论是JVM抛出的还是代码显式抛出的)就被认为是方法调用正常结束。如果当前方法调用正常结束,那么一个值可能被返回给调用它的方法。
在这种情况下,当前帧被用于恢复调用者的状态,包括它的局部变量和操作数堆栈以及适当增加程序计数器以跳过方法调用指令。方法调用者所在的帧的程序的执行正常的继续,如果有方法返回,返回值被压入帧的操作数堆栈。
如果方法里面的一个JVM指令的执行引起JVM抛出一个异常并且那个异常在方法里面没有被处理就会导致方法调用突然结束,执行一个athrow指令也可以导致一个异常被显式的抛出并且如果那个异常没有被当前方法捕获也可以导致方法调用突然结束,一个突然结束的方法调用永远也不会向它的调用者返回一个值。
一个帧可能会被像调试信息这样的与实现相关的特定信息扩展。
常常在网上看到有人询问:如何把 java 程序编译成 .exe 文件。通常回答只有两种,一种是制作一个可执行的 JAR 文件包,然后就可以像.chm 文档一样双击运行了;而另一种是使用 JET 来进行 编译但是 JET 是要用钱买的,而且据说 JET 也不是能把所有的 Java 程序都编译成执行文件,性能也要打些折扣。所以,使用制作可执行 JAR 文件包的方法就是最佳选择了,何况它还能保持 Java 的跨平台特性。
下面就来看看什么是 JAR 文件包吧:
1. JAR 文件包
JAR 文件就是 Java Archive File,顾名思意,它的应用是与 Java 息息相关的,是 Java 的一种文档格式。JAR 文件非常类似 ZIP 文件——准确的说,它就是 ZIP 文件,所以叫它文件包。JAR 文件与 ZIP 文件唯一的区别就是在 JAR 文件的内容中,包含了一个 META-INF/MANIFEST.MF 文件,这个文件是在生成 JAR 文件的时候自动创建的。举个例子,如果我们具有如下目录结构的一些文件:
==
`-- test
`-- Test.class
把它压缩成 ZIP 文件 test.zip,则这个 ZIP 文件的内部目录结构为:
test.zip
`-- test
`-- Test.class
如果我们使用 JDK 的 jar 命令把它打成 JAR 文件包 test.jar,则这个 JAR 文件的内部目录结构为:
test.jar
|-- META-INF
| `-- MANIFEST.MF
`-- test
`--Test.class
2. 创建可执行的 JAR 文件包
制作一个可执行的 JAR 文件包来发布你的程序是 JAR 文件包最典型的用法。
Java 程序是由若干个 .class 文件组成的。这些 .class 文件必须根据它们所属的包不同而分级分目录存放;运行前需要把所有用到的包的根目录指定给 CLASSPATH 环境变量或者 java 命令的 -cp 参数;运行时还要到控制台下去使用 java 命令来运行,如果需要直接双击运行必须写 Windows 的批处理文件 (.bat) 或者 Linux 的 Shell 程序。因此,许多人说,Java 是一种方便开发者苦了用户的程序设计语言。
其实不然,如果开发者能够制作一个可执行的 JAR 文件包交给用户,那么用户使用起来就方便了。在 Windows 下安装 JRE (Java Runtime Environment) 的时候,安装文件会将 .jar 文件映射给 javaw.exe 打开。那么,对于一个可执行的 JAR 文件包,用户只需要双击它就可以运行程序了,和阅读 .chm 文档一样方便 (.chm 文档默认是由 hh.exe 打开的)。那么,现在的关键,就是如何来创建这个可执行的 JAR 文件包。
创建可执行的 JAR 文件包,需要使用带 cvfm 参数的 jar 命令,同样以上述 test 目录为例,命令如下:
jar cvfm test.jar manifest.mf test
这里 test.jar 和 manifest.mf 两个文件,分别是对应的参数 f 和 m,其重头戏在 manifest.mf。因为要创建可执行的 JAR 文件包,光靠指定一个 manifest.mf 文件是不够的,因为 MANIFEST 是 JAR 文件包的特征,可执行的 JAR 文件包和不可执行的 JAR 文件包都包含 MANIFEST。关键在于可执行 JAR 文件包的 MANIFEST,其内容包含了 Main-Class 一项。这在 MANIFEST 中书写格式如下:
Main-Class: 可执行主类全名(包含包名)
例如,假设上例中的 Test.class 是属于 test 包的,而且是可执行的类 (定义了 public static void main(String[]) 方法),那么这个 manifest.mf 可以编辑如下:
Main-Class: test.Test <回车>
这个 manifest.mf 可以放在任何位置,也可以是其它的文件名,只需要有 Main-Class: test.Test 一行,且该行以一个回车符结束即可。创建了 manifest.mf 文件之后,我们的目录结构变为:
==
|-- test
| `-- Test.class
`-- manifest.mf
这时候,需要到 test 目录的上级目录中去使用 jar 命令来创建 JAR 文件包。也就是在目录树中使用“==”表示的那个目录中,使用如下命令:
jar cvfm test.jar manifest.mf test
之后在“==”目录中创建了 test.jar,这个 test.jar 就是执行的 JAR 文件包。运行时只需要使用 java -jar test.jar 命令即可。
需要注意的是,创建的 JAR 文件包中需要包含完整的、与 Java 程序的包结构对应的目录结构,就像上例一样。而 Main-Class 指定的类,也必须是完整的、包含包路径的类名,如上例的 test.Test;而且在没有打成 JAR 文件包之前可以使用 java <类名> 来运行这个类,即在上例中 java test.Test 是可以正确运行的 (当然要在 CLASSPATH 正确的情况下)。
3. jar 命令详解
jar 是随 JDK 安装的,在 JDK 安装目录下的 bin 目录中,Windows 下文件名为 jar.exe,Linux 下文件名为 jar。它的运行需要用到 JDK 安装目录下 lib 目录中的 tools.jar 文件。不过我们除了安装 JDK 什么也不需要做,因为 SUN 已经帮我们做好了。我们甚至不需要将 tools.jar 放到 CLASSPATH 中。
使用不带任何的 jar 命令我们可以看到 jar 命令的用法如下:
jar {ctxu}[vfm0M] [jar-文件] [manifest-文件] [-C 目录] 文件名 ...
其中 {ctxu} 是 jar 命令的子命令,每次 jar 命令只能包含 ctxu 中的一个,它们分别表示:
-c 创建新的 JAR 文件包
-t 列出 JAR 文件包的内容列表
-x 展开 JAR 文件包的指定文件或者所有文件
-u 更新已存在的 JAR 文件包 (添加文件到 JAR 文件包中)
[vfm0M] 中的选项可以任选,也可以不选,它们是 jar 命令的选项参数
-v 生成详细报告并打印到标准输出
-f 指定 JAR 文件名,通常这个参数是必须的
-m 指定需要包含的 MANIFEST 清单文件
-0 只存储,不压缩,这样产生的 JAR 文件包会比不用该参数产生的体积大,但速度更快
-M 不产生所有项的清单(MANIFEST〕文件,此参数会忽略 -m 参数
[jar-文件] 即需要生成、查看、更新或者解开的 JAR 文件包,它是 -f 参数的附属参数
[manifest-文件] 即 MANIFEST 清单文件,它是 -m 参数的附属参数
[-C 目录] 表示转到指定目录下去执行这个 jar 命令的操作。它相当于先使用 cd 命令转该目录下再执行不带 -C 参数的 jar 命令,它只能在创建和更新 JAR 文件包的时候可用。
文件名 ... 指定一个文件/目录列表,这些文件/目录就是要添加到 JAR 文件包中的文件/目录。如果指定了目录,那么 jar 命令打包的时候会自动把该目录中的所有文件和子目录打入包中。
下面举一些例子来说明 jar 命令的用法:
1) jar cf test.jar test
该命令没有执行过程的显示,执行结果是在当前目录生成了 test.jar 文件。如果当前目录已经存在 test.jar,那么该文件将被覆盖。
2) jar cvf test.jar test
该命令与上例中的结果相同,但是由于 v 参数的作用,显示出了打包过程,如下:
标明清单(manifest)
增加:test/(读入= 0) (写出= 0)(存储了 0%)
增加:test/Test.class(读入= 7) (写出= 6)(压缩了 14%)
3) jar cvfM test.jar test
该命令与 2) 结果类似,但在生成的 test.jar 中没有包含 META-INF/MANIFEST 文件,打包过程的信息也略有差别:
增加:test/(读入= 0) (写出= 0)(存储了 0%)
增加:test/Test.class(读入= 7) (写出= 6)(压缩了 14%)
4) jar cvfm test.jar manifest.mf test
运行结果与 2) 相似,显示信息也相同,只是生成 JAR 包中的 META-INF/MANIFEST 内容不同,是包含了 manifest.mf 的内容
5) jar tf test.jar
在 test.jar 已经存在的情况下,可以查看 test.jar 中的内容,如对于 2) 和 3) 生成的 test.jar 分别应该此命令,结果如下;
对于 2)
META-INF/
META-INF/MANIFEST.MF
test/
test/Test.class
对于 3)
test/
test/Test.class
6) jar tvf test.jar
除显示 5) 中显示的内容外,还包括包内文件的详细信息,如:
0 Wed Jun 19 15:39:06 GMT 2002 META-INF/
86 Wed Jun 19 15:39:06 GMT 2002 META-INF/MANIFEST.MF
0 Wed Jun 19 15:33:04 GMT 2002 test/
7 Wed Jun 19 15:33:04 GMT 2002 test/Test.class
7) jar xf test.jar
解开 test.jar 到当前目录,不显示任何信息,对于 2) 生成的 test.jar,解开后的目录结构如下:
==
|-- META-INF
| `-- MANIFEST
`-- test
`--Test.class
jar xvf test.jar
运行结果与 7) 相同,对于解压过程有详细信息显示,如:
创建:META-INF/
展开:META-INF/MANIFEST.MF
创建:test/
展开:test/Test.class
9) jar uf test.jar manifest.mf
在 test.jar 中添加了文件 manifest.mf,此使用 jar tf 来查看 test.jar 可以发现 test.jar 中比原来多了一个 manifest。这里顺便提一下,如果使用 -m 参数并指定 manifest.mf 文件,那么 manifest.mf 是作为清单文件 MANIFEST 来使用的,它的内容会被添加到 MANIFEST 中;但是,如果作为一般文件添加到 JAR 文件包中,它跟一般文件无异。
10) jar uvf test.jar manifest.mf
与 9) 结果相同,同时有详细信息显示,如:
增加:manifest.mf(读入= 17) (写出= 19)(压缩了 -11%)
4. 关于 JAR 文件包的一些技巧
1) 使用 unzip 来解压 JAR 文件
在介绍 JAR 文件的时候就已经说过了,JAR 文件实际上就是 ZIP 文件,所以可以使用常见的一些解压 ZIP 文件的工具来解压 JAR 文件,如 Windows 下的 WinZip、WinRAR 等和 Linux 下的 unzip 等。使用 WinZip 和 WinRAR 等来解压是因为它们解压比较直观,方便。而使用 unzip,则是因为它解压时可以使用 -d 参数指定目标目录。
在解压一个 JAR 文件的时候是不能使用 jar 的 -C 参数来指定解压的目标的,因为 -C 参数只在创建或者更新包的时候可用。那么需要将文件解压到某个指定目录下的时候就需要先将这具 JAR 文件拷贝到目标目录下,再进行解压,比较麻烦。如果使用 unzip,就不需要这么麻烦了,只需要指定一个 -d 参数即可。如:
unzip test.jar -d dest/
2) 使用 WinZip 或者 WinRAR 等工具创建 JAR 文件
上面提到 JAR 文件就是包含了 META-INF/MANIFEST 的 ZIP 文件,所以,只需要使用 WinZip、WinRAR 等工具创建所需要 ZIP 压缩包,再往这个 ZIP 压缩包中添加一个包含 MANIFEST 文件的 META-INF 目录即可。对于使用 jar 命令的 -m 参数指定清单文件的情况,只需要将这个 MANIFEST 按需要修改即可。
3) 使用 jar 命令创建 ZIP 文件
有些 Linux 下提供了 unzip 命令,但没有 zip 命令,所以需要可以对 ZIP 文件进行解压,即不能创建 ZIP 文件。如要创建一个 ZIP 文件,使用带 -M 参数的 jar 命令即可,因为 -M 参数表示制作 JAR 包的时候不添加 MANIFEST 清单,那么只需要在指定目标 JAR 文件的地方将 .jar 扩展名改为 .zip 扩展名,创建的就是一个不折不扣的 ZIP 文件了,如将上一节的第 3) 个例子略作改动:
jar cvfM test.zip test