文章目录
- 友好的空指针异常提示
- JAVA打包工具
- JFR事件流
- 简介
- JFR使用
- JMC工具
- JFR事件
- JFR事件流
- 外部存储器API (孵化阶段)
- 非易失性映射字节缓冲区
友好的空指针异常提示
NullpointerException是java开发中经常遇见的问题,在JDK14之前的版本中,空指针异常的提示信息就是简答的null,并不会告诉我们更加有用的信息,根据异常产生的日志来进行查找和处理。
public class Test2 {public static void main(String[] args) {Person p =new Person();p.cat.eat();}
}class Person{public Cat cat;
}
class Cat {public void eat(){}
}
上面的代码在调用eat方法时就会出现空指针异常
这种提示其实并不是很详细,我们可以在运行代码的时候加上一段配置,用以展示比较友好的控制成提示信息
-XX:+ShowCodeDetailsInExceptionMessages
输出的信息如下所示
JAVA打包工具
该特征旨在创建一个用于打包独立java应用程序的工具,JAVA应用的打包和分发一直都是个老大的难题,用户希望JAVA引用的安装和运行方式和其他应用有相似的体验, 比如在windows上只需要双击文件就可以运行。 JAVA平台本身没有提供实用的工具解决这个问题.,通常都依赖第三方的工具完成,这个JEP的目标就是创建一个简单的JAVa打包工具jpackage.。相对于第三方工具,jpackage只适用于比较简单的场景,不过对很多应用来说已经足够好了。
jpackage工具将java的应用程序打包到特定的平台的程序包中,该程序包包含所必须的依赖。该应用程序可以作为普通的jar文件或者模块的集合提供,受支持的特定平台的软件包格式为:
1 Linux deb或者 rpm
2 maxOS: pkg和dmg
3 windows L msi和exe
默认情况下,jpackage以最适合其运行系统的格式生成软件包
如果有一个包含jar文件的应用程序,所有的应用程序都位于一个名为lib 的目录总,并且lib/main.jar包含主类,可以通过如下命令打包
$ jpackage --name myapp -- input lib --main-jar main.jar
将以本地系统的默认格式打包应用程序,将生成的打包文件保留到当前目录中。如果MANIFEST.MF文件中没有main.jar,没有Main-Class属性,则必须显式指定主类
$ jpackage --name myapp --input lib --main-jar main.jar \ --main-class myapp.Main
软件包的名称将为没有app ,尽管软件包文件本身的名称将更长,并以软件包类型皆为,该软件包将包括该应用程序的启动器也称为myapp 。要启动应用程序,启动程序将会从输入目录复制的每个jar文件放在jvm的类路径上。
如果希望默认格式以外的其他格式制作软件包,请使用 --type选项.。例如,要在macOS 上生成pkg文件而不是dmg文件
$ jpackage --name myapp --input lib --main-jar main.jar --type pkg
如果有一个模块化应用程序,该程序有目录中的模块化jar文件或JMOD文件组成,并且模块中lib包含主类myAPP,则命令为
$ jpackage -name myapp --moudule-path lib -m myapp
如果myAPP模块未标识主类,则必须再次明确
$ jpackage -name myapp --moudule-path lib -m myapp/myapp.Main
JFR事件流
简介
Java Flight Recorder(JFR)是JVM的诊断和性能分析工具。JAVA14之前只能做离线的分析,现在可以做实时的持续监视。它可以收集有关JVM以及在其上运行的Java应用程序的数据。JFR是集成到JVM中的,所以JFR对JVM的性能影响非常小,我们可以放心的使用它。
一般来说,在使用默认配置的时候,性能影响要小于1%。JFR是一个基于事件的低开销的分析引擎,具有高性能的后端,可以以二进制格式编写事件,而JMC是一个GUI工具,用于检查JFR创建的数据文件。
这些工具最早是在BEA的JRockit JVM中出现的,最后被移植到了Oracle JDK。最开始JFR是商用版本,但是在JDK11的时候,JFR和JMC完全开源了,这意味着我们在非商用的情况下也可以使用了。
而在今天的JDK 14中,引入了一个新的JFR特性叫做JFR Event Streaming。
JFR使用
JFR是JVM的调优工具,通过不停的收集JVM和java应用程序中的各种事件,从而为后续的JMC分析提供数据。
Event是由三部分组成的:时间戳,事件名和数据。同时JFR也会处理三种类型的Event:持续一段时间的Event,立刻触发的Event和抽样的Event。
为了保证性能的最新影响,在使用JFR的时候,请选择你需要的事件类型。
JFR从JVM中搜集到Event之后,会将其写入一个小的thread-local缓存中,然后刷新到一个全局的内存缓存中,最后将缓存中的数据写到磁盘中去。
或者你可以配置JFR不写到磁盘中去,但是这样缓存中只会保存部分events的信息。这也是为什么会有JDK14 JEP 349的原因。
开启JFR有很多种方式,这里我们关注下面两种:
- 添加命令行参数
-XX:StartFlightRecording:<options>
启动命令行参数的格式如上所述。
JFR可以获取超过一百种不同类型的元数据。如果要我们一个个来指定这些元数据,将会是一个非常大的功能。所以JDK已经为我们提供了两个默认的profile:default.jfc and profile.jfc。
其中 default.jfc 是默认的记录等级,对JVM性能影响不大,适合普通的,大部分应用程序。而profile.jfc包含了更多的细节,对性能影响会更多一些。
如果你不想使用默认的两个jfc文件,也可以按照你自己的需要来创建。
下面看一个更加完整的命令行参数:
-XX:StartFlightRecording:disk=true,filename=/tmp/customer.jfr,maxage=5h,settings=profile
上面的命令会创建一个最大age是5h的profile信息文件。
命令行添加参数还是太麻烦了,如果我们想动态添加JFR,则可以使用jcmd命令。
jcmd <pid> JFR.start name=custProfile settings=default
jcmd <pid> JFR.dump filename=custProfile.jfr
jcmd <pid> JFR.stop
上面的命令在一个运行中的JVM中启动了JFR,并将统计结果dump到了文件中。
上面的custProfile.jfr是一个二进制文件,为了对其进行分析,我们需要和JFR配套的工具JMC。
JMC工具
JDK Mission Control 是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。
在JDK14中,JMC是独立于JDK单独发行的。我们可以下载之后进行安装。
我们先启动一个程序,用于做JFR的测试。
@Slf4j
public class ThreadTest {public static void main(String[] args) {ExecutorService executorService= Executors.newFixedThreadPool(10);Runnable runnable= ()->{while(true){log.info(Thread.currentThread().getName());try {Thread.sleep(500);} catch (InterruptedException e) {log.error(e.getMessage(),e);}}};for(int i=0; i<10; i++){executorService.submit(runnable);}}
}
很简单的一个程序,启动了10个线程,启动这个程序,然后再去看看JMC的界面。
JFR事件
JMC好用是好用,但是要一个一个的去监听JFR文件会很繁琐。接下来介绍一下怎么采用写代码的方式来监听JFR事件。
如果想通过程序来获取“Class Loading Statistics"的信息,可以这样做。
上图的右侧是具体的信息,可以看到主要包含三个字段:开始时间,Loaded Class Count和 Unloaded Class Count。
思路就是使用jdk.jfr.consumer.RecordingFile去读取生成的JFR文件,然后对文件中的数据进行解析。
相应代码如下:
@Slf4j
public class JFREvent {private static Predicate<RecordedEvent> testMaker(String s) {return e -> e.getEventType().getName().startsWith(s);}private static final Map<Predicate<RecordedEvent>,Function<RecordedEvent, Map<String, String>>> mappers =Map.of(testMaker("jdk.ClassLoadingStatistics"),ev -> Map.of("start", ""+ ev.getStartTime(),"Loaded Class Count",""+ ev.getLong("loadedClassCount"),"Unloaded Class Count", ""+ ev.getLong("unloadedClassCount")));@Testpublic void readJFRFile() throws IOException {RecordingFile recordingFile = new RecordingFile(Paths.get("/Users/flydean/flight_recording_1401comflydeaneventstreamThreadTest.jfr"));while (recordingFile.hasMoreEvents()) {var event = recordingFile.readEvent();if (event != null) {var details = convertEvent(event);if (details == null) {// details为空} else {// 打印目标log.info("{}",details);}}}}public Map<String, String> convertEvent(final RecordedEvent e) {for (var ent : mappers.entrySet()) {if (ent.getKey().test(e)) {return ent.getValue().apply(e);}}return null;}
}
注意,在convertEvent方法中,将从文件中读取的Event转换成了map对象。
在构建map时,先判断Event的名字是不是我们所需要的jdk.ClassLoadingStatistics,然后将Event中其他的字段进行转换。最后输出。
运行结果:
{start=2021-04-29T02:18:41.770618136Z, Loaded Class Count=2861, Unloaded Class Count=0}
...
可以看到输出结果和界面上面是一样的。
JFR事件流
上面的JFR事件中,需要去读取JFR文件,进行分析。但是文件是死的,人是活的,每次分析都需要先生成JFR文件简直是太复杂了,是个程序员都不能容忍。
在JFR事件流中,可以监听Event的变化,从而在程序中进行相应的处理。这样不需要生成JFR文件也可以监听事件变化。
public static void main(String[] args) throws IOException, ParseException {//default or profile 两个默认的profiling configuration filesConfiguration config = Configuration.getConfiguration("default");try (var es = new RecordingStream(config)) {es.onEvent("jdk.GarbageCollection", System.out::println);es.onEvent("jdk.CPULoad", System.out::println);es.onEvent("jdk.JVMInformation", System.out::println);es.setMaxAge(Duration.ofSeconds(10));es.start();}}
上面的例子,通过Configuration.getConfiguration(“default”)获取到了默认的default配置。
然后通过构建了default的RecordingStream。通过onEvent方法,对相应的Event进行处理。
外部存储器API (孵化阶段)
通过一个API以允许java程序安全有效的访问JAVA堆之外的外部存储(堆以外的外部存储空间)
目的:JEP 370旨在实现一种提供“通用性”,“安全性”和“确定性”的“外部存储器API”JEP还指出,此外部内存API旨在替代当前使用的方法( java.nio.ByteBuffer和sun.misc.Unsafe )。
许多java的库都能访问外部存储,例如 ignite 、mapDB 、memcached以及netty的ByteBuffer API ,这样可以:
- 避免垃圾回收相关成本和不可预测性
- 跨多个进程共享内存
- 通过将文件映射到内存中来序列化和反序列化内容
但是JAVAAPI本身没有提供一个令人满意的访问外部内存的解决方案
当java程序需要访问堆内存之外的外部存储是,通常有两种方式
- java.nio.ByteBuffer ,:ByteBuffer 允许使用allcateDirect() 方法在堆内存之外分配内存空间
- sum.misc.Unsafe : Unsafe 中的方法可以直接对内存地址进行操作
ByteBuffer有自己的限制,首先是ByteBuffer的大小不能超过2G,其次是内存的释放依靠垃圾回收器,Unsafe的API在使用是不安全的,风险很高,可能会造成JVM崩溃,另外Unsafe本身是不被支持的API,并不推荐。
JEP 370的“描述”部分引入了安全高效的API来访问外部外部内存地址,目前该API还是属于孵化阶段,相关API在jdk.incubator.foreign模块的jdk.incubator.foreign包中,三个API分别是: MemorySegment
, MemoryAddress
和 MemoryLayout
。 MemorySegment
用于对具有给定空间和时间范围的连续内存区域进行建模。 可以将 MemoryAddress
视为段内的偏移量。 最后, MemoryLayout
是内存段内容的程序化描述。
非易失性映射字节缓冲区
JAVA14增加了一种文件映射模式,用于访问非易失性内存,非易失性内存能够持久保持数据,因此可以利用该特性来改进性能的MappedByteBuffer实例,该JEP建议升级MappedByteBuffer以支持对非易失性存储器的访问,唯一需要的API更改是FileChannel客户端,以请求映射位于NVM的支持的文件系统,而不是常规的文件存储系统上的文件,对MappedByteBuffer API最新的更改意味着他支持允许直接内存更新所需要的所有行为,并提供更高级别的JAVA客户端库所需要的持久性保证,以实现持久性的数据类型。
目标
NVM为引用程序程序员提供了在程序运行过程中创建和更新程序转台的机会,而减少了输出到持久性介质或者从持久性介质输入是的成本. 对于事务程序特别重要,在事务程序中,需要定期保持不确定状态以启用崩溃恢复.
现有的C库(例如Intel的libpmen),为c程序员提供了对集成NVM的高效访问,它们还一次基础来支持对各种持久性数据类型的简单管理.当前,由于频繁需要进行系统调用或者JNI来调用原始操作,从而确保内存更改是持久的,因此即使禁用JAVA的基础类库也很昂贵.同样的问题限制了高级库的使用.并且,由于C中提供的持久数据类型分配在无法从JAVA直接访问的内存中这一事实而加剧了这一问题.
该特性试图通过允许映射到ByteBuffer的NVM的有效写回来解决第一个问题.。由于java可以直接访问ByteBuffer映射内存,因此可以通过实现与C语言中提供的客户端库等效的客户端库来解决第二个问题,以管理不同持久数据类型存储。
初步变更
该JEP使用了JAVASE API的两个增强功能
- 支持 Implementation-defined的映射模式
- MappedByteBuffer::force方法指定范围
特定于JDK的API更改
- 通过新模块中的公共API公开新的MApMode枚举值
一个公共扩展枚举ExtendedMapMode将添加到jdk.nio.mapmode程序包
package jdk.nio.mapmode;
public class ExtendedMapMode{private ExtendedMapMode(){}public static final MapMode READ_ONLY_SYNC= ... ...}
在调用FileChannel::map方法创建映射到NVM设备文件上的只读或者写MappedByteBuffer时,可以使用上述的枚举值,如果这些标志在不支持NVM设备文件平台上传递,程序会抛出UnsupportedOperationException异常,在受支持的平台上,及当目标FileChannel实例是通过NVM设备打开的派生文件时才能传递这些参数,在任何情况下都会抛出IOException;