本文会介绍Java在集合、网络与监控方面的特性更新,包括常量集合构造方法、有序集合接口、httpclient、事件流监控等。文章中代码较多,适合在PC上查看,源码可查看原文链接。
6.1 集合的快速创建方法
不知道你有没有这样的困扰,我偶尔会在创建一个常量小集合时纠结一阵,比如新建一个包含 "a" "b" "c" 三个元素的Set,或者新建一个包含"a" -> 1, "b" -> 2,"c" -> 3的Map,然后在网上查半天,最后大概率还是老老实实地写好几行代码一条一条地add进去。
例如创建一个Set,一般来说有这几种方式:
// 方法一,代码太长
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set = Collections.unmodifiableSet(set);
// 方法二,语法过于啰嗦
Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c"));
// 方法三,我从来都没记住过这个语法
Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
add("a"); add("b"); add("c");
}});
// 方法四,Stream可读性好了很多,但有额外开销
Set<String> set = Stream.of("a", "b", "c").collect(toSet());
为了更方便地创建集合,集合类List、Set、Map增添了新的创建方法,语法上更加非常直观,List和Set可以直接这样创建:
List.of("a", "b", "c");
Set.of("a", "b", "c");
值得一提的是,如果你点到of()方法的源码,会发现这个方法是这样声明的:
static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
...
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)
static <E> List<E> of(E... elements)
参数在10个以内时都有独立的方法声明,这是为了避免可变参数调用所产生的数组分配、初始化和垃圾回收的开销,这种性能优化思路是值得我们借鉴的。
Map的创建方法是这样的:
Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...
// 创建包含"a" -> 1, "b" -> 2,"c" -> 3的Map
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);
// 大于10个键值对可用的创建方法
Map.ofEntries(
entry(k1, v1),
entry(k2, v2),
entry(k3, v3),
// ...
entry(kn, vn));
使用新的集合创建方法生成的集合,是不可变集合,并且是线程安全的。新的集合创建方法十分简明有效,在很多java后续的新特性的实现中也得到了广泛的应用,相信你也一定会从中受益。
6.2 有序集合接口( Sequenced Collections )
"Life can only be understood backwards; but it must be lived forwards."
—— Kierkegaard
有序集合接口要解决的问题是,java现有的集合类型的接口,对于是否有序的定义是缺失的,并且有序相关操作在各个集合接口和类型中的定义也不统一。
具体来说,例如List 和 Deque 都被定义为有序,但它们的共同超类型是 Collection,并没有被定义为有序的集合。同样,Set 并未定义为有序的集合,子类型如 HashSet 也未定义,但子类型如 SortedSet 和 LinkedHashSet 被定义了。与有序相关的操作定义都分散在类型层次结构中,使得在 API 中表达某些语义变得困难。
另一方面,视图集合(view collections)经常被迫降级到更弱的语义上。例如将 LinkedHashSet 包装到 Collections::unmodifiableSet 中,返回值类型是 Set,从而丢失了集合原本有序的信息。
由于缺少定义有序集合的接口,与有序相关的操作要么不一致,要么缺失。例如虽然许多集合类型都实现了获取第一个或最后一个元素的操作,但每个集合的定义都不完全一样,有的操作是完全缺失的:
| First element | Last element | |
|---|---|---|
| List | list.get(0) | list.get(list.size() - 1) |
| Deque | deque.getFirst() | deque.getLast() |
| SortedSet | sortedSet.first() | sortedSet.last() |
| LinkedHashSet | linkedHashSet.iterator().next() | // missing |
对有序的集合来说,正序遍历比较直观,但反向遍历很困难,并且也不是所有集合都能支持。几个可以实现反向遍历的类型包括NavigableSet、Deque 和List ,其他有序的集合类型想要反向遍历,只能通过复制到另一个集合的方式实现。几种可实现反向遍历的集合的代码如下:
for (var e : navSet.descendingSet())
process(e);
for (var it = deque.descendingIterator(); it.hasNext();) {
var e = it.next();
process(e);
}
for (var it = list.listIterator(list.size()); it.hasPrevious();) {
var e = it.previous();
process(e);
}
类似地,使用流来处理集合元素是个常用方法,但是反向获取流可能很困难。在定义为有序的各种集合中,唯一方便支持此操作的是NavigableSet:
navSet.descendingSet().stream()
- SequencedCollection
SequencedCollection是一个新增的集合接口,是指其内部元素有序的集合,这个接口提供了获取和修改第一个和最后一个元素的方法,以及返回反向排序视图的方法,定义如下:
interface SequencedCollection<E> extends Collection<E> {
// new method
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
其中reversed()接口返回原始集合的反向排序视图,原始视图的任何修改都在视图中可见,在视图被允许修改的情况下,视图的变更也会直接写入到原始集合中。反向排序视图支持所有的遍历操作,包括iterator、stream等。例如,原本获取LinkedHashSet的反向遍历stream非常困难,现在可以写为:
linkedHashSet.reversed().stream()
SequencedSet
SequencedSet继承SequencedCollection,定义如下:
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed(); // covariant override
}
通过比较来排序的Set,例如SortedSet,不支持在SequencedCollection接口中声明的addFirst(E)和addLast(E)方法,因此这些方法可能会抛出UnsupportedOperationException异常。
SequencedSet的addFirst(E)和addLast(E)方法针对LinkedHashSet等集合具有特殊的语义:如果元素已经存在于集合中,则将其移动到适当的位置,这也解决了LinkedHashSet长期以来无法重新定位元素的缺陷。
SequencedMap
SequencedMap继承Map,定义如下:
interface SequencedMap<K,V> extends Map<K,V> {
// new methods
SequencedMap<K,V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
V putFirst(K, V);
V putLast(K, V);
// methods promoted from NavigableMap
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
}
新的 put*(K,V) 方法具有类似于SequencedSet中相应的 add*(E) 方法的特殊语义:对于LinkedHashMap等Map类型,如果该条目已经存在于映射中,则具有重新定位条目的额外效果。对于SortedMap等Map类型,这些方法会抛出UnsupportedOperationException异常。
集合类型重构
新增三种有序集合接口后,整体类图依赖更新如下:
原本一些有序集合的class,实现接口改为了有序集合接口。同时,Collections 也增加了三个不可变集合的封装方法:
Collections.unmodifiableSequencedCollection(sequencedCollection)
Collections.unmodifiableSequencedSet(sequencedSet)
Collections.unmodifiableSequencedMap(sequencedMap)
总的来说,有序集合接口对现有java集合类型进行了较大的调整,对集合类型顺序访问这一最常用的操作进行了标准化和优化,相信你会从新接口中获得很大的收益。
6.3 HTTP Client
说到网络编程,我们最常用的就是基于http协议的编程,但有趣的是,我们几乎从来不用jdk提供的httpclient API,原因是这个API不但使用方式复杂、难以维护,而且不支持异步请求。java11正式发布了新的http client API,以下是使用同步发GET请求的示例代码:
public class HttpClientSynchronous {
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://www.bxyuer.com/"))
.setHeader("User-Agent", "Java 11 HttpClient Bot") // add request header
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
HttpHeaders headers = response.headers();
headers.map().forEach((k, v) -> System.out.println(k + ":" + v));
System.out.println(response.statusCode());
System.out.println(response.body());
}
}
通过替换 GET 为 POST 即可发POST请求。再来看一个异步请求的例子,通过 sendAsync 接口异步请求:
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://www.bxyuer.com/"))
.setHeader("User-Agent", "Java 11 HttpClient Bot")
.build();
CompletableFuture<HttpResponse<String>> response =
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
String result = response.thenApply(HttpResponse::body).get(5, TimeUnit.SECONDS);
System.out.println(result);
异步请求还可以设置executor,例如设置虚拟线程:
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.executor(Executors.newVirtualThreadPerTaskExecutor())
.connectTimeout(Duration.ofSeconds(10))
.build();
也可以设置身份校验信息:
private static final HttpClient httpClient = HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user", "password".toCharArray());
}
})
.connectTimeout(Duration.ofSeconds(10))
.build();
可以看到,新的http client API无论在可读性还是功能丰富度上都比原来的API有了很大进步,相比于Apache HttpClient \ OkHttpClient \ Spring WebClient 这几个常用的httpclient可以说至少也算新增了一个解决方案。如果代码中没有强依赖于某个框架,也不需要过多的定制化,并且也不想引入过多依赖时,使用JDK自带的http client我认为是最佳选择。
6.4 静态HTTP文件服务器( Simple Web Server )
命令行启动
Simple Web Server是一个开箱即用的web服务器,可以直接返回本地目录下的静态文件,可以直接通过命令行启动:
$ jwebserver
如果command not found: jwebserver,可以使用命令java -m jdk.httpserver启动成功后,jwebserver会向System.out打印一条消息,列出正在提供服务的本地地址和绝对路径:
$ java -m jdk.httpserver
默认情况下绑定到环回。如果要表示所有接口,请使用 "-b 0.0.0.0" 或 "-b ::"。
为 127.0.0.1 端口 8000 上的 /Users/liuliming/git-repo/health-report/src 及子目录提供服务
URL http://127.0.0.1:8000/
可选参数可以通过 -h来查看,常用参数如 -p可以指定端口号,-d可以指定本地目录的目标路径等。访问URL,即可看到文件服务器:
API 的使用
如果我们想和代码集成,快速搭建一个简单的静态文件服务,可以使用Simple Web Service的API来实现。新的类包括SimpleFileServer、HttpHandlers和Request,每个类都基于com.sun.net.httpserver包中的现有类和接口构建:HttpServer、HttpHandler、Filter和HttpExchange。SimpleFileServer类支持创建文件服务器、文件服务器处理程序和输出过滤器,代码如下:
package com.sun.net.httpserver;
public final class SimpleFileServer {
public static HttpServer createFileServer(InetSocketAddress addr,
Path rootDirectory,
OutputLevel outputLevel) {...}
public static HttpHandler createFileHandler(Path rootDirectory) {...}
public static Filter createOutputFilter(OutputStream out,
OutputLevel outputLevel) {...}
...
}
利用API通过几行简单的代码,我们就可以启动服务:
var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
Path.of("/some/path"), OutputLevel.VERBOSE);
server.start();
也可以把静态文件handler加入到现有服务中:
var server = HttpServer.create(new InetSocketAddress(8080),
10, "/store/", new SomePutHandler());
var handler = SimpleFileServer.createFileHandler(Path.of("/some/path"));
server.createContext("/browse/", handler);
server.start();
- 请求处理增强
为了支持扩展请求处理能力,JDK引入了一个新的HttpHandlers类,其中包括两种静态方法用于自定义HttpHandler,以及Filter类中用于适配请求的新方法:
package com.sun.net.httpserver;
public final class HttpHandlers {
public static HttpHandler handleOrElse(Predicate<Request> handlerTest,
HttpHandler handler,
HttpHandler fallbackHandler) {...}
public static HttpHandler of(int statusCode, Headers headers, String body) {...}
{...}
}
public abstract class Filter {
public static Filter adaptRequest(String description,
UnaryOperator<Request> requestOperator) {...}
{...}
}
handleOrElse方法可以指定条件设置Handler,of工厂方法可以创建具有预设响应状态的Handler。从adaptRequest获取的预处理Filter可用于检查和适配处理请求之前的某些属性。这里用到了JDK引入的一个新的代表HTTP请求的Request接口,只提供有限的请求状态视图:
public interface Request {
URI getRequestURI();
String getRequestMethod();
Headers getRequestHeaders();
default Request with(String headerName, List<String> headerValues)
{...}
}
我们可以使用这些请求处理增强API,快速定制化一个web服务:
// PUT请求使用SomePutHandler
var h = HttpHandlers.handleOrElse(r -> r.getRequestMethod().equals("PUT"),
new SomePutHandler(), new SomeHandler());
// 向请求header添加Foo:Bar
var f = Filter.adaptRequest("Add Foo header", r -> r.with("Foo", List.of("Bar")));
var s = HttpServer.create(new InetSocketAddress(8080), 10, "/", h, f);
s.start();
Simple Web Server提供的搭建静态web服务的能力,在一些场景下非常适用,例如开发文件共享服务器、本地开发和测试服务等,而以往要搭建一个web服务需要使用Web服务器框架,下载、配置、调试等成本非常高。
你可以看到Java语言在提升其易用性方面,已经作出了一些改变,例如上文提到的使用命令行方式的开发和启动模式,更多内容会在后面的文章中继续介绍。
6.5 事件监控:Flight Recorder (JFR)
Flight Recorder是一个基于事件的监控工具,它会实时记录来自应用程序、JVM和操作系统的事件,使用Flight Recorder可以帮助我们更好地分析和定位生产环境的问题。
Java7中引入了一个叫Event-Based JVM Tracing的特性,它可以通过抛出事件的方式来追踪JVM信息,并且集成了简单的输出工具,可以把事件信息打印到stdout上。Flight Recorder正是基于这个特性,将原来的事件集进行了扩充,包含了java程序多方面的信息,并且在输出方面基于二进制文件实现,可以进行更高效率的事件读写。
JFR事件非常丰富,大致包含了JVM相关信息如类加载、GC、内存分析、方法调用等等,Java程序相关信息如线程调用、对象monitor进入和等待、对象分配、IO读写等等,操作系统相关信息如硬件信息、资源使用率、环境变量等等。在Java21中,完整的JFR事件定义,可以在这里查看:sap.github.io/SapMachine/…
基于录制文件的使用
我们可以通过命令行参数启动Flight Recorder:
$ java -XX:StartFlightRecording ...
也可以通过jcmd实时开启Flight Recorder:
$ jcmd <pid> JFR.start
$ jcmd <pid> JFR.dump filename=recording.jfr
$ jcmd <pid> JFR.stop
开发者可以自定义JFR事件:
import jdk.jfr.*;
@Label("Hello World")
@Description("Helps the programmer getting started")
class HelloWorld extends Event {
@Label("Message")
String message;
}
public static void main(String... args) throws IOException {
HelloWorld event = new HelloWorld();
event.message = "hello, world!";
event.commit();
}
通过读取JFR文件来消费对应事件:
import java.nio.file.*;
import jdk.jfr.consumer.*;
Path p = Paths.get("recording.jfr");
for (RecordedEvent e : RecordingFile.readAllEvents(p)) {
System.out.println(e.getStartTime() + " : " + e.getValue("message"));
}
JFR事件有四个基础属性:
- enabled - 是否记录事件
- threshold - 事件连续记录的最小持续时间
- stackTrace - 是否记录Event.commit()方法的堆栈跟踪
- period - 周期性事件的发出间隔
基于事件流的使用
JFR事件是记录在磁盘上的,分析事件要经过:录制 - 停止录制 - 下载文件 - 解析文件,这样一组操作来完成。显然,直接使用JFR对于应用程序的分析,效果很好,因为通常每次录制至少记录一分钟的数据。但对于监控目的来说则不太合适,监控需要的是数据和事件的动态更新。
为了解决实时监控的问题,JFR提供了一套基于事件流的新API。使用新API可以在java代码中直接开启JFR事件流,并实现实时监听,事件的写入和消费并且不会在磁盘中生成 .jfr文件,使用方式如下:
try (var rs = new RecordingStream()) {
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
rs.onEvent("jdk.CPULoad", event -> {
System.out.println(event.getFloat("machineTotal"));
});
rs.onEvent("jdk.JavaMonitorEnter", event -> {
System.out.println(event.getClass("monitorClass"));
});
rs.start();
}
这段代码监听了两个事件,一个是操作系统的CPU负载事件,每个1s发送一次当前CPU负载,打印字段machineTotal代表多核系统整体CPU负载情况;另一个是java对象monitor进入事件,事件最小间隔为10ms,打印java对象class信息。我们之前在虚拟线程这一章里一个例子也用到了JFR事件流,监控了虚拟线程固定事件:
try (var rs = new RecordingStream()) {
rs.enable("jdk.VirtualThreadPinned").withoutThreshold();
rs.onEvent("jdk.VirtualThreadPinned", e -> {
System.out.println(e);
});
rs.start();
}
有关JFR更多事件,可以在上文中的文档里查询。
JFR事件流除了可以监听本地java进程的事件外,还能够通过JMX实现远程进程的事件监听。基于这个功能我们可以很方便地实现一个中心化监控系统,官方提供了一个JFR事件流监控的开源demo,让我们来看一下它的具体实现。
应用案例:HealthReport
HealthReport是官方提供的一个JFR实例代码,包含了比较丰富的监控信息,可以监控本地java进程,也可以通过JMX监控远程java进程。源码git地址:github.com/flight-reco…
事件注册和解析部分代码如下:
public final class HealthReport {
...
// Only recording streams can enable settings.
private static EventSettings enable(EventStream es, String eventName) {
if (es instanceof RemoteRecordingStream) {
return ((RemoteRecordingStream)es).enable(eventName);
} else {
return ((RecordingStream)es).enable(eventName);
}
}
private void startStream(String source) throws Exception {
try (EventStream es = createStream(source)) {
if (es instanceof RemoteRecordingStream || es instanceof RecordingStream) {
// Event configuration
Duration duration = Duration.ofSeconds(1);
enable(es, "jdk.CPULoad").withPeriod(duration);
enable(es, "jdk.YoungGarbageCollection").withoutThreshold();
enable(es, "jdk.OldGarbageCollection").withoutThreshold();
enable(es, "jdk.GCHeapSummary").withPeriod(duration);
enable(es, "jdk.PhysicalMemory").withPeriod(duration);
enable(es, "jdk.GCConfiguration").withPeriod(duration);
enable(es, "jdk.SafepointBegin");
enable(es, "jdk.SafepointEnd");
enable(es, "jdk.ObjectAllocationSample").with("throttle", "150/s");
enable(es, "jdk.ExecutionSample").withPeriod(Duration.ofMillis(10)).withStackTrace();
enable(es, "jdk.JavaThreadStatistics").withPeriod(duration);
enable(es, "jdk.ClassLoadingStatistics").withPeriod(duration);
enable(es, "jdk.Compilation").withoutThreshold();
enable(es, "jdk.GCHeapConfiguration").withPeriod(duration);
enable(es, "jdk.Flush").withoutThreshold();
}
// Dispatch handlers
es.onEvent("jdk.CPULoad", this::onCPULoad);
es.onEvent("jdk.YoungGarbageCollection", this::onYoungColletion);
es.onEvent("jdk.OldGarbageCollection", this::onOldCollection);
es.onEvent("jdk.GCHeapSummary", this::onGCHeapSummary);
es.onEvent("jdk.PhysicalMemory", this::onPhysicalMemory);
es.onEvent("jdk.GCConfiguration", this::onGCConfiguration);
es.onEvent("jdk.SafepointBegin", this::onSafepointBegin);
es.onEvent("jdk.SafepointEnd", this::onSafepointEnd);
es.onEvent("jdk.ObjectAllocationSample", this::onAllocationSample);
es.onEvent("jdk.ExecutionSample", this::onExecutionSample);
es.onEvent("jdk.JavaThreadStatistics", this::onJavaThreadStatistics);
es.onEvent("jdk.ClassLoadingStatistics", this::onClassLoadingStatistics);
es.onEvent("jdk.Compilation", this::onCompilation);
es.onEvent("jdk.GCHeapConfiguration", this::onGCHeapConfiguration);
es.onEvent("jdk.Flush", this::onFlushpoint);
var heartBeat = new AtomicReference<>(Instant.now());
es.onFlush(() -> {
heartBeat.set(Instant.now());
printReport();
});
es.startAsync();
while (Duration.between(heartBeat.get(), Instant.now()).toSeconds() < timeout) {
Thread.sleep(100);
}
timedOut = true;
}
}
private void onCPULoad(RecordedEvent event) {
MACH_CPU.addSample(event.getDouble("machineTotal"));
SYS_CPU.addSample(event.getDouble("jvmSystem"));
USR_CPU.addSample(event.getDouble("jvmUser"));
}
private void onYoungColletion(RecordedEvent event) {
long nanos = event.getDuration().toNanos();
YC_COUNT.addSample(nanos);
YC_MAX.addSample(nanos);
YC_AVG.addSample(nanos);
}
private void onOldCollection(RecordedEvent event) {
long nanos = event.getDuration().toNanos();
OC_COUNT.addSample(nanos);
OC_MAX.addSample(nanos);
OC_AVG.addSample(nanos);
}
private void onGCHeapSummary(RecordedEvent event) {
USED_HEAP.addSample(event.getLong("heapUsed"));
COM_HEAP.addSample(event.getLong("heapSpace.committedSize"));
}
private void onPhysicalMemory(RecordedEvent event) {
PHYSIC_MEM.addSample(event.getLong("totalSize"));
}
private void onCompilation(RecordedEvent event) {
MAX_COM.addSample(event.getDuration().toNanos());
}
private void onGCConfiguration(RecordedEvent event) {
String gc = event.getString("oldCollector");
String yc = event.getString("youngCollector");
if (yc != null) {
gc += "/" + yc;
}
GC_NAME.addSample(gc);
}
private final Map<Long, Instant> safepointBegin = new HashMap<>();
private void onSafepointBegin(RecordedEvent event) {
safepointBegin.put(event.getValue("safepointId"), event.getEndTime());
}
private void onSafepointEnd(RecordedEvent event) {
long id = event.getValue("safepointId");
Instant begin = safepointBegin.get(id);
if (begin != null) {
long nanos = Duration.between(begin, event.getEndTime()).toNanos();
safepointBegin.remove(id);
SAFEPOINTS.addSample(nanos);
MAX_SAFE.addSample(nanos);
}
}
private double totalAllocated;
private long firstAllocationTime = -1;
private void onAllocationSample(RecordedEvent event) {
long size = event.getLong("weight");
String topFrame = topFrame(event.getStackTrace());
if (topFrame != null) {
ALLOCACTION_TOP_FRAME.addSample(topFrame, size);
AL_PE.addSample(topFrame, size);
}
TOT_ALLOC.addSample(size);
long timestamp = event.getEndTime().toEpochMilli();
totalAllocated += size;
if (firstAllocationTime > 0) {
long elapsedTime = timestamp - firstAllocationTime;
if (elapsedTime > 0) {
double rate = 1000.0 * (totalAllocated / elapsedTime);
ALLOC_RATE.addSample(rate);
}
} else {
firstAllocationTime = timestamp;
}
}
private void onExecutionSample(RecordedEvent event) {
String topFrame = topFrame(event.getStackTrace());
EXECUTION_TOP_FRAME.addSample(topFrame, 1);
EX_PE.addSample(topFrame, 1);
}
private void onJavaThreadStatistics(RecordedEvent event) {
THREADS.addSample(event.getDouble("activeCount"));
}
private void onClassLoadingStatistics(RecordedEvent event) {
long diff = event.getLong("loadedClassCount") - event.getLong("unloadedClassCount");
CLASSES.addSample(diff);
}
private void onGCHeapConfiguration(RecordedEvent event) {
INIT_HEAP.addSample(event.getLong("initialSize"));
}
private final static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private void onFlushpoint(RecordedEvent event) {
LocalDateTime d = LocalDateTime.ofInstant(event.getEndTime(), ZoneOffset.systemDefault());
FLUSH_TIME.addSample(FORMATTER.format(d));
}
...
private EventStream createStream(String source) throws Exception {
if (source.equals("self")) {
return new RecordingStream();
}
Path path = makePath(source);
if (path != null) {
return createFromPath(path);
}
JMXServiceURL url = makeJMXServiceURL(source);
if (url != null) {
return createRemoteStream(url);
}
Path repository = findRepository(source);
if (repository != null) {
return EventStream.openRepository(repository);
}
throw new Exception("Could not open : " + source);
}
}
这段代码展示了相当丰富的事件监听方法,例如宿主机硬件情况、GC信息、堆的使用情况、进程和线程信息、对象和方法调用情况等等。创建事件流的方法如下:
// # # # INSTANTIATE EVENT STREAM# # #
private EventStream createStream(String source) throws Exception {
if (source.equals("self")) {
return new RecordingStream();
}
Path path = makePath(source);
if (path != null) {
return createFromPath(path);
}
JMXServiceURL url = makeJMXServiceURL(source);
if (url != null) {
return createRemoteStream(url);
}
Path repository = findRepository(source);
if (repository != null) {
return EventStream.openRepository(repository);
}
throw new Exception("Could not open : " + source);
}
private boolean isFile;
private EventStream createFromPath(Path path) throws IOException {
if (Files.isDirectory(path)) {
return EventStream.openRepository(path);
} else {
EventStream es = EventStream.openFile(path);
isFile = true;
es.onClose( () -> {
timeout = 0; // aborts stream
});
if (replaySpeed != 0) {
es.onFlush(() -> takeNap(1000 / replaySpeed));
}
return es;
}
}
private EventStream createRemoteStream(JMXServiceURL url) throws IOException {
JMXConnector c = JMXConnectorFactory.newJMXConnector(url, null);
c.connect();
EventStream es = new RemoteRecordingStream(c.getMBeanServerConnection());
es.onClose(() -> {
CompletableFuture.runAsync(() -> {
try {
c.close();
} catch (IOException e) {
}
});
});
return es;
}
private static JMXServiceURL makeJMXServiceURL(String source) {
try {
return new JMXServiceURL(source);
} catch (MalformedURLException e) {
}
try {
String[] s = source.split(":");
if (s.length == 2) {
String host = s[0];
String port = s[1];
return new JMXServiceURL("rmi", "", 0, "/jndi/rmi://" + host + ":" + port + "/jmxrmi");
}
} catch (MalformedURLException e) {
}
return null;
}
private static Path makePath(String source) {
try {
Path p = Path.of(source);
if (Files.exists(p)) {
return p;
}
} catch (InvalidPathException ipe) {
}
return null;
}
record AttachableProcess(VirtualMachineDescriptor desc, String path) {
@Override
public String toString() {
String jfr = path != null ? "[JFR]" : " ";
return String.format("%-5s %s %s", desc.id(), jfr, desc.displayName());
}
}
private static Path findRepository(String source) {
for (AttachableProcess p : listProcesses()) {
if (source.equals(p.desc().id()) || p.desc().displayName().endsWith(source)) {
return Path.of(p.path());
}
}
return null;
}
private static List<AttachableProcess> listProcesses() {
List<AttachableProcess> list = new ArrayList<>();
try {
for (VirtualMachineDescriptor vm : VirtualMachine.list()) {
try {
VirtualMachine jvm = VirtualMachine.attach(vm);
Properties p = jvm.getSystemProperties();
String path = p.getProperty("jdk.jfr.repository");
jvm.detach();
list.add(new AttachableProcess(vm, path));
} catch (Exception e) {
debug(e.getMessage());
}
}
} catch (Exception e) {
debug(e.getMessage());
}
return list;
}
这段逻辑主要体现了三种创建事件流的方式:
- 创建当前进程的事件流,直接通过new RecordingStream()实现
- 基于本地路径或文件创建,使用EventStream.openRepository(path)或EventStream.openFile(path)
- 基于远程进程创建,使用new RemoteRecordingStream( MBeanServerConnection connection)
HealthReport程序可以采用单文件执行的方式来运行,这一启动特性我们会在脚本式开发章节中继续介绍,启动后效果如下:
总结一下,JFR是一个功能强大且性能良好的新工具,相比于java.lang.management下的监控工具类,JFR事件流不但提供了更简单和统一的API,而且在功能覆盖上也更全面,例如ThreadMXBean只能管理到平台线程,不能获取虚拟线程的信息。目前我们在实际生产环境中已经在使用JFR事件流来监控了,启动JFR的服务整体开销(CPU负载、内存等)也并不会有很明显的提升。