Java21手册(六):集合、网络与监控

890 阅读16分钟

本文会介绍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 elementLast element
Listlist.get(0)list.get(list.size() - 1)
Dequedeque.getFirst()deque.getLast()
SortedSetsortedSet.first()sortedSet.last()
LinkedHashSetlinkedHashSet.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负载、内存等)也并不会有很明显的提升。

image.png