Java8 到 Java17 升级指南(Bug大全)
文章目录
- Java8 到 Java17 升级指南(Bug大全)
- 编译相关
- JEP 320
- 使用了 sun.misc.* 下的包
- lombok 使用了 com.sun.tools.javac.* 下的包
- kotlin 版本限制
- 废弃依赖分析
- 参数迁移
- 什么是 Unified Logging
- 输出什么信息(selectors)
- 第二部分:输出到哪里(output)
- 第三部分:日志 decorators
- Unified Logging 小结
- GC 参数迁移
- 举例
- 推荐的配置
- 运行相关
- 反射+私有 API 调用之伤
- 关于 GC 算法的选择
- ZGC 三倍 RES 内存
- G1 参数调整
- 不要配置新生代的大小
- 调整 `-XX:InitiatingHeapOccupancyPercent` 到合适的值
- 结语
Java 8 是旧时代的 Java 6,还不快升级 。最近在做 Java8 到 Java17 的迁移工作,前期做了一些准备,过程中的一些信息记录如下(持续更新。。。)
分为几个部分:
- 编译相关
- 参数迁移相关
- 运行相关
编译相关
JEP 320
在 Java11 中引入了一个提案 JEP 320: Remove the Java EE and CORBA Modules (openjdk.org/jeps/320) 提案,移除了 Java EE and CORBA 的模块,如果项目中用到需要手动引入。比如代码中用到了 javax.annotation.*
下的包:
在编译时会找不到相关的类。这是因为 Java EE 已经在 Java 9
中被标记为 deprecated,Java 11 中被正式移除,可以手动引入 javax 的包:
使用了 sun.misc.* 下的包
比如 sun.misc.BASE64Encoder,这个简单,替换一下工具类即可。
netty 低版本使用了 sun.misc.*,编译错误信息如下
对应的源码如下:
lombok 使用了 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
下的文件,升级到最新版可以解决。ps,个人很不喜欢 lombok, 调试的时候代码和 class 对不上真的很恶心。
kotlin 版本限制
我们后端在很多年前就 all-in 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 的编译会直接报错
废弃依赖分析
可以用 jdeps --jdk-internals --multi-release 17 --class-path . encloud-api.jar
来做项目的依赖分析
这样你就可以知道哪些库需要做升级了。
参数迁移
什么是 Unified Logging
在 Java 领域,有广为人知的日志框架,slf4j、log4j 等,这些框架提供了统一的编程接口,让用户可以通过简单的配置实现日志输出的个性化配置,比如日志 tag、级别(info、debug 等)、上下文(线程 id、行号、时间等),在 JVM 内部之前一直缺乏这样的规范,于是出来了 Unified Logging,实现了日志格式的大一统,这就是我们接下来要介绍的重点 Unified Logging
。
我们接触最多的是 gc 的日志,在 java8 中,我们配置 gc 日志的参数是 -Xloggc:/tmp/gc.log
。在 JVM 中除了 GC,还有大量的其它相关的日志,比如线程、os 等,在新的 Unified Logging 日志中,日志输出的方式变更为了 java -Xlog:xxx
,GC 不再特殊只是做为日志的一种存在形式。
输出结果如下:
可以看到日志输出里,不仅有 GC 相关的日志,还有 os 线程相关的信息。事实上 java 的日志的生产者有非常多部分,比如 thread、class load、unload、safepoint、cds 等。
归根到底,日志打印,需要回答清楚三个问题:
- what:要输出什么信息(tag),以什么日志级别输出(level)
- where:输出到哪里(console 还是 file)
- decorators:日志如何
输出什么信息(selectors)
首先来看 what 的部分,如何指定要输出哪些信息,这个在 JVM 内部被称之为 selectors。
JVM 采用的是 <tag-set>=<level>
的形式来表示 selectors,默认情况下,tag 为all
,表示所有的 tag,level 为 INFO
,java -Xlog -version
等价于下面的形式
如果我们想输出tag 为 gc,日志级别为 debug 的日志,可以用 java -Xlog:gc=debug
的形式:
这样就输出了 tag 为 gc,级别为 debug 的日志信息。
不过这里有一个比较坑的点是,这里的 tag 匹配规则是精确匹配,如果某条日志的 tag 是 gc,metaspace
,通过上面的规则是匹配不到的,我们可以手动指定的方式来输出。
这里的 selector 也是可以进行组合的,不同的 selector 之间用逗号分隔即可。比如同时输出 gc
和 gc+metaspace
这两类 tag 的日志,就可以这么写:
当然这么搞是很麻烦的,JVM 提供了通配符 *
来解决精确匹配的问题,比如我们想要所有 tag 为 gc 的日志,可以这么写:
如果只想要 INFO 级别的日志,则可以省略 level 的设置,使用 java -Xlog:gc* -version
即可。
如果想知道有哪些个性化的 tag 可以选择,可以用 java -Xlog:help
来找到所有可用的 tag。
阶段性小结
第二部分:输出到哪里(output)
默认情况下,日志会输出到 stdout,jvm 支持以下三种输出方式:
- stdout
- stderr
- file
一般而言我们会把日志输出到文件中,方便后续进一步分析
还可以指定日志切割的大小和方式
第三部分:日志 decorators
每条日志除了正常的信息以外,还有不少日志相关的上下文信息,在 jvm 中被称为 decorators
,有下面这些可选项。
Option | Description |
time | Current time and date in ISO-8601 format. |
uptime | Time since the start of the JVM in seconds and milliseconds (e.g., 6.567s). |
timemillis | The same value as generated by System.currentTimeMillis(). |
uptimemillis | Milliseconds since the JVM started. |
timenanos | The same value as generated by System.nanoTime(). |
uptimenanos | Nanoseconds since the JVM started. |
pid | The process identifier. |
tid | The thread identifier. |
level | The level associated with the log message. |
tags | The tag-set associated with the log message. |
比如可以用 java -Xlog:all=debug:stdout:level,tags,time,uptime,pid -version
选项来打印日志。
Unified Logging 小结
输出格式如下:
- selectors 是多个 tag 和 level 的组合,起到了 what(过滤器)的作用,格式为
tag1[+tag2...][*][=level][,...]
- decorators 是日志相关的描述信息,也可以理解为上下文
- output 是输出相关的选项,一般我们会配置为输出到文件,按文件大小切割
这里补充一个知识点,就是默认值:
- tag:all
- level:info
- output:stdout
- decorators: uptime, level, tags
GC 参数迁移
可以看到 GC 相关的参数都已经收拢到 Xlog 下,以前的很多 Java8 下的参数已经被移除或者标记为过期。
比如 PrintGCDetails
已经被 -Xlog:gc*
取代:
常见的标记为废弃的参数还有 -XX:+PrintGC
和 -Xloggc:<filepath>
,迁移前后的参数如下:
旧参数 | 新参数 |
-XX:+PrintGCDetails | -Xlog:gc* |
-XX:+PrintGC | -Xlog:gc |
-Xloggc: | -Xlog:gc: |
除此之外,大量的 GC 的参数被移除,比如常用的参数 -XX:+PrintTenuringDistribution
,Java17 会拒绝启动
更详细的移除的参数如下
这些移除的参数大部分都能在新的日志体系下找到对应的参数,比如 PrintHeapAtGC
这个参数可以用 -Xlog:gc+heap=debug
来替代
虽然理解起来不太直观,不过要记住 -XX:+PrintGCApplicationStoppedTime
和 -XX+PrintGCApplicationConcurrentTime
这两个参数一起被 -Xlog:safepoint
取代。
还有一个常见的参数 -XX:+PrintAdaptiveSizePolicy
被 -Xlog:gc+ergo*=trace
取代,
看一下这部分的源码的变迁,就可以知道确实是如此了,在 Java8 中,PSYoungGen::resize_spaces
代码如下:
在 Java17 中,这部分日志打印被 gc+ergo 的标签日志取代:
还有一个分代 GC 中非常有用的参数 -XX:+PrintTenuringDistribution
,现在被 gc+age=trace
取代
完整的参数变迁对应表如下:
旧 GC 参数 -XX:+… | 对应新 GC 参数 | GC 参数含义 |
PrintGC -Xloggc: | gc | Print message at garbage collection |
PrintGCDetails -Xloggc: | gc* | Print more details at garbage collection |
-verbose:gc | gc=trace gc+heap=trace gc+heap+exit=trace gc+metaspace=trace gc+sweep=debug gc+heap+ergo=debug | Verbose GC |
PrintGCCause | GC cause is now always logged | Include GC cause in GC logging |
PrintGCID | GC ID is now always logged | Print an identifier for each garbage collection |
PrintGCApplicationStoppedTime | safepoint | Print the time the application has been stopped |
PrintGCApplicationConcurrentTime | safepoint | Print the time the application has been running |
PrintTenuringDistribution | gc+age*=trace | Print tenuring age information |
PrintAdaptiveSizePolicy | gc+ergo*=trace | Print information about AdaptiveSizePolicy |
PrintHeapAtGC | gc+heap=debug | Print heap layout before and after each GC |
PrintHeapAtGCExtended | gc+heap=trace | Print extended information about the layout of the heap when -XX:+PrintHeapAtGC is set |
PrintClassHistogramBeforeFullGC | classhisto*=trace | Print a class histogram before any major stop-world GC |
PrintClassHistogramAfterFullGC | classhisto*=trace | Print a class histogram after any major stop-world GC |
PrintStringDeduplicationStatistics | gc+stringdedup*=debug | Print string deduplication statistics |
PrintJNIGCStalls | gc+jni=debug | Print diagnostic message when GC is stalled by JNI critical section |
PrintReferenceGC | gc+ref=debug | Print times spent handling reference objects during GC |
PrintGCTaskTimeStamps | task*=debug | Print timestamps for individual gc worker thread tasks |
PrintTaskQueue | gc+task+stats=trace | Print taskqueue statistics for parallel collectors |
PrintPLAB | gc+plab=trace | Print (survivor space) promotion LAB’s sizing decisions |
PrintOldPLAB | gc+plab=trace | Print (old gen) promotion LAB’s sizing decisions |
PrintPromotionFailure | gc+promotion=debug | Print additional diagnostic information following promotion failure |
PrintTLAB | gc+tlab=trace | Print various TLAB related information (augmented with -XX:+TLABStats) |
PrintTerminationStats | gc+task+stats=debug | Print termination statistics for parallel collectors |
G1PrintHeapRegions | gc+region=trace | If set G1 will print information on which regions are being allocated and which are reclaimed |
G1PrintRegionsLivenessInfo | gc+liveness=trace | Prints the liveness information for all regions in the heap at the end of a marking cycle |
G1SummarizeConcMark | gc+marking=trace | Summarize concurrent mark info |
G1SummarizeRSets | gc+remset*=trace | Summarize remembered set processing info |
G1TraceConcRefinement | gc+refine=debug | Trace G1 concurrent refinement |
G1TraceEagerReclaimHumongousObjects | gc+humongous=debug | Print some information about large object liveness at every young GC |
G1TraceStringSymbolTableScrubbing | gc+stringdedup=trace | Trace information string and symbol table scrubbing |
PrintParallelOldGCPhaseTimes | gc+phases=trace | Print the time taken by each phase in ParallelOldGC |
CMSDumpAtPromotionFailure | gc+promotion=trace | Dump useful information about the state of the CMS old generation upon a promotion failure (complemented by flags CMSPrintChunksInDump or CMSPrintObjectsInDump) |
CMSPrintEdenSurvivorChunks | gc+heap=trace | Print the eden and the survivor chunks used for the parallel initial mark or remark of the eden/survivor spaces |
PrintCMSInitiationStatistics | gc=trace | Statistics for initiating a CMS collection |
PrintCMSStatistics | gc=debug (trace) gc+task=trace gc+survivor=trace log+sweep=debug (trace) | Statistics for CMS (complemented by CMSVerifyReturnedBytes) |
PrintFLSCensus | gc+freelist+census=debug | Census for CMS’ FreeListSpace |
PrintFLSStatistics | gc+freelist+stats=debug (trace) gc+freelist*=debug (trace) | Statistics for CMS’ FreeListSpace |
TraceCMSState | gc+state=debug | Trace the state of the CMS collection |
TraceSafepoint | safepoint=debug | Trace application pauses due to VM operations in safepoints |
TraceSafepointCleanupTime | safepoint+cleanup=info | break down of clean up tasks performed during safepoint |
TraceAdaptativeGCBoundary | heap+ergo=debug | Trace young-old boundary moves |
TraceDynamicGCThreads | gc+task=trace | Trace the dynamic GC thread usage |
TraceMetadataHumongousAllocation | gc+metaspace+alloc=debug | Trace humongous metadata allocations |
VerifySilently | gc+verify=debug |
举例
变迁后:
推荐的配置
运行相关
反射+私有 API 调用之伤
在 Java8 中,没有人能阻止你访问特定的包,比如 sun.misc,对反射也没有限制,只要 setAccessible(true) 就可以了。Java9 模块化以后,一切都变了,只能通过 --add-exports
和 --add-opens
来打破模块封装
-
--add-opens
导出特定的包 -
--add-opens
允许模块中特定包的类路径深度反射访问
比如:
关于 GC 算法的选择
CMS 正式退出历史舞台,G1 正式接棒,ZGC 蓄势待发。在GC 算法的选择上,目前来看 G1 还是最佳的选择,ZGC 因为有内存占用被 OS 标记过高(三倍共享内存)虚高的问题,进程可能被 OOM-killer 杀掉。
ZGC 三倍 RES 内存
ZGC 底层用到了一个称之为染色指针的技术,使用三个视图(Marked0、Marked1 和 Remapped)来映射到同一块共享内存区域,原理如下:
你可以想象 p1、p2、p3 这三块内存区域就是 ZGC 中三种视图。
但是在 linux 统计中,虽然是共享内存,但是依然会统计三次,比如 RES。
同一个应用,使用 G1 RES 显示占用 2G,ZGC 则显示占用 6G
接下面我们讨论的都是 G1 相关的。
G1 参数调整
不要配置新生代的大小
这个在《JVM G1 源码分析和调优》一书里有详细的介绍,有两个主要的原因:
- G1对内存的管理是不连续的,重新分配一个分区代价很低
- G1 的需要根据目标停顿时间动态调整搜集的分区的个数,如果不能调整新生代的大小,那么 G1 可能不能满足停顿时间的要求
诸如 -Xmn, -XX:NewSize, -XX:MaxNewSize, -XX:SurvivorRatio
都不要在 G1 中出现,只需要控制最大、最小堆和目标暂停时间即可
调整 -XX:InitiatingHeapOccupancyPercent 到合适的值
IHOP 默认值为 45,这个值是启动并发标记的先决条件,只有当老年代内存栈总空间的 45% 之后才会启动并发标记任务。
增加这个值:导致并发标记可能花费更多的时间,同时导致 YGC 和 Mixed-GC 收集时的分区数变少,可以根据整体应用占用的平均内存来设置。
结语
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、评论、收藏➕关注,您的支持是我坚持写作最大的动力。