本文記錄了作者從JDK8升級到11的實踐過程和升級後的效果以及JDK11新玩法。
一、背景
為什麼要升級JDK11
效能
JDK11的G1的GC效能高很多,對比JDK8無論是效能還是記憶體佔比都有很大的提升,業內各項資料指標也都表明JDK11的G1在應對突發流量的下的效果驚人;
版本相容
Spring Boot 2.7.x及以後的版本將不再支援Java 8作為最低版本。Spring Boot 2.6.x是最後一個正式支援Java 8的主線版本,一些新的中介軟體與元件也不再支援JDK8了;
必然趨勢
JDK11(LTS)已經成為業界主流,在Java開發社羣和工業界中得到了廣泛的接受和使用;
二、升級前你要知道的點
JDK11版本改動較大,且不會向下相容。所以當你的業務程式碼越複雜,呼叫的鏈路越多,升級的難度越大。你會遇到很多相容性問題,比如 二方包不支援新版本JDK;
JDK11移除了部分在Java 8就已經標記為過時的API例如sun.misc.Unsafe的部分方法,所以你的升級可能還涉及到程式碼的改動;
驗證是個漫長而又耗時的過程,很多問題可能在執行時階段纔會暴露,你需要驗證系統整體功能來保證系統穩定;
三、升級過程
本地升級,讓你的JDK11跑起來
本地JDK11下載
這裏不過多闡述,需要注意區分JDK的arm版本與x64版本。
IDEA選擇JDK11啟動
框架升級
修改pom檔案
<maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> <java.version>11</java.version> <spring-boot.version>2.1.6.RELEASE</spring-boot.version> <lombok.version>1.18.12</lombok.version>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
jdk11已移除,需手工依賴二方庫
<dependency> <groupId>javax.xml.soap</groupId> <artifactId>javax.xml.soap-api</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-ri</artifactId> <version>2.3.3</version> <type>pom</type> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.sun.activation</groupId> <artifactId>javax.activation</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.alibaba.jvm</groupId> <artifactId>java-migration-jdk-patch</artifactId> <version>0.3.1</version> <type>pom</type> </dependency> <dependency> <groupId>javax.transaction</groupId> <artifactId>javax.transaction-api</artifactId> <version>1.2</version> </dependency>
遇到的問題
Deprecated: A global security auto-configuration is now provided
在Spring Boot 2.0及以上版本中,這個配置項已經被廢棄並移除。如果你要關閉端點的安全性,需要在Spring Security的配置中對Actuator端點進行配置。該配置項是預設開啟安全檢測。
Dependency 'org.hibernate:hibernate-validator:' not found
需要指定版本號
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.2.4.Final</version> </dependency>
應用不能進行遠端除錯
原因分析
JDK 8 中 jdwp 預設繫結的 host/ip 是 0.0.0.0,初於安全考慮在 JDK 9 後改成了 localhost(127.0.0.1),匯出如果開發者在配置除錯選項時只指定埠時,在升級後無法進行遠端除錯。
解決方案
指定除錯選項時設定 host/ip 為 *,如:
agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000
或者 0.0.0.0,如:
agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000
其他問題
1、maven-compiler-plugin:此外掛建議直接升級到最新版,同時在父Pom和每個你需要額外確定版本的包(比如說打給別人用的JDK8版本的包)裡的Pom,指定版本:
<maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target>
2、Springboot和Spring版本:Spring從5.1開始支援11,Springboot從2.1.X開始支援11,我們的推薦是支援升級到當前的最新版;
3、Netty因為堆外記憶體的釋放問題,請升級到4.1.33以上的版本;
4、lombok因為會在編譯期插入自己的編譯邏輯,所以升級到11之後,需要將lombok升級到最新版,(編輯文件時的最新版本是1.18.24);
5、可能大部分應用都需要進行Spring或者Springboot升級,請務必做好迴歸;
6、security-spring-boot-starter分為1.x.x和2.x.x版本,對應springboot1和springboot2,請升級到2.x.x版本;
應用部署釋出
使用G1垃圾回收器
去除 #SERVICE_OPTS="${SERVICE_OPTS} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5000" #SERVICE_OPTS="${SERVICE_OPTS} -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly" #SERVICE_OPTS="${SERVICE_OPTS} -XX:+ExplicitGCInvokesConcurrent -Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000" #SERVICE_OPTS="${SERVICE_OPTS} -XX:ParallelGCThreads=4" #SERVICE_OPTS="${SERVICE_OPTS} -Xloggc:${MIDDLEWARE_LOGS}/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps" SERVICE_OPTS="${SERVICE_OPTS} -XX:+UseG1GC -XX:+UseVtableBasedCHA -XX:+UseCompactObjectHeaders" SERVICE_OPTS="${SERVICE_OPTS} -XX:G1HeapRegionSize=8m" SERVICE_OPTS="${SERVICE_OPTS} -XX:+G1BarrierSkipDCQ" SERVICE_OPTS="${SERVICE_OPTS} -Xlog:gc*:/home/admin/logs/gc.log:time" SERVICE_OPTS="${SERVICE_OPTS} -XX:G1HeapWastePercent=2" SERVICE_OPTS="${SERVICE_OPTS} -XX:+ExplicitGCInvokesConcurrent -Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000" if [ -n "$AJDK_MAX_PROCESSORS_LIMIT" ]; then SERVICE_OPTS="${SERVICE_OPTS} -XX:ActiveProcessorCount=$AJDK_MAX_PROCESSORS_LIMIT" fi
GC調優的注意事項(資料來源JVM團隊)
通常G1 GC是一個免調參的GC,並不需要額外的引數調整。老的一些的八股文Java GC調參經驗並不適用。
-Xmn引數一般不需要設定
G1預設了-XX:NewSize和-XX:MaxNewSize的值(不一致),會根據實際執行來計算設定每次GC的young區的size,實現GC暫停的軟可控。
-XX:NewRatio同理不需要設定
-XX:SurvivorRatio一般也不設定
通常絕大部分使用者並不清楚這個引數的含義以及對GC帶來的影響,G1會自適應處理這個引數相關的GC行為。
升級G1後可能需要關注的引數
-XX:MaxGCPauseMillis=N,G1暫停的目標時間(毫秒)
預設為200,很多使用者會刻意設小,通常情況下意義不大。G1實際的GC暫停任務,並不會隨著暫停時間縮小而變少,可以設小會導致更頻繁的GC影響吞吐。一般不需要設定,如果需要更好的吞吐,通常是設定更大,保持young區不會縮減的太小。也可以諮詢JVM答疑專家考慮調整-XX:NewSize和-XX:MaxNewSize,來保持young 區的Size,維持合適的吞吐效能。
-XX:InitiatingHeapOccupancyPercent=N -XX:-G1UseAdaptiveIHOP (電商核心使用)
這兩個引數通常同時使用。JDK11引入了G1UseAdaptiveIHOP來提升老區的利用率(大堆,通常大幾十G,或者100G以上)。在我們容器規格中等規模的heap(通常5-20g)的size中,有時會出現old gc過於頻繁或者young gc過於頻繁的現象。因此考慮一個合適的靜態IHOP(老區使用佔全堆比例觸發GC的閾值),會更加合適。
-XX:G1HeapRegionSize,(電商核心通常使用8m-32m)
設定G1 region的大小,應對humongous物件(超過heap region size一半獨佔一個或多個region)引起的GC異常。Heap region size預設為Heap size/2048,如果預設值過小,humongous物件分配過多,容易引起To-space exhausted的異常暫停時間:
[2024-01-05T14:14:31.817+0800] GC(266) To-space exhausted
-XX:G1HeapWastePercent,(預設5,部分電商核心應用設定為2)
G1在回收老區物件時,可以允許5% heap size的垃圾物件不回收,來減少mixed GC的暫停開銷。當Xmx10G時,5%就有500m的空間,對於Java heap是一種浪費,因此可以考慮減少heap空間浪費設定成2。不建議設定成0,可能會極大增加mixed GC的暫停。
-XX:G1MixedGCCountTarget,Mixed GC目標次數,預設為8
實際的Mixed GC次數通常會小於G1MixedGCCountTarget,如果Concurrent mark/mixed gc的週期並不頻繁,單次mixed gc的暫停過長,通常可以考慮增大這個引數,例如16,來分散單次mixed GC暫停的工作量,減少暫停時間。
升級G1的常見問題
CMS升級G1後,容器和Java程序記憶體佔用變高
很多應用在升級JDK11,出現容器和Java程序記憶體整體變高的現象,主要源自Heap的使用率差異。CMS的Old generation為非移動式,由 CMSInitiatingOccupancyFraction 來控制使用比例來觸發gc,因此應用啟動後短時間內,heap old區使用率不會上升。而G1的heap region是鬆散管理,整體利用heap,所以顯得記憶體使用率高。本質是一個heap利用率的問題,cms初始留著部分heap不用。這個問題可以透過調低Xmx來解決(部分電商核心使用這個方案)。
GC日誌中To-space exhausted引起的異常暫停
絕大部分是由於大物件分配過多,GC日誌中頻繁出現
Pause Young (Concurrent Start) (G1 Humongous Allocation)
大物件分配過多,會導致堆空間快速被佔滿,GC是出現To-space exhausted/evacuation failure,需要額外的暫停時間處理,甚至出現更耗時的Full GC全堆整理。
GC過於頻繁
相比傳統的CMS/Parallel GC,固定的young 區size。G1的young區size是自動調整的,當爲了滿足暫停要求時,會縮小young區,導致GC頻率過高。一般的情況是避免MaxGCPauseMillis設定過小,參考上面引數的介紹。或者增大MaxGCPauseMillis的配置,同時有必要的話諮詢答疑專家,調整-XX:NewSize和-XX:MaxNewSize。
Mixed GC暫停過長
G1除了整理清除young區物件的young GC,還有在Concurrent mark之後,包含整理老區物件的mixed gc。因此通常mixed GC會有更長的暫停時間。如果單次mixed GC暫停過長,考慮增大上面介紹的引數G1MixedGCCountTarget,來進一步分散老區物件整理的任務,降低暫停
四、升級效果
日常執行
可以看到在日常執行中,G1的垃圾回收耗時也有不錯的提升
壓測效果
相同壓測條件下TPS20
可以明顯看到GC耗時降低了不少,速度快了70%左右
線上執行情況
從圖中可以看到YGC的耗時明顯縮短,效能將近提升50%!這歸功於分代收集的能力
|
|
|
|
|
|
|
|
|
|
|
|
五、JDK11新玩法
字串String加強
String str = " i am lzc "; boolean isblank = str.isBlank(); //判斷字串是空白 boolean isempty = str.isEmpty(); //判斷字串是否為空 String result1 = str.strip(); //首位空白 String result2 = str.stripTrailing(); //去除尾部空白 String result3 = str.stripLeading(); //去除首部空白 String copyStr = str.repeat(2); //複製幾遍字串 long lineCount = str.lines().count(); //行數統計 System.out.println(isblank); //結果:false System.out.println(isempty); //結果:false System.out.println(result1); //結果:i am lzc System.out.println(result2); //結果: i am lzc System.out.println(result3); //結果:i am lzc System.out.println(copyStr); //結果: i am lzc i am lzc System.out.println(lineCount); //結果:1
檔案Files方法加強
Path filePath = Files.writeString(Path.of("/temp/a.txt"), "Sample text"); String fileContent = Files.readString(filePath); System.out.println(fileContent.equals("Sample text"));
數據流Stream方法加強
//Stream,允許接受一個null值,計算count時,返回0 long count = Stream.ofNullable(null).count(); System.out.println(count); // 0 //方法都接受一個謂詞來決定從流中放棄哪些元素 //通俗理解:從集合中刪除滿足條件的元素,直到不滿足為止 List list1 = Stream.of(1, 2, 3, 2, 1) .dropWhile(n -> n < 3) .collect(Collectors.toList()); System.out.println(list1); // [3, 2, 1] //方法都接受一個謂詞來決定從流中選用哪些元素 //通俗理解:從集合中提取滿足條件的元素,直到不滿足為止 List list2 = Stream.of(1, 2, 3, 2, 1) .takeWhile(n -> n < 3) .collect(Collectors.toList()); System.out.println(list2); // [1, 2]
集合List、Map等方法加強
List list1 = List.of(1, 3, 5, 7); List list2 = List.copyOf(list1); System.out.println(list2); //結果: [1,3,5,7] Map<Integer, String> map1 = Map.of(1, "a", 2, "b", 3, "c"); Map<Integer, String> map2 = Map.copyOf(map1); System.out.println(map2); //結果: {1=a, 2=b, 3=c}
optional加強
//新增orElseThrow,為空時拋異常 Object v2 = Optional.ofNullable(null).orElseThrow(); //結果:拋異常 //新增ifPresentOrElse,不為null執行第1個回撥函式,為null時執行第2個回撥函式 Optional.ofNullable(null).ifPresentOrElse( (x) -> { System.out.println("資料:" + x); }, () -> { System.out.println("資料不存在"); }); //提供另一個Optionals 作為空Optionals的回撥 Object v3 = Optional.ofNullable(null) .or(() -> Optional.of("fallback")) .get(); //結果:fallback System.out.println(v3);
HTTP Client
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uri)) .build(); // 非同步 client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println) .join(); // 同步 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());