vlambda博客
学习文章列表

Java8到Java17迁移指南


Java8到Java17迁移指南

我正在从Java8迁移到Java17。我在早期阶段已经做了一些准备。该过程中的一些信息记录如下。

1. Compilation related

1.1 GIVE 320

在Java 11中,引入了一项提案JEP 320:移除Java EE和CORBA模块提案,该提案移除了Java EE和CORBA模块,如果在项目中使用,则需要手动引入。例如,以下javax.Annotation.*

import javax.annotation.PreDestroy;

public abstract class FridayAgent 

    @PreDestroy

    public void destroy() {

        agentClient.close();

    }

}

编译时将找不到相关的类。这是因为Java EEJava 9在Java 11中已被标记为不推荐使用,在Java 11中正式删除,并且可以手动导入javax包:

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

1.2 Packages under sun.misc.* are used

例如sun.misc.BASE64Encode,这很简单,只需替换Tool类即可。

[ERROR]   symbol:   class BASE64Encoder
[ERROR]   location: package sun.misc

较低版本的netty使用sun.misc.*,编译错误消息如下

Caused by: java.lang.Nthe oClassDefFoundError: Could not initialize class io.netty.util.internal.PlatformDependent0 at io.netty.util.internal.PlatformDependent.getSystemClassLoader(PlatformDependent.java:694) ~[netty-all-4.0.42.Final.jar!/:4.0.42.Final]

对应的源代码如下:

/**
 * The {@link PlatformDependent} operations which requires access to {@code sun.misc.*}.
 */
final class PlatformDependent0 {
}

1.3 Lombok uses packages under com.sun.tools.javac.*

错误消息如下:

Failed to execute goal org.apache.maven.plugins:maven-compiler
-plugin:3.2:compile (default-compile) on project encloud-common:
Fatal error compiling: java.lang.ExceptionInInitializerError:
Unable to make field private 
com.sun.tools.javac.processing.JavacProcessingEnvironment
$DiscoveredProcessors com.sun.tools.javac.processing.
JavacProcessingEnvironment.discoveredProcs accessible: 
module jdk.compiler does not "opens com.sun.tools.javac.processing"to unnamed module

如果您在项目中使用Lombok,并且它是一个低版本,它将会出现。Lombok的原理是在编译期间做一些小技巧,使用下的com.sun.tools.javac文件,升级到最新版本来解决这个问题。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
   <!-- <version>1.16.4</version>-->
    <version>1.18.24</version>
</dependency>

1.4 Kotlin version limit

多年来,我们的后端一直是全力以赴的Kotlin,而Kotlin的升级也是我们的首要任务。

[ERROR] Failed to execute goal org.jetbrains.kotlin:kotlin-maven-plugin:1.2.71:compile (compile) on project encloud-core: Compilation failure [ERROR] Unknown JVM target version: 17 [ERROR] Supported versions: 1.6, 1.8

Kotlin在1.6.0版本中开始支持Java17字节码,1.6.0以下的编译将直接报告错误

1.5 Deprecated dependency analysis

可以使用jdeps--jdk-interals--多版本17--类路径。EnCloud-api.jar用于项目依赖分析

Java8到Java17迁移指南

这样,您就可以知道哪些库需要升级。

2. Parameter migration

2.1 What is Unified Logging

在Java领域,有著名的日志框架,slf4j、log4j等。这些框架提供了统一的编程接口,允许用户通过简单的配置,如日志标签、级别、上下文(线程ID、行号、时间)来实现对日志输出的个性化配置,而JVM内部一直缺乏这样的规范,于是统一日志应运而生,实现了日志格式的统一。

我们接触最多的是GC日志。在java8中,我们配置GC日志的参数是-Xloggc:/tmp/gc.log。除了GC之外,JVM中还有大量的其他相关日志,如线程、os等。在新的统一日志中,日志输出的方式发生了变化,Java-xlog:xxx使得GC不再是特殊的,只是日志的一种存在形式。

java -Xlog -version

输出如下:

Java8到Java17迁移指南

您可以看到,在日志输出中,不仅有与GC相关的日志,还有与操作系统线程相关的信息。事实上,Java日志的生成器有很多部分,如线程、类加载、卸载、安全点、CD等。

Java8到Java17迁移指南

归根结底,日志打印需要明确回答三个问题:

  1. What information to output (tag) and what log level to output (level).
  2. Where to output the output (console or file).
  3. How to log.

2.2 What information to output (selectors)

首先看一下什么部分,如何指定要输出哪些信息,这在JVM中称为选择器。

JVM使用<;tag-set>;=<;Level>;的形式来表示选择器。默认情况下,标签为all,表示所有标签,级别为info,相当于以下形式

java -Xlog:all=info -version

如果我们想输出标签为GC、日志级别为DEBUG的日志,可以使用java-xlog:gc=DEBUG形式:

$ java -Xlog:gc=debug -version
[0.023s][info][gc] Using G1
[0.023s][debug][gc] ConcGCThreads: 3 offset 22
[0.023s][debug][gc] ParallelGCThreads: 10
[0.024s][debug][gc] Initialize mark stack with 4096 chunks, maximum 524288

这样,就可以输出标签为GC、级别为DEBUG的日志信息。

然而,这里有一个维修站。这里的标记匹配规则是完全匹配。如果日志gc,metspace的标签与上述规则不匹配,我们可以手动输出。

$ java -Xlog:gc+metaspace -version
[0.022s][info][gc,metaspace] CDS archive(s) mapped at: ... size 12443648.
[0.022s][info][gc,metaspace] Compressed class space mapped at: reserved size:...
[0.022s][info][gc,metaspace] Narrow klass base:..., Narrow 
klass shift: 0, Narrow klass range: 0x100000000

当然,这样做很麻烦。JVM提供了通配符*来解决精确匹配的问题。例如,如果我们想要标签为GC的所有日志,我们可以这样写:

$ java -Xlog:gc*=debug -version
[0.024s][debug][gc,heap] Minimum heap 8388608
[0.024s][info ][gc     ] Using G1
[0.024s][debug][gc,heap,coops] Heap address: 0x0000000707400000
[0.024s][debug][gc           ] ConcGCThreads: 3 offset 22
[0.024s][debug][gc           ] ParallelGCThreads: 10
[0.024s][debug][gc           ] Initialize mark stack with 4096 chunks
[0.024s][debug][gc,ergo,heap ] Expand the heap. requested expansion amount:
[0.025s][debug][gc,heap,region] Activate regions [0, 125)[0.025s][debug][gc,ihop       ] Target occupancy update: old: 0B, new: 262144000B
[0.025s][debug][gc,ergo,refine] Initial Refinement Zones: green: 2560
[0.026s][debug][gc,task       ] G1 Service Thread 
[0.026s][debug][gc,task       ] G1 Service Thread (Periodic GC Task) (register)
[0.026s][info ][gc,init       ] Version: 17.0.3+7 (release)
...

如果您只需要INFO级别的日志,Java-Xlog:gc*-Version可以。

如果您想知道哪些个性化标记可用,可以使用Java-Xlog:Help来查找所有可用的标记。

2.3 Where to Output (output)

默认情况下,日志将输出到stdout,JVM支持以下三种输出方法:

  • Stdout
  • Stderr
  • File

一般来说,我们会将日志输出到文件中进行进一步分析。

-Xlog:all=debug:file=/path_to_logs/app.log

还可以指定原木切割的大小和方式

-Xlog:gc*:file=/path_to_logs/app.log:filesize=104857600,filecount=5

2.4 Log decorators

除了正常信息外,每个日志都有大量与日志相关的上下文信息,在JVM中称为修饰符,并有以下选项。

[2022-06-15T19:54:01.529+0800][0.001s][5235][info ][os,thread] Thread attached
[2022-06-15T19:54:01.529+0800][0.001s][5235][debug][os,thread] Thread 5237 stack...
[2022-06-15T19:54:01.529+0800][0.001s][5235][debug][perf,datacreation]

输出格式如下:

-Xlog:[selectors]:[output]:[decorators][:output-options]
  • Selectors is a combination of multiple tags and levels, and plays the role of what (filter). The format istag1[+tag2...][*][=level][,...]
  • Decorators are log-related description information, which can also be understood as context
  • Output is an output-related option. Generally, we will configure it to output to a file and cut it according to the file size.

下面是一个补充知识点,这是缺省值:

  • Tag:all
  • Level:info
  • Output:stdout
  • Decorators: uptime, level, tags

2.6 GC parameter migration

可以看到,在Xlog下已经收集了GC相关的参数,而在Java8下之前的许多参数已经被移除或标记为过期。

例如PrintGCDetail已被-xlog:gc*替换为:

java -XX:+PrintGCDetails -version
[0.001s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.

标记为已弃用的常见参数有-XX:+PrintGC-Xloggc:<;filepath&>

此外,删除了大量GC参数,如常用参数-XX:+PrintTenuringDistributed,Java 17将拒绝启动。

java -XX:+PrintTenuringDistribution -version
Unrecognized VM option 'PrintTenuringDistribution'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

更详细的删除参数如下

CMSDumpAtPromotionFailure,
CMSPrintEdenSurvivorChunks,
GlLogLevel,
G1PrintHeapRegions, 
G1PrintRegionLivenessInfo, 
G1SummarizeConcMark,
G1SummarizeRSetStats,
G1TraceConcRefinement,
G1TraceEagerReclaimHumongousObjects, 
G1TraceStringSymbolTableScrubbing,
GCLogFileSize, NumberofGCLogFiles, 
PrintAdaptiveSizePolicy,
PrintclassHistogramAfterFullGC,
PrintClassHistogramBeforeFullGC,
PrintCMSInitiationStatistics
PrintCMSStatistics, 
PrintFLSCensus,
PrintFLSStatistics,
PrintGCApplicationConcurrentTime
PrintGCApplicationStoppedTime,
PrintGCCause,
PrintGCDateStamps, 
PrintGCID,
PrintGCTaskTimeStamps,
PrintGCTimeStamps,
PrintHeapAtGC,
PrintHeapAtGCExtended, 
PrintJNIGCStalls,
PrintOldPLAB
PrintParallel0ldGCPhaseTimes, 
PrintPLAB,
PrintPromotionFailure,
PrintReferenceGC, 
PrintStringDeduplicationStatistics, 
PrintTaskqueue,
PrintTenuringDistribution, 
PrintTerminationStats,
PrintTLAB,
TraceDynamicGCThreads,
TraceMetadataHumongousAllocation,
UseGCLogFileRotation,
VerifySilently

这些删除的参数中的大多数可以在新的日志系统中找到。例如,PrintHeapAtGC可以使用-xlog:gc+heap=DEBUG来替换。

$ java -Xlog:gc+heap=debug -cp . G1GCDemo01
[0.004s][debug][gc,heap] Minimum heap 8388608  Initial heap 268435456  Maximum heap 
hello, g1gc!
[12.263s][debug][gc,heap] GC(0) Heap before GC invocations=0 (full 0):
[12.265s][debug][gc,heap] GC(0)  garbage-first heap 
[12.265s][debug][gc,heap] GC(0)   region size 2048K, 1 young (2048K)
[12.265s][debug][gc,heap] GC(0)  Metaspace       used 3678K
[12.265s][debug][gc,heap] GC(0)   class space    used 300K
[12.280s][debug][gc,heap] GC(0) Uncommittable regions after shrink: 124
[0.122s][debug][gc, ergo, refine] Initial Refinement Zones: green: 23, yellow:
69, red: 115, min yellow size: 46
[0.142s ][debug][gc, ergo, heap ] Expand the heap. requested expansion amount: 268435456B expansion amount: 268435456B
[2.475s][trace][gc, ergo, cset] GC(0) Start choosing CSet. pending cards: 0 predicted base time: 10.00ms remaining time:
190.00ms target pause time: 200.00ms
[2.476s][trace][gc, ergo, cset ] GC(9) Add young regions to CSet. eden: 24 regions, survivors: 0 regions, predicted young
region time: 367.19ms, target pause time: 200.00ms
[2.476s ][debug][gc, ergo, cset ] GC(0) Finish choosing CSet. old: 0 regions, predicted old region time: 0.00ms, time
remaining: 0.00
[2.826s][debug][gc, ergo] GC(0) Running G1 Clear Card Table Task using 1 workers for 1 units of work for 24 regions.
[2.827s][debug][gc, ergo] GC (0) Running G1 Free Collection Set using 1 workers for collection set length 24
[2.828s][trace][gc, ergo, refine] GC(0) Updating Refinement Zones: update rs time: 0.004ms, update rs buffers: 0, update rs
goal time: 19.999ms
[2.829s][debug][gc, ergo, refine] GC(0) Updated Refinement Zones: green: 23, yellow: 69, red: 115
[3.045s][trace][gc, ergo, set ] GC(1) Start choosing CSet. pending cards: 5898 predicted base time: 26.69ms remaining
time: 173.31ms target pause time: 200.00ms
[3.045s][trace][gc, ergo, cset ] GC(1) Add young regions to Set. eden: 9 regions, survivors: 3 regions, predicted young
region time: 457.38ms, target pause time: 200.00ms
[3.045s][debug](gc, ergo, set ] GC(1) Finish choosing CSet. old: @ regions, predicted old region time: 0.00ms, time
remaining: 0.00
[3.090s ][debug][gc, ergo
] GC (1) Running G1 Clear Card Table Task using 1 workers for 1 units of work for 12 regions.
[3.091s][debug][gc, ergo
GC (1) Running G1 Free Collection Set using 1 workers for collection set length 12
[3.093s][trace][gc, ergo, refine] GC(1) Updating Refinement Zones: update rs time: 2.510ms, update rs buffers: 25, update rs
goal time: 19.999ms
[3.093s ][debug][gc, ergo, refine] GC(1) Updated Refinement Zones: green: 25, yellow: 75, red: 125

看看这部分源代码的变化,你就会知道情况就是这样。在Java 8中,PSYoungGen::RESIZE_SPAKS代码如下:

Java8到Java17迁移指南

在Java 17中,日志打印的这一部分被GC+ERGO的标记日志所取代:

Java8到Java17迁移指南

在世代GC中还有一个非常有用的参数-XX:+PrintTenuringDistributed,现在GC+age=TRACE被取代。

2.7 Parameter migration example

-XX:+PrintGCDetails                           \  // gc*
-XX:+PrintGCApplicationStoppedTime            \  // safepoint
-XX:+PrintGCApplicationConcurrentTime         \  // safepoint 
-XX:+PrintGCCause                             \  // default output
-XX:+PrintGCID                                \  // default output
-XX:+PrintTenuringDistribution                \  // gc+age*=trace
-XX:+PrintGCDateStamps                        \  // :time,tags,level
-XX:+UseGCLogFileRotation                     \  // :filecount=5,filesize=10M 
-XX:NumberOfGCLogFiles=5                      \  // :filecount=5,filesize=10M 
-XX:GCLogFileSize=10M                         \  // :filecount=5,filesize=10M 
-Xloggc:/var/log/`date +%FT%H-%M-%S`-gc.log   \  // -Xlog::file=/var/log/%t-gc.log

更改后:

-Xlog:
  gc*, 
  safepoint, 
  gc+heap=debug, 
  gc+ergo*=trace, 
  gc+age*=trace, 
  :file=/var/log/%t-gc.log 
  :time,tags,level 
  :filecount=5,filesize=10M

推荐配置

-Xlog:
  // selections
    codecache+sweep*=trace,
    class+unload,                      // TraceClassUnloading
    class+load,                        // TraceClassLoading
    os+thread,
    safepoint,                        // TraceSafepoint
    gc*,                              // PrintGCDetails
    gc+stringdedup=debug,             // PrintStringDeduplicationStatistics
    gc+ergo*=trace,
    gc+age=trace,                     // PrintTenuringDistribution
    gc+phases=trace,
    gc+humongous=trace,
    jit+compilation=debug
// output
:file=/path_to_logs/app.log   
// decorators               
:level,tags,time,uptime,pid
// output-options                
:filesize=104857600,filecount=5

3. Run related

3.1 Reflection + private API call injury

在Java 8中,没有人可以阻止您访问特定的包,比如sun.misc,并且对反射没有限制,只要setAccesable(True)是可以的。Java 9模块化后,一切都变了,--add-exports--Add-Open模块封装只能突破和

  • --add-opensexport a specific package
  • --add-opensAllow classpath deep reflective access to specific packages in modules
--add-opens java.base/java.lang=ALL-UNNAMED 
--add-opens java.base/java.io=ALL-UNNAMED 
--add-opens java.base/java.math=ALL-UNNAMED 
--add-opens java.base/java.net=ALL-UNNAMED 
--add-opens java.base/java.nio=ALL-UNNAMED 
--add-opens java.base/java.security=ALL-UNNAMED 
--add-opens java.base/java.text=ALL-UNNAMED 
--add-opens java.base/java.time=ALL-UNNAMED 
--add-opens java.base/java.util=ALL-UNNAMED 
--add-opens java.base/jdk.internal.access=ALL-UNNAMED 
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED

3.2 About the choice of GC algorithm

CMS正式退出历史舞台,G1正式接手,ZGC蓄势待发。在GC算法的选择上,G1仍是目前的最佳选择。由于ZGC的内存使用率被操作系统占用过高(是共享内存的三倍),进程可能会被OOM杀手杀死。

3.3 ZGC triple RES memory

ZGC的底层使用一种称为彩色指针的技术,该技术使用三个视图(Marked0、Marked1和Remap)来映射到相同的共享内存区。原则如下:

#include <iostream>

#include <sys/mman.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <cstdio>

#include <cstdlib>

int main() {

    int fd = ::shm_open("/test", O_RDWR | O_CREAT | O_EXCL, 0600);

    if (fd < 0) {

        shm_unlink("/test");

        perror("shm open failed");

        return 0;

    }

    size_t size = 1 * 1024 * 1024 * 1024;

    ::ftruncate(fd, size);

    int prot = PROT_READ | PROT_WRITE;

    uint32_t *p1 = (uint32_t *) (mmap(nullptr, size, prot, MAP_SHARED, fd, 0));

    uint32_t *p2 = (uint32_t *) (mmap(nullptr, size, prot, MAP_SHARED, fd, 0));

    uint32_t *p3 = (uint32_t *) (mmap(nullptr, size, prot, MAP_SHARED, fd, 0));

    ::close(fd);

    *p1 = 0xcafebabe;

    ::printf("Address of addr1: %p, value is 0x%x\n", p1, *p1);

    ::printf("Address of addr2: %p, value is 0x%x\n", p2, *p2);

    ::printf("Address of addr3: %p, value is 0x%x\n", p3, *p3);

    ::getchar();

    *p2 = 0xcafebaba;

    ::printf("Address of addr1: %p, value is 0x%x\n", p1, *p1);

    ::printf("Address of addr2: %p, value is 0x%x\n", p2, *p2);

    ::printf("Address of addr3: %p, value is 0x%x\n", p3, *p3);

    ::getchar();

    munmap(p1, size);

    munmap(p2, size);

    munmap(p3, size);

    shm_unlink("/test");

    std::cout << "hello" << std::endl;

}

您可以想象三个内存区p1、p2和p3是ZGC中的三个视图。

但在Linux统计中,虽然是共享内存,但仍然会被计算三次,比如res。

同样的应用,使用G1 res显示它占用2G,ZGC显示它占用6G

java -XX:+AlwaysPreTouch -Xms2G -Xmx2G -XX:+UseZGC MyTest
java -XX:+AlwaysPreTouch -Xms2G -Xmx2G -XX:+UseG1GC MyTest                                                                                              
Java8到Java17迁移指南

我们下面讨论的是与G1相关的。

3.4 Do not configure the size of the young generation

在《JVM G1源代码分析和调优》一书中详细描述了这一点,主要有两个原因:

  • G1’s management of memory is discontinuous, and the cost of reassigning a partition is very low.
  • G1 needs to dynamically adjust the number of partitions collected according to the target pause time. If the size of the new generation cannot be adjusted, then G1 may not be able to meet the pause time requirements.

例如-xmann,-XX:NewSize,-XX:MaxNewSize,-XX:Survivor Ratio不会出现在G1中,只需要控制最大和最小堆以及目标暂停时间。

3.5 Adjust -XX:InitiatingHeapOccupancyPercentto the appropriate value

IHOP的默认值为45。该值是开始并发标记的先决条件。并发标记任务将仅在旧一代中总内存堆栈空间的45%之后启动。

增加此值:并发标记可能会花费更多时间,同时YGC和混合GC收集过程中的分区数量会减少,可以根据整个应用程序占用的平均内存进行设置。