JShell因为已经有很多人在谈论 Jigsaw,因此在第一部分我们先跳过不去讲它。在这一部分我们将会照本宣科地拿 JShell 做些事情, 这是 Java 的一个全新的 REPL (说到它能做的事情,例如你在一个地方敲入了 Java 代码,有了它就可以马上把代码的运行结果计算出来)。如果你还并不(特别地)了解这个东西但又感觉有点兴趣的话,可以看看 Robert Field 在去年的 Devoxx Belgium 上提供的这份不错的介绍。(JEP 222) 新的版本字符串让我们先来个简单的入门介绍: 版本名称。 我尝试过去理解Java的版本命名模式,直到这样做的时候才觉得值得去深究一番。它是从 1.0 和 1.1 版本的 JDK 开始的 – 这俩版本还算是那么回事儿,但这俩版本以后就越来越不那么像话了。版本 1.2 到 1.5 对商标进行了重命名,如 Java 2 这样的,改变比较明显(还记得 J2SE 吗? 其实指的就是 2 这个版本)。到了 JDK 1.5 就很明显可以看出上述的命令模式没有真正起作用,因此Sun就开始将它叫做 Java 5了。围绕 Java 6,整个自 Java 2 开始的命名创意被悄无声息的埋没了,不过这样反而更让人明白——我们简单的叫它“Java X”就可以了。 (你是否知道 Java 版本,包含 Java 7 其实都有一个像 Tiger 和 Mustang 这样很酷的工程名字?) |
JVM 所报告的版本字符串并没有做出修改——它们总会是 1.x.... 这样的形式,不过现在有了 JEP 223, 版本字串和命令模式做了对齐。如果检查相关的系统属性(见这里的demo), 输出会是下面这样的内容:
这并非过分显示的信息,因为它是跑在一个早期可访问的构建本上的。在将来 java.version 会报告像 9.1.2 这样的字符串, 所遵循的是 $MAJOR.$MINOR.$SECURITY 模式:
现在我们已经没必要对这些字符串进行转换了,因为有了 Version , 这是一个能为我们做这些事情的小类,很不错。
|
GNU 风格的命令行选项
|
扩展和更新
|
1
|
money = € / \u20AC |
在 Java 8 中访问这个文件:
1
|
money = â▯¬ / € |
JEP 226 终结了那个时代,不再需要进行 Unicode 转义。Java 9 中同样的代码访问这个文件会得到我们期望的结果:
1
|
money = € / € |
(有一个 完整示例,不过我们的代码高亮不太好用。)
值得注意的是,我们有很多种方法用于访问属性文件,但是只有通过 PropertyResourceBundle 访问的这种方法被更新了。JavaDoc 中的 API 文档说明了如何精确的检测编码以及如何对其进行配置。默认配置是明智的,虽然它只是让 API 在一般情况下 “可以工作”:
1
2
3
4
5
6
7
|
try (InputStream propertyFile = new FileInputStream( "config.properties" )) { PropertyResourceBundle properties = new PropertyResourceBundle(propertyFile); properties.getKeys().asIterator().forEachRemaining(key -> { String value = properties.getString(key); System.out.println(key + " = " + value); });} catch (IOException e) { e.printStackTrace();} |
在 示例 中你可以找到使用了 Properties API 的代码。如果你想在 Java 8 中运行它来进行比较,你会发现 Java 9 的 API 中有一个漂亮的小修改。这个小修改是为仍然在使用古老的 Enumeration 的可怜开发者而做。
在其它 Unicode 相关的新闻里,Java 9 支付了 Unicode 8.0。耶!(JEP 227, JEP 267) 图形图像
|
安全性SHA-3 哈希算法已经实现了 SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。可以通过 MessageDigestAPI 使用它们。(JEP 287) 在使用 SecureRandom(在任何 Java 版本中)时,你可以获取操作系统的原生实现,或者 纯 Java 实现的版本。后者是“旧的基于 SHA-1 的 RNG 实现,它的健壮性不如被认可的 DRBG [Deterministic Random Bit Generator] 机制。”既然是旧的,特别是嵌入式系统,它依赖于 Java 变化,它的安全性随 NIST 800-90Ar1 所描述的 DRBG 机制得到了提升。SecureRandom API 已经被改进,可以传递参数以使用 DRBG 和将来的算法。(JEP 273):
|
新的 Java 虚拟机特性
|
1
|
jar9 --create --file out-mr/mr.jar -C out-mr/java- 8 . \ --release 9 -C out-mr/java- 9 . |
确实,使用 Java 8 运行 java -cp out-mr/mr.jar ...Main 会输出 “Java 8 version” 而 使用 Java 9 运行会输出 “Java 9 version”。
JAR 内部结构看起来像这样:
1
2
3
4
5
6
7
8
9
10
|
└ org └ codefx ... (moar folders) ├ Main. class └ VersionDependent. class └ META-INF └ versions └ 9 └ org └ codefx ... (moar folders) └ VersionDependent. class |
Java 8 或更早的版本会在直接使用 org 下面的类,但新版本会使在 META-INFO/versions 子目录中去找合适的内容来代替默认的。干净利落。
统一的日志
|
1
2
3
4
5
6
7
|
java9 -cp out-mr/mr.jar -Xlog:os,modules,gc ...Main [ 0 .002s][info][os] SafePoint Polling address: 0x00007feea4c96000 [ 0 .002s][info][os] Memory Serialize Page address: 0x00007feea4c94000 [ 0 .002s][info][os] HotSpot is running with glibc 2.22 , NPTL 2.22 [ 0 .009s][info][gc] Using G1 Java 9 version |
咦,难道没有模块信息?让我们把那个标签改为 debug(如果没有指定等级,默认是 info):
1
2
3
4
5
6
7
8
9
|
java9 -cp out-mr/mr.jar -Xlog:os,modules=debug,gc org.codefx.demo.java9.internal.multi_release.Main [ 0 .002s][info][os] SafePoint Polling address: 0x00007f3054a22000 [ 0 .002s][info][os] Memory Serialize Page address: 0x00007f3054a20000 [ 0 .002s][info][os] HotSpot is running with glibc 2.22 , NPTL 2.22 [ 0 .009s][info][gc] Using G1 [ 0 .059s][debug][modules] set_bootloader_unnamed_module(): recording unnamed module for boot loader [ 0 .063s][debug][modules] define_javabase_module(): Definition of module: java.base, version: 9 -ea, location: jrt:/java.base, package #: 159 [... snip ... many, many more module messages ... ] |
太多了,不过我们可以看到这样:
1
|
[ 0 .079s][info][modules,startuptime] Phase2 initialization, 0.0366552 secs |
嘿,这是 info!为什么之前它没有显示出来?!Hey, that’sinfo! Why did it not show up before?! 令人惊异的是它有两个标签,在命令中只匹配其中一个标签是不够的——必须匹配所有标签。我们可以通过 modules+startuptime 或者使用通配符来扩展匹配:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
java9 -cp out-mr/mr.jar -Xlog:os,modules*,gc* ...Main [ 0 .002s][info][os] SafePoint Polling address: 0x00007f9c7f307000 [ 0 .002s][info][os] Memory Serialize Page address: 0x00007f9c7f305000 [ 0 .003s][info][os] HotSpot is running with glibc 2.22 , NPTL 2.22 [ 0 .007s][info][gc,heap] Heap region size: 1M [ 0 .009s][info][gc ] Using G1 [ 0 .009s][info][gc,heap,coops] Heap address: 0x00000006c6200000 , size: 3998 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 [ 0 .077s][info][modules,startuptime] Phase2 initialization, 0.0367418 secs Java 9 version [ 0 .090s][info][gc,heap,exit ] Heap [ 0 .090s][info][gc,heap,exit ] garbage-first heap total 256000K, used 2048K [ 0x00000006c6200000 , 0x00000006c63007d0 , 0x00000007c0000000 ) [ 0 .090s][info][gc,heap,exit ] region size 1024K, 3 young (3072K), 0 survivors (0K) [ 0 .090s][info][gc,heap,exit ] Metaspace used 4225K, capacity 4532K, committed 4864K, reserved 1056768K [ 0 .090s][info][gc,heap,exit ] class space used 414K, capacity 428K, committed 512K, reserved 1048576K |
瞧,即使是垃圾收集器也有信息显示出来——在这个示例中,是关于退出和堆的。
这些都只是表面上的——还有更多选项可用于调整。JEP 很好的解释了它,还包含示例。
事实上,这个改进并不能解决一切问题。因为 JEP 关注于通过它提供基础设施和改变一些(很多?那一定是所有 GC 消息)已经存在的调用,而不是所有事情。虽然我找不到其它使用新机制的日志,但它可能就在那里。
命令行参数验证
|
1
|
java -cp out-mr/mr.jar -XX:BlockLayoutMinDiamondPercentage= 120 ...Main |
我不知道 BlockLayoutMinDiamondPercentage 有什么作用(也懒得查),看起来 120不是一个有效的百分比。但 Java 8 没有收到干扰,愉快地执行了 JAR 包中的代码——显然这并不是程序运行所需要的值。也许 120 就是一个有效的值呢?Java 9 并不这么看:
1
2
3
4
5
6
|
java9 -cp out-mr/mr.jar -XX:BlockLayoutMinDiamondPercentage= 120 ...Main intx BlockLayoutMinDiamondPercentage= 120 is outside the allowed range [ 0 ... 100 ] Improperly specified VM option 'BlockLayoutMinDiamondPercentage=120' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. |
不错 ... JEP 245 为 Java 9 带来了在开始运行时验证所有输入参数的能力。从上面的例子可以看到,它也输出了解释性的错误消息。
保存栈区域假如一个线程运行在栈空间外,它会抛出StackOverflowError错误。在用户代码中,这通常表现应用程序的瘫痪,所以没办法进行干预。但是在库里面,尤其是java核心部分,就算会失败也要尽量保持某些不变量。 比如 ReentrantLock,一个StackOverflowError错误会导致锁状态的错误,会中断锁的状态,使它不能再锁。假如应用程序决定捕获异常,那这个锁会使所有依赖的线程发生死锁。这会很糟糕。 为关键区域设置保留页面JEP 270添加新的注解@ReservedStackAccess。有了它,操作模式会被标记为关键区域,即系统如果处于正常状态,则需要完成运行。JVM的行为由于执行栈而被替代。它保存一些额外栈空间,并且无论何时,一个栈溢出错误发生时,它会在栈的注解方法检查是否有任何@ReservedStackAccess。假如有,会预留一定空闲空间用于执行一些额外工作。它一旦完成,异常就不会再抛出。 这意味着使用注解不会产生大惊喜,异常还是会抛出,并且线程可能会由于失去了它以同样方式瘫痪。只是当区域代码需要完成,并且要确保在预留空间内完成时,这一操作才有意义。 在栈里保存多大空间?直到现在,默认是一个单一存储页,“实验表明,这足以覆盖java.util.concurrent的临界区已注明的关键部分”。 |
示例编写例子会有点难,因为我们很难在托管程序产生堆栈溢出的同时,还能有一部分空间来做些其他的事。在演示项目中,我提供了我认为最佳的解决方案,文中我没有做详细阐述,但我展示了一些以无法控制的形式不断溢出堆栈的代码,其对保留空间无任何作用。
从下往上看,你会发现这是一个无限递归和递增 depth 字段的方法,只是用另外一种方式进行了包装,它能捕获异常并打印 depth 的值。最后,第三个方法请求访问保留堆栈区域。我并没有使用保留的堆栈区域,而是通过递归调用来将它耗尽。这意味着不管调用 determineDepth 还是 determineDepthWithReservedStack,最终都会导致堆栈的溢出,并打印出调用的次数。但输出结果会有所不同,如下:
其输出结果非常不稳定。我猜想这里还有其他机制在工作,而这里我只做了简单演示。 |
||||
内部构造!
|
本文标题:Java 9 内部探索——版本架构,多版本 jar 包及其他
本文地址:https://www.oschina.net/translate/inside-java-9-part-i
参与翻译:边城, leoxu, Tony, 无若, xufuji456, wnull, debugging, Robbie_Zhu