做網(wǎng)站的公司經(jīng)營范圍怎么寫微信小程序怎么做
1 GraalVM
1.1 什么是GraalVM
GraalVM是Oracle官方推出的一款高性能JDK,使用它享受比OpenJDK或者OracleJDK更好的性能。
GraalVM的官方網(wǎng)址:https://www.graalvm.org/
官方標(biāo)語:Build faster, smaller, leaner applications。
更低的CPU、內(nèi)存使用率
官方標(biāo)語:Build faster, smaller, leaner applications。
-
更低的CPU、內(nèi)存使用率
-
更快的啟動(dòng)速度,無需預(yù)熱即可獲得最好的性能
-
更好的安全性、更小的可執(zhí)行文件
-
支持多種框架Spring Boot、Micronaut、Helidon 和 Quarkus。
-
多家云平臺(tái)支持。
-
通過Truffle框架運(yùn)行JS、Python、Ruby等其他語言。
GraalVM分為社區(qū)版(Community Edition)和企業(yè)版(Enterprise Edition)。企業(yè)版相比較社區(qū)版,在性能上有更多的優(yōu)化。
特性 | 描述 | 社區(qū)版 | 企業(yè)版 |
收費(fèi) | 是否收費(fèi) | 免費(fèi) | 收費(fèi) |
G1垃圾回收器 | 使用G1垃圾回收器優(yōu)化垃圾回收性能 | × | √ |
Profile Guided Optimization(PGO) | 運(yùn)行過程中收集動(dòng)態(tài)數(shù)據(jù),進(jìn)一步優(yōu)化本地鏡像的性能 | × | √ |
高級(jí)優(yōu)化特性 | 更多優(yōu)化技術(shù),降低內(nèi)存和垃圾回收的開銷 | × | √ |
高級(jí)優(yōu)化參數(shù) | 更多的高級(jí)優(yōu)化參數(shù)可以設(shè)置 | × | √ |
需求:
搭建Linux下的GraalVM社區(qū)版本環(huán)境。
步驟:
1、使用arch查看Linux架構(gòu)
2、根據(jù)架構(gòu)下載社區(qū)版的GraalVM:https://www.graalvm.org/downloads/
3、安裝GraalVM,安裝方式與安裝JDK相同
解壓文件:
設(shè)置環(huán)境變量:
4、使用java -version和HelloWorld測試GraalVM。
1.2 GraalVM的兩種運(yùn)行模式
-
JIT( Just-In-Time )模式 ,即時(shí)編譯模式
-
AOT(Ahead-Of-Time)模式 ,提前編譯模式
JIT模式的處理方式與Oracle JDK類似,滿足兩個(gè)特點(diǎn):
Write Once,Run Anywhere -> 一次編寫,到處運(yùn)行。
預(yù)熱之后,通過內(nèi)置的Graal即時(shí)編譯器優(yōu)化熱點(diǎn)代碼,生成比Hotspot JIT更高性能的機(jī)器碼。
需求:
分別在JDK8 、 JDK21 、 GraalVM 21 Graal即時(shí)編譯器、GraalVM 21 不開啟Graal即時(shí)編譯器運(yùn)行Jmh性能測試用例,對(duì)比其性能。
步驟:
1、在代碼文件夾中找到GraalVM的案例代碼,將java-simple-stream-benchmark文件夾下的代碼使用maven打包成jar包。
2、將jar包上傳到服務(wù)器,使用不同的JDK進(jìn)行測試,對(duì)比結(jié)果。
注意:
-XX:-UseJVMCICompiler參數(shù)可以關(guān)閉GraalVM中的Graal編譯器。
GraalVM開啟Graal編譯器下的性能還是不錯(cuò)的:
AOT(Ahead-Of-Time)模式 ,提前編譯模式
AOT 編譯器通過源代碼,為特定平臺(tái)創(chuàng)建可執(zhí)行文件。比如,在Windows下編譯完成之后,會(huì)生成exe文件。通過這種方式,達(dá)到啟動(dòng)之后獲得最高性能的目的。但是不具備跨平臺(tái)特性,不同平臺(tái)使用需要單獨(dú)編譯。
這種模式生成的文件稱之為Native Image本地鏡像。
需求: 使用GraalVM AOT模式制作本地鏡像并運(yùn)行。
步驟: 1、安裝Linux環(huán)境本地鏡像制作需要的依賴庫:
https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites
2、使用 native-image 類名 制作本地鏡像。
3、運(yùn)行本地鏡像可執(zhí)行文件。
社區(qū)版的GraalVM使用本地鏡像模式性能不如Hotspot JVM的JIT模式,但是企業(yè)版的性能相對(duì)會(huì)高很多。
1.3 應(yīng)用場景
GraalVM的AOT模式雖然在啟動(dòng)速度、內(nèi)存和CPU開銷上非常有優(yōu)勢,但是使用這種技術(shù)會(huì)帶來幾個(gè)問題:
1、跨平臺(tái)問題,在不同平臺(tái)下運(yùn)行需要編譯多次。編譯平臺(tái)的依賴庫等環(huán)境要與運(yùn)行平臺(tái)保持一致。
2、使用框架之后,編譯本地鏡像的時(shí)間比較長,同時(shí)也需要消耗大量的CPU和內(nèi)存。
3、AOT 編譯器在編譯時(shí),需要知道運(yùn)行時(shí)所有可訪問的所有類。但是Java中有一些技術(shù)可以在運(yùn)行時(shí)創(chuàng)建類,例如反射、動(dòng)態(tài)代理等。這些技術(shù)在很多框架比如Spring中大量使用,所以框架需要對(duì)AOT編譯器進(jìn)行適配解決類似的問題。
解決方案:
1、使用公有云的Docker等容器化平臺(tái)進(jìn)行在線編譯,確保編譯環(huán)境和運(yùn)行環(huán)境是一致的,同時(shí)解決了編譯資源問題。
2、使用SpringBoot3等整合了GraalVM AOT模式的框架版本。
1.3.1 SpringBoot搭建GraalVM應(yīng)用
需求:
SpringBoot3對(duì)GraalVM進(jìn)行了完整的適配,所以編寫GraalVM服務(wù)推薦使用SpringBoot3。
步驟:
1、使用 https://start.spring.io/ spring提供的在線生成器構(gòu)建項(xiàng)目。
2、編寫業(yè)務(wù)代碼,修改原代碼將PostConstructor
注解去掉:
@Service
public class UserServiceImpl implements UserService, InitializingBean {private List<User> users = new ArrayList<>();@Autowiredprivate UserDao userDao;@Overridepublic List<UserDetails> getUserDetails() {return userDao.findUsers();}@Overridepublic List<User> getUsers() {return users;}@Overridepublic void afterPropertiesSet() throws Exception {//初始化時(shí)生成數(shù)據(jù)for (int i = 1; i <= 10; i++) {users.add(new User((long) i, RandomStringUtils.randomAlphabetic(10)));}}
}
3、執(zhí)行 mvn -Pnative clean native:compile 命令生成本地鏡像。
4、運(yùn)行本地鏡像。
什么場景下需要使用GraalVM呢?
1、對(duì)性能要求比較高的場景,可以選擇使用收費(fèi)的企業(yè)版提升性能。
2、公有云的部分服務(wù)是按照CPU和內(nèi)存使用量進(jìn)行計(jì)費(fèi)的,使用GraalVM可以有效地降低費(fèi)用。
1.3.2 函數(shù)計(jì)算
傳統(tǒng)的系統(tǒng)架構(gòu)中,服務(wù)器等基礎(chǔ)設(shè)施的運(yùn)維、安全、高可用等工作都需要企業(yè)自行完成,存在兩個(gè)主要問題:
1、開銷大,包括了人力的開銷、機(jī)房建設(shè)的開銷。
2、資源浪費(fèi),面對(duì)一些突發(fā)的流量沖擊,比如秒殺等活動(dòng),必須提前規(guī)劃好容量準(zhǔn)備好大量的服務(wù)器,這些服務(wù)器在其他時(shí)候會(huì)處于閑置的狀態(tài),造成大量的浪費(fèi)。
Serverless架構(gòu)
隨著虛擬化技術(shù)、云原生技術(shù)的愈發(fā)成熟,云服務(wù)商提供了一套稱為Serverless無服務(wù)器化的架構(gòu)。企業(yè)無需進(jìn)行服務(wù)器的任何配置和部署,完全由云服務(wù)商提供。比較典型的有亞馬遜AWS、阿里云等。
Serverless架構(gòu)中第一種常見的服務(wù)是函數(shù)計(jì)算(Function as a Service),將一個(gè)應(yīng)用拆分成多個(gè)函數(shù),每個(gè)函數(shù)會(huì)以事件驅(qū)動(dòng)的方式觸發(fā)。典型代表有AWS的Lambda、阿里云的FC。
函數(shù)計(jì)算主要應(yīng)用場景有如下幾種:
① 小程序、API服務(wù)中的接口,此類接口的調(diào)用頻率不高,使用常規(guī)的服務(wù)器架構(gòu)容易產(chǎn)生資源浪費(fèi),使用Serverless就可以實(shí)現(xiàn)按需付費(fèi)降低成本,同時(shí)支持自動(dòng)伸縮能應(yīng)對(duì)流量的突發(fā)情況。
② 大規(guī)模任務(wù)的處理,比如音視頻文件轉(zhuǎn)碼、審核等,可以利用事件機(jī)制當(dāng)文件上傳之后,自動(dòng)觸發(fā)對(duì)應(yīng)的任務(wù)。
函數(shù)計(jì)算的計(jì)費(fèi)標(biāo)準(zhǔn)中包含CPU和內(nèi)存使用量,所以使用GraalVM AOT模式編譯出來的本地鏡像可以節(jié)省更多的成本。
步驟:
1、在項(xiàng)目中編寫Dockerfile文件。
# Using Oracle GraalVM for JDK 17
FROM container-registry.oracle.com/graalvm/native-image:17-ol8 AS builder# Set the working directory to /home/app
WORKDIR /build# Copy the source code into the image for building
COPY . /build
RUN chmod 777 ./mvnw# Build
RUN ./mvnw --no-transfer-progress native:compile -Pnative# The deployment Image
FROM container-registry.oracle.com/os/oraclelinux:8-slimEXPOSE 8080# Copy the native executable into the containers
COPY --from=builder /build/target/spring-boot-3-native-demo app
ENTRYPOINT ["/app"]
2、使用服務(wù)器制作鏡像,這一步會(huì)消耗大量的CPU和內(nèi)存資源,同時(shí)GraalVM相關(guān)的鏡像服務(wù)器在國外,建議使用阿里云的鏡像服務(wù)器制作Docker鏡像。
3、使用函數(shù)計(jì)算將Docker鏡像轉(zhuǎn)換成函數(shù)服務(wù)。
配置觸發(fā)器:
4、綁定域名并進(jìn)行測試。
需要準(zhǔn)備一個(gè)自己的域名:
配置接口路徑:
會(huì)出現(xiàn)一個(gè)錯(cuò)誤:
把域名導(dǎo)向阿里云的域名:
測試成功:
1.3.3 Serverless應(yīng)用
函數(shù)計(jì)算的服務(wù)資源比較受限,比如AWS的Lambda服務(wù)一般無法支持超過15分鐘的函數(shù)執(zhí)行,所以云服務(wù)商提供了另外一套方案:基于容器的Serverless應(yīng)用,無需手動(dòng)配置K8s中的Pod、Service等內(nèi)容,只需選擇鏡像就可自動(dòng)生成應(yīng)用服務(wù)。
同樣,Serverless應(yīng)用的計(jì)費(fèi)標(biāo)準(zhǔn)中包含CPU和內(nèi)存使用量,所以使用GraalVM AOT模式編譯出來的本地鏡像可以節(jié)省更多的成本。
服務(wù)分類 | 交付模式 | 彈性效率 | 計(jì)費(fèi)模式 |
函數(shù)計(jì)算 | 函數(shù) | 毫秒級(jí) | 調(diào)用次數(shù) CPU內(nèi)存使用量 |
Serverless應(yīng)用 | 鏡像容器 | 秒級(jí) | CPU內(nèi)存使用量 |
步驟:
1、在項(xiàng)目中編寫Dockerfile文件。
2、使用服務(wù)器制作鏡像,這一步會(huì)消耗大量的CPU和內(nèi)存資源,同時(shí)GraalVM相關(guān)的鏡像服務(wù)器在國外,建議使用阿里云的鏡像服務(wù)器制作Docker鏡像。
前兩步同實(shí)戰(zhàn)案例2
3、配置Serverless應(yīng)用,選擇容器鏡像、CPU和內(nèi)存。
4、綁定外網(wǎng)負(fù)載均衡并使用Postman進(jìn)行測試。
先別急著點(diǎn)確定,需要先創(chuàng)建彈性公網(wǎng)IP:
全選默認(rèn),然后創(chuàng)建:
創(chuàng)建SLB負(fù)載均衡:
這次就可以成功創(chuàng)建了:
綁定剛才創(chuàng)建的SLB負(fù)載均衡:
訪問公網(wǎng)IP就能處理請(qǐng)求了:
1.4 參數(shù)優(yōu)化和故障診斷
由于GraalVM是一款獨(dú)立的JDK,所以大部分HotSpot中的虛擬機(jī)參數(shù)都不適用。常用的參數(shù)參考:官方手冊(cè)。
-
社區(qū)版只能使用串行垃圾回收器(Serial?GC),使用串行垃圾回收器的默認(rèn)最大 Java 堆大小會(huì)設(shè)置為物理內(nèi)存大小的 80%,調(diào)整方式為使用 -Xmx最大堆大小。如果希望在編譯期就指定該大小,可以在編譯時(shí)添加參數(shù)-R:MaxHeapSize=最大堆大小。
-
G1垃圾回收器只能在企業(yè)版中使用,開啟方式為添加--gc=G1參數(shù),有效降低垃圾回收的延遲。
-
另外提供一個(gè)Epsilon?GC,開啟方式:--gc=epsilon ,它不會(huì)產(chǎn)生任何的垃圾回收行為所以沒有額外的內(nèi)存、CPU開銷。如果在公有云上運(yùn)行的程序生命周期短暫不產(chǎn)生大量的對(duì)象,可以使用該垃圾回收器,以節(jié)省最大的資源。
-XX:+PrintGC -XX:+VerboseGC 參數(shù)打印垃圾回收詳細(xì)信息。
添加虛擬機(jī)參數(shù):
打印出了垃圾回收的信息:
1.4.1 實(shí)戰(zhàn)案例4:內(nèi)存快照文件的獲取
需求:
獲得運(yùn)行中的內(nèi)存快照文件,使用MAT進(jìn)行分析。
步驟:
1、編譯程序時(shí),添加 --enable-monitoring=heapdump,參數(shù)添加到pom文件的對(duì)應(yīng)插件中。
<plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><buildArgs><arg>--enable-monitoring=heapdump,jfr</arg></buildArgs></configuration>
</plugin>
2、運(yùn)行中使用 kill -SIGUSR1 進(jìn)程ID 命令,創(chuàng)建內(nèi)存快照文件。
3、使用MAT分析內(nèi)存快照文件。
1.4.2 實(shí)戰(zhàn)案例5:運(yùn)行時(shí)數(shù)據(jù)的獲取
JDK Flight Recorder (JFR) 是一個(gè)內(nèi)置于 JVM 中的工具,可以收集正在運(yùn)行中的 Java 應(yīng)用程序的診斷和分析數(shù)據(jù),比如線程、異常等內(nèi)容。GraalVM本地鏡像也支持使用JFR生成運(yùn)行時(shí)數(shù)據(jù),導(dǎo)出的數(shù)據(jù)可以使用VisualVM分析。
步驟:
1、編譯程序時(shí),添加 --enable-monitoring=jfr,參數(shù)添加到pom文件的對(duì)應(yīng)插件中。
<plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><buildArgs><arg>--enable-monitoring=heapdump,jfr</arg></buildArgs></configuration>
</plugin>
2、運(yùn)行程序,添加 -XX:StartFlightRecording=filename=recording.jfr,duration=10s參數(shù)。
3、使用VisualVM分析JFR記錄文件。
2 新一代的GC
2.1 垃圾回收器的技術(shù)演進(jìn)
不同的垃圾回收器設(shè)計(jì)的目標(biāo)是不同的,如下圖所示:
2.2 Shenandoah GC
Shenandoah 是由Red Hat開發(fā)的一款低延遲的垃圾收集器,Shenandoah 并發(fā)執(zhí)行大部分 GC 工作,包括并發(fā)的整理,堆大小對(duì)STW的時(shí)間基本沒有影響。
1、下載。Shenandoah只包含在OpenJDK中,默認(rèn)不包含在內(nèi)需要單獨(dú)構(gòu)建,可以直接下載構(gòu)建好的。 下載地址:https://builds.shipilev.net/openjdk-jdk-shenandoah/
選擇方式如下:
{aarch64, arm32-hflt, mipsel, mips64el, ppc64le, s390x, x86_32, x86_64}:架構(gòu),使用arch命令選擇對(duì)應(yīng)的的架構(gòu)。
{server,zero}:虛擬機(jī)類型,選擇server,包含所有GC的功能。
{release, fastdebug, Slowdebug, optimization}:不同的優(yōu)化級(jí)別,選擇release,性能最高。
{gcc*-glibc*, msvc*}:編譯器的版本,選擇較高的版本性能好一些,如果兼容性有問題(無法啟動(dòng)),選擇較低的版本。
2、配置。將OpenJDK配置到環(huán)境變量中,使用java –version進(jìn)行測試。打印出如下內(nèi)容代表成功。
3、添加參數(shù),運(yùn)行Java程序。
-XX:+UseShenandoahGC 開啟Shenandoah GC
-Xlog:gc 打印GC日志
/** Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved.* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.** This code is free software; you can redistribute it and/or modify it* under the terms of the GNU General Public License version 2 only, as* published by the Free Software Foundation. Oracle designates this* particular file as subject to the "Classpath" exception as provided* by Oracle in the LICENSE file that accompanied this code.** This code is distributed in the hope that it will be useful, but WITHOUT* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License* version 2 for more details (a copy is included in the LICENSE file that* accompanied this code).** You should have received a copy of the GNU General Public License version* 2 along with this work; if not, write to the Free Software Foundation,* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.** Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA* or visit www.oracle.com if you need additional information or have any* questions.*/package org.sample;import com.sun.management.OperatingSystemMXBean;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;//執(zhí)行5輪預(yù)熱,每次持續(xù)2秒
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
//輸出毫秒單位
@OutputTimeUnit(TimeUnit.MILLISECONDS)
//統(tǒng)計(jì)方法執(zhí)行的平均耗時(shí)
@BenchmarkMode(Mode.AverageTime)
//java -jar benchmarks.jar -rf json
@State(Scope.Benchmark)
public class MyBenchmark {//每次測試對(duì)象大小 4KB和4MB@Param({"4","4096"})int perSize;private void test(Blackhole blackhole){//每次循環(huán)創(chuàng)建堆內(nèi)存60%對(duì)象 JMX獲取到Java運(yùn)行中的實(shí)時(shí)數(shù)據(jù)MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();//獲取堆內(nèi)存大小MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();//獲取到剩余的堆內(nèi)存大小long heapSize = (long) ((heapMemoryUsage.getMax() - heapMemoryUsage.getUsed()) * 0.6);//計(jì)算循環(huán)次數(shù)long size = heapSize / (1024 * perSize);for (int i = 0; i < 4; i++) {List<byte[]> objects = new ArrayList<>((int)size);for (int j = 0; j < size; j++) {objects.add(new byte[1024 * perSize]);}blackhole.consume(objects);}}@Benchmark@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseSerialGC"})public void serialGC(Blackhole blackhole){test(blackhole);}@Benchmark@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseParallelGC"})public void parallelGC(Blackhole blackhole){test(blackhole);}@Benchmark@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g"})public void g1(Blackhole blackhole){test(blackhole);}@Benchmark@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseShenandoahGC"})public void shenandoahGC(Blackhole blackhole){test(blackhole);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).forks(1).build();new Runner(opt).run();}
}
測試結(jié)果:
Shenandoah GC對(duì)小對(duì)象的GC停頓很短,但是大對(duì)象效果不佳。
2.3 ZGC
ZGC 是一種可擴(kuò)展的低延遲垃圾回收器。ZGC 在垃圾回收過程中,STW的時(shí)間不會(huì)超過一毫秒,適合需要低延遲的應(yīng)用。支持幾百兆到16TB 的堆大小,堆大小對(duì)STW的時(shí)間基本沒有影響。
ZGC降低了停頓時(shí)間,能降低接口的最大耗時(shí),提升用戶體驗(yàn)。但是吞吐量不佳,所以如果Java服務(wù)比較關(guān)注QPS(每秒的查詢次數(shù))那么G1是比較不錯(cuò)的選擇。
ZGC版本更迭
ZGC的使用
OracleJDK和OpenJDK中都支持ZGC,阿里的DragonWell龍井JDK也支持ZGC但屬于其自行對(duì)OpenJDK 11的ZGC進(jìn)行優(yōu)化的版本。
建議使用JDK17之后的版本,延遲較低同時(shí)無需手動(dòng)配置并行線程數(shù)。
分代 ZGC添加如下參數(shù)啟用 -XX:+UseZGC -XX:+ZGenerational
非分代 ZGC通過命令行選項(xiàng)啟用 -XX:+UseZGC
ZGC的環(huán)境搭建
ZGC在設(shè)計(jì)上做到了自適應(yīng),根據(jù)運(yùn)行情況自動(dòng)調(diào)整參數(shù),讓用戶手動(dòng)配置的參數(shù)最少化。
-
自動(dòng)設(shè)置年輕代大小,無需設(shè)置-Xmn參數(shù)。
自動(dòng)晉升閾值(復(fù)制中存活多少次才搬運(yùn)到老年代),無需設(shè)置-XX:TenuringThreshold。
JDK17之后支持自動(dòng)的并行線程數(shù),無需設(shè)置-XX:ConcGCThreads。
-
需要設(shè)置的參數(shù):
??-Xmx 值 最大堆內(nèi)存大小
??這是ZGC最重要的一個(gè)參數(shù),必須設(shè)置。ZGC在運(yùn)行過程中會(huì)使用一部分內(nèi)存用來處理垃圾回收,所以盡量保證堆中有足夠的空間。設(shè)置多少值取決于對(duì)象分配的速度,根據(jù)測試情況來決定。
-
可以設(shè)置的參數(shù):
??-XX:SoftMaxHeapSize=值
??ZGC會(huì)盡量保證堆內(nèi)存小于該值,這樣在內(nèi)存靠近這個(gè)值時(shí)會(huì)盡早地進(jìn)行垃圾回收,但是依然有可能會(huì)超過該值。
??例如,-Xmx5g -XX:SoftMaxHeapSize=4g 這個(gè)參數(shù)設(shè)置,ZGC會(huì)盡量保證堆內(nèi)存小于4GB,最多不會(huì)超過5GB。
@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseZGC","-XX:+UseLargePages"})
public void zGC(Blackhole blackhole){test(blackhole);
}@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseZGC","-XX:+ZGenerational","-XX:+UseLargePages"})
public void zGCGenerational(Blackhole blackhole){test(blackhole);
}
ZGC整體表現(xiàn)還是非常不錯(cuò)的,分代也讓ZGC的停頓時(shí)間有更好的表現(xiàn)。
ZGC調(diào)優(yōu)
ZGC 中可以使用Linux的Huge Page大頁技術(shù)優(yōu)化性能,提升吞吐量、降低延遲。
注意:安裝過程需要 root 權(quán)限,所以ZGC默認(rèn)沒有開啟此功能。
操作步驟:
1、計(jì)算所需頁數(shù),Linux x86架構(gòu)中大頁大小為2MB,根據(jù)所需堆內(nèi)存的大小估算大頁數(shù)量。比如堆空間需要16G,預(yù)留2G(JVM需要額外的一些非堆空間),那么頁數(shù)就是18G / 2MB = 9216。
2、配置系統(tǒng)的大頁池以具有所需的頁數(shù)(需要root權(quán)限):
$ echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
3、添加參數(shù)-XX:+UseLargePages 啟動(dòng)程序進(jìn)行測試
2.4 實(shí)戰(zhàn)案例
需求:
Java服務(wù)中存在大量軟引用的緩存導(dǎo)致內(nèi)存不足,測試下g1、Shenandoah、ZGC這三種垃圾回收器在這種場景下的回收情況。
步驟:
測試代碼:
package com.itheima.jvmoptimize.fullgcdemo;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.SneakyThrows;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;@RestController
@RequestMapping("/fullgc")
public class Demo2Controller {private Cache cache = Caffeine.newBuilder().weakKeys().softValues().build();private List<Object> objs = new ArrayList<>();private static final int _1MB = 1024 * 1024;//FULLGC測試//-Xms8g -Xmx8g -Xss256k -XX:MaxMetaspaceSize=512m -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/test.hprof -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps//ps + po 50并發(fā) 260ms 100并發(fā) 474 200并發(fā) 930//cms -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 50并發(fā) 157ms 200并發(fā) 833//g1 JDK11 并發(fā)200 248@GetMapping("/1")public void test() throws InterruptedException {cache.put(RandomStringUtils.randomAlphabetic(8),new byte[10 * _1MB]);}}
1、啟動(dòng)程序,添加不同的虛擬機(jī)參數(shù)進(jìn)行測試。
2、使用Apache Benchmark測試工具對(duì)本機(jī)進(jìn)行壓測。
3、生成GC日志,使用GcEasy進(jìn)行分析。
4、對(duì)比壓測之后的結(jié)果。
兩種垃圾回收器在并行回收時(shí)都會(huì)使用垃圾回收線程占用CPU資源
在內(nèi)存足夠的情況下,ZGC垃圾回收表現(xiàn)的效果會(huì)更好,停頓時(shí)間更短。
在內(nèi)存不是特別充足的情況下, Shenandoah GC表現(xiàn)更好,并行垃圾回收的時(shí)間較短,用戶請(qǐng)求的執(zhí)行效率比較高。
3 揭秘Java工具
在Java的世界中,除了Java編寫的業(yè)務(wù)系統(tǒng)之外,還有一類程序也需要Java程序員參與編寫,這類程序就是Java工具。
常見的Java工具有以下幾類:
1、診斷類工具,如Arthas、VisualVM等。
2、開發(fā)類工具,如Idea、Eclipse。
3、APM應(yīng)用性能監(jiān)測工具,如Skywalking、Zipkin等。
4、熱部署工具,如Jrebel等。
3.1 Java工具的核心:Java Agent技術(shù)
Java Agent技術(shù)是JDK提供的用來編寫Java工具的技術(shù),使用這種技術(shù)生成一種特殊的jar包,這種jar包可以讓Java程序運(yùn)行其中的代碼。
Java Agent技術(shù)實(shí)現(xiàn)了讓Java程序執(zhí)行獨(dú)立的Java Agent程序中的代碼,執(zhí)行方式有兩種:
-
靜態(tài)加載模式
靜態(tài)加載模式可以在程序啟動(dòng)的一開始就執(zhí)行我們需要執(zhí)行的代碼,適合用APM等性能監(jiān)測系統(tǒng)從一開始就監(jiān)控程序的執(zhí)行性能。靜態(tài)加載模式需要在Java Agent的項(xiàng)目中編寫一個(gè)premain的方法,并打包成jar包。
接下來使用以下命令啟動(dòng)Java程序,此時(shí)Java虛擬機(jī)將會(huì)加載agent中的代碼并執(zhí)行。
premain方法會(huì)在主線程中執(zhí)行:
-
動(dòng)態(tài)加載模式
動(dòng)態(tài)加載模式可以隨時(shí)讓java agent代碼執(zhí)行,適用于Arthas等診斷系統(tǒng)。動(dòng)態(tài)加載模式需要在Java Agent的項(xiàng)目中編寫一個(gè)agentmain的方法,并打包成jar包。
接下來使用以下代碼就可以讓java agent代碼在指定的java進(jìn)程中執(zhí)行了。
agentmain方法會(huì)在獨(dú)立線程中執(zhí)行:
3.1.1 搭建java agent靜態(tài)加載模式的環(huán)境
步驟:
1、創(chuàng)建maven項(xiàng)目,添加maven-assembly-plugin插件,此插件可以打包出java agent的jar包。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><configuration><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive><manifestFile>src/main/resources/MANIFEST.MF</manifestFile></archive></configuration>
</plugin>
2、編寫類和premain方法,premain方法中打印一行信息。
public class AgentDemo {/*** 參數(shù)添加模式 啟動(dòng)java主程序時(shí)添加 -javaangent:agent路徑* @param agentArgs* @param inst*/public static void premain(String agentArgs, Instrumentation inst) {System.out.println("java agent執(zhí)行了...");}
}
3、編寫MANIFEST.MF文件,此文件主要用于描述java agent的配置屬性,比如使用哪一個(gè)類的premain方法。
Manifest-Version: 1.0
Premain-Class: com.itheima.jvm.javaagent.AgentDemo
Agent-Class: com.itheima.jvm.javaagent.AgentDemo
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
4、使用maven-assembly-plugin進(jìn)行打包。
5、創(chuàng)建spring boot應(yīng)用,并靜態(tài)加載上一步打包完的java agent。
3.1.2 搭建java agent動(dòng)態(tài)加載模式的環(huán)境
步驟:
1、創(chuàng)建maven項(xiàng)目,添加maven-assembly-plugin插件,此插件可以打包出java agent的jar包。
2、編寫類和agentmain方法, agentmain方法中打印一行信息。
package com.itheima.jvm.javaagent.demo01;import java.lang.instrument.Instrumentation;public class AgentDemo {/*** 參數(shù)添加模式 啟動(dòng)java主程序時(shí)添加 -javaangent:agent路徑* @param agentArgs* @param inst*/public static void premain(String agentArgs, Instrumentation inst) {System.out.println("java agent執(zhí)行了...");}/*** attach 掛載模式 java主程序運(yùn)行之后,隨時(shí)可以將agent掛載上去*/public static void agentmain(String agentArgs, Instrumentation inst) {//打印線程名稱System.out.println(Thread.currentThread().getName());System.out.println("attach模式執(zhí)行了...");}
}
3、編寫MANIFEST.MF文件,此文件主要用于描述java agent的配置屬性,比如使用哪一個(gè)類的agentmain方法。
4、使用maven-assembly-plugin進(jìn)行打包。
5、編寫main方法,動(dòng)態(tài)連接到運(yùn)行中的java程序。
package com.itheima.jvm.javaagent.demo01;import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class AttachMain {public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {VirtualMachine vm = VirtualMachine.attach("24200");vm.loadAgent("D:\\jvm-java-agent\\target\\itheima-jvm-java-agent-jar-with-dependencies.jar");}
}
3.2 實(shí)戰(zhàn)案例1:簡化版的Arthas
需求:
編寫一個(gè)簡化版的Arthas程序,具備以下幾個(gè)功能:
1、查看內(nèi)存使用情況
2、生成堆內(nèi)存快照
3、打印棧信息
4、打印類加載器
5、打印類的源碼
6、打印方法執(zhí)行的參數(shù)和耗時(shí)
需求:
該程序是一個(gè)獨(dú)立的Jar包,可以應(yīng)用于任何Java編寫的系統(tǒng)中。
具備以下特點(diǎn):代碼無侵入性、操作簡單、性能高。
3.2.1 查看內(nèi)存使用情況
JDK從1.5開始提供了Java Management Extensions (JMX) 技術(shù),通過Mbean對(duì)象的寫入和獲取,實(shí)現(xiàn):
運(yùn)行時(shí)配置的獲取和更改
應(yīng)用程序運(yùn)行信息的獲取(線程棧、內(nèi)存、類信息等)
獲取JVM默認(rèn)提供的Mbean可以通過如下的方式,例如獲取內(nèi)存信息:
ManagementFactory提供了一系列的方法獲取各種各樣的信息:
package com.itheima.jvm.javaagent.demo02;import java.lang.instrument.Instrumentation;
import java.lang.management.*;
import java.util.List;/*** 1、查詢所有進(jìn)程* 2、顯示內(nèi)存相關(guān)的信息*/
public class AgentDemo {/*** 參數(shù)添加模式 啟動(dòng)java主程序時(shí)添加 -javaangent:agent路徑* @param agentArgs* @param inst*/public static void premain(String agentArgs, Instrumentation inst) {System.out.println("java agent執(zhí)行了...");}/*** attach 掛載模式 java主程序運(yùn)行之后,隨時(shí)可以將agent掛載上去*///-XX:+UseSerialGC -Xmx1g -Xms512mpublic static void agentmain(String agentArgs, Instrumentation inst) {//打印內(nèi)存的使用情況memory();}//獲取內(nèi)存信息private static void memory(){List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();System.out.println("堆內(nèi)存:");//獲取堆內(nèi)存getMemoryInfo(memoryPoolMXBeans, MemoryType.HEAP);//獲取非堆內(nèi)存System.out.println("非堆內(nèi)存:");getMemoryInfo(memoryPoolMXBeans, MemoryType.NON_HEAP);//nio使用的直接內(nèi)存try{@SuppressWarnings("rawtypes")Class bufferPoolMXBeanClass = Class.forName("java.lang.management.BufferPoolMXBean");@SuppressWarnings("unchecked")List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(bufferPoolMXBeanClass);for (BufferPoolMXBean mbean : bufferPoolMXBeans) {StringBuilder sb = new StringBuilder();sb.append("name:").append(mbean.getName()).append(" used:").append(mbean.getMemoryUsed()/ 1024 / 1024).append("m").append(" max:").append(mbean.getTotalCapacity() / 1024 / 1024).append("m");System.out.println(sb);}}catch (Exception e){System.out.println(e);}}private static void getMemoryInfo(List<MemoryPoolMXBean> memoryPoolMXBeans, MemoryType heap) {memoryPoolMXBeans.stream().filter(x -> x.getType().equals(heap)).forEach(x -> {StringBuilder sb = new StringBuilder();sb.append("name:").append(x.getName()).append(" used:").append(x.getUsage().getUsed() / 1024 / 1024).append("m").append(" max:").append(x.getUsage().getMax() / 1024 / 1024).append("m").append(" committed:").append(x.getUsage().getCommitted() / 1024 / 1024).append("m");System.out.println(sb);});}public static void main(String[] args) {memory();}
}
3.2.2 生成堆內(nèi)存快照
更多的信息可以通過ManagementFactory.getPlatformMXBeans獲取,比如:
通過這種方式,獲取到了Java虛擬機(jī)中分配的直接內(nèi)存和內(nèi)存映射緩沖區(qū)的大小。
獲取到虛擬機(jī)診斷用的MXBean,通過這個(gè)Bean對(duì)象可以生成內(nèi)存快照。
public static void heapDump(){SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm");String filename = simpleDateFormat.format(new Date()) + ".hprof";System.out.println("生成內(nèi)存dump文件,文件名為:" + filename);HotSpotDiagnosticMXBean hotSpotDiagnosticMXBean =ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);try {hotSpotDiagnosticMXBean.dumpHeap(filename, true);} catch (IOException e) {e.printStackTrace();}
}
3.2.3 打印棧信息
package com.itheima.jvm.javaagent.demo03;import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;public class ThreadCommand {public static void printStackInfo(){ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();ThreadInfo[] infos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported(),threadMXBean.isSynchronizerUsageSupported());for (ThreadInfo info : infos) {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("name:").append(info.getThreadName()).append(" threadId:").append(info.getThreadId()).append(" state:").append(info.getThreadState());System.out.println(stringBuilder);StackTraceElement[] stackTrace = info.getStackTrace();for (StackTraceElement stackTraceElement : stackTrace) {System.out.println(stackTraceElement.toString());}System.out.println();}}public static void main(String[] args) {printStackInfo();}
}
3.2.4 打印類加載器
Java Agent中可以獲得Java虛擬機(jī)提供的Instumentation對(duì)象:
該對(duì)象有以下幾個(gè)作用:
1、redefine,重新設(shè)置類的字節(jié)碼信息。
2、retransform,根據(jù)現(xiàn)有類的字節(jié)碼信息進(jìn)行增強(qiáng)。
3、獲取所有已加載的類信息。
Oracle官方手冊(cè): https://docs.oracle.com/javase/17/docs/api/java/lang/instrument/Instrumentation.html
package com.itheima.jvm.javaagent.demo04;import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
import org.jd.core.v1.api.loader.Loader;
import org.jd.core.v1.api.loader.LoaderException;
import org.jd.core.v1.api.printer.Printer;import java.lang.instrument.*;
import java.security.ProtectionDomain;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.stream.Collectors;public class ClassCommand {//獲取所有類加載器private static Set<ClassLoader> getAllClassLoader(Instrumentation inst){HashSet<ClassLoader> classLoaders = new HashSet<>();Class[] allLoadedClasses = inst.getAllLoadedClasses();for (Class clazz : allLoadedClasses) {ClassLoader classLoader = clazz.getClassLoader();classLoaders.add(classLoader);}return classLoaders;}public static void printAllClassLoader(Instrumentation inst){Set<ClassLoader> allClassLoader = getAllClassLoader(inst);String result = allClassLoader.stream().map(x -> {if (x ==null) {return "BootStrapClassLoader";} else {return x.toString();}}).distinct().sorted(String::compareTo).collect(Collectors.joining(","));System.out.println(result);}}
3.2.5 打印類的源碼
打印類的源碼需要分為以下幾個(gè)步驟
1、獲得內(nèi)存中的類的字節(jié)碼信息。利用Instrumentation提供的轉(zhuǎn)換器來獲取字節(jié)碼信息。
2、通過反編譯工具將字節(jié)碼信息還原成源代碼信息。
這里我們會(huì)使用jd-core依賴庫來完成,github地址:https://github.com/java-decompiler/jd-core
Pom添加依賴:
<dependency><groupId>org.jd</groupId><artifactId>jd-core</artifactId><version>1.1.3</version>
</dependency>
//獲取類信息
public static void printClass(Instrumentation inst){Scanner scanner = new Scanner(System.in);System.out.println("請(qǐng)輸入類名:");String next = scanner.next();Class[] allLoadedClasses = inst.getAllLoadedClasses();System.out.println("要查找的類名是:" + next);//匹配類名for (Class clazz : allLoadedClasses) {if(clazz.getName().equals(next)){System.out.println("找到了類,類加載器為:" + clazz.getClassLoader());ClassFileTransformer transformer = new ClassFileTransformer() {@Overridepublic byte[] transform(Module module, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {ClassFileToJavaSourceDecompiler classFileToJavaSourceDecompiler = new ClassFileToJavaSourceDecompiler();Printer printer = new Printer() {protected static final String TAB = " ";protected static final String NEWLINE = "\n";protected int indentationCount = 0;protected StringBuilder sb = new StringBuilder();@Override public String toString() { return sb.toString(); }@Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {}@Override public void end() {System.out.println(sb.toString());}@Override public void printText(String text) { sb.append(text); }@Override public void printNumericConstant(String constant) { sb.append(constant); }@Override public void printStringConstant(String constant, String ownerInternalName) { sb.append(constant); }@Override public void printKeyword(String keyword) { sb.append(keyword); }@Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { sb.append(name); }@Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { sb.append(name); }@Override public void indent() { this.indentationCount++; }@Override public void unindent() { this.indentationCount--; }@Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) sb.append(TAB); }@Override public void endLine() { sb.append(NEWLINE); }@Override public void extraLine(int count) { while (count-- > 0) sb.append(NEWLINE); }@Override public void startMarker(int type) {}@Override public void endMarker(int type) {}};try {classFileToJavaSourceDecompiler.decompile(new Loader() {@Overridepublic boolean canLoad(String s) {return false;}@Overridepublic byte[] load(String s) throws LoaderException {return classfileBuffer;}},printer,className);} catch (Exception e) {e.printStackTrace();}//System.out.println(new String(classfileBuffer));return ClassFileTransformer.super.transform(module, loader, className, classBeingRedefined, protectionDomain, classfileBuffer);}};inst.addTransformer(transformer,true);try {inst.retransformClasses(clazz);} catch (UnmodifiableClassException e) {e.printStackTrace();}finally {inst.removeTransformer(transformer);}}}
}
3.2.6 打印方法執(zhí)行的參數(shù)和耗時(shí)
Spring AOP是不是也可以實(shí)現(xiàn)類似的功能呢?
Spring AOP 確實(shí)可以實(shí)現(xiàn)統(tǒng)計(jì)方法執(zhí)行時(shí)間,打印方法參數(shù)等功能,但是使用這種方式存在幾個(gè)問題:
① 代碼有侵入性,AOP代碼必須在當(dāng)前項(xiàng)目中被引入才能完成相應(yīng)的功能。
② 無法做到靈活地開啟和關(guān)閉功能。
③ 與Spring框架強(qiáng)耦合,如果項(xiàng)目沒有使用Spring框架就不可以使用。
所以使用Java Agent技術(shù) + 字節(jié)碼增強(qiáng)技術(shù),就可以解決上述三個(gè)問題。
3.2.6.1 ASM字節(jié)碼增強(qiáng)技術(shù)
打印方法執(zhí)行的參數(shù)和耗時(shí)需要對(duì)原始類的方法進(jìn)行增強(qiáng),可以使用類似于Spring AOP這類面向切面編程的方式,但是考慮到并非每個(gè)項(xiàng)目都使用了Spring這些框架,所以我們選擇的是最基礎(chǔ)的字節(jié)碼增強(qiáng)框架。字節(jié)碼增強(qiáng)框架是在當(dāng)前類的字節(jié)碼信息中插入一部分字節(jié)碼指令,從而起到增強(qiáng)的作用。
ASM是一個(gè)通用的 Java 字節(jié)碼操作和分析框架。它可用于直接以二進(jìn)制形式修改現(xiàn)有類或動(dòng)態(tài)生成類。ASM重點(diǎn)關(guān)注性能。讓操作盡可能小且盡可能快,所以它非常適合在動(dòng)態(tài)系統(tǒng)中使用。ASM的缺點(diǎn)是代碼復(fù)雜。
ASM的官方網(wǎng)址:https://asm.ow2.io/
操作步驟:
1、引入依賴
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.6</version>
</dependency>
2、搭建基礎(chǔ)框架,此代碼為固定代碼。
3、編寫一個(gè)類描述如何去增強(qiáng)類,類需要繼承自MethodVisitor
ASM基礎(chǔ)案例:
package com.itheima.jvm.javaagent.demo05;import org.objectweb.asm.*;import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;import static org.objectweb.asm.Opcodes.*;public class ASMDemo {public static byte[] classASM(byte[] bytes){ClassWriter cw = new ClassWriter(0);// cv forwards all events to cwClassVisitor cv = new ClassVisitor(ASM7, cw) {@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);return new MyMethodVisitor(this.api,mv);}};ClassReader cr = new ClassReader(bytes);cr.accept(cv, 0);return cw.toByteArray();}public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {InputStream inputStream = ASMDemo.class.getResourceAsStream("/com/itheima/jvm/javaagent/demo05/ASMDemo.class");byte[] b1 = inputStream.readAllBytes();byte[] b2 = classASM(b1); // b2 represents the same class as b1//創(chuàng)建類加載器MyClassLoader myClassLoader = new MyClassLoader();Class clazz = myClassLoader.defineClass("com.itheima.jvm.javaagent.demo05.ASMDemo", b2);clazz.getDeclaredConstructor().newInstance();}
}class MyClassLoader extends ClassLoader {public Class defineClass(String name, byte[] b) {return defineClass(name, b, 0, b.length);}
}class MyMethodVisitor extends MethodVisitor {public MyMethodVisitor(int api, MethodVisitor methodVisitor) {super(api, methodVisitor);}@Overridepublic void visitCode() {mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");mv.visitLdcInsn("開始執(zhí)行");mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);super.visitCode();}@Overridepublic void visitInsn(int opcode) {if(opcode == ARETURN || opcode == RETURN ) {mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");mv.visitLdcInsn("結(jié)束執(zhí)行");mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);}super.visitInsn(opcode);}@Overridepublic void visitEnd() {mv.visitMaxs(20,50);super.visitEnd();}}
3.2.6.2 Byte Buddy字節(jié)碼增強(qiáng)技術(shù)
Byte Buddy 是一個(gè)代碼生成和操作庫,用于在 Java 應(yīng)用程序運(yùn)行時(shí)創(chuàng)建和修改 Java 類,而無需編譯器的幫助。 Byte Buddy底層基于ASM,提供了非常方便的 API。
Byte Buddy官網(wǎng): https://bytebuddy.net/
操作步驟:
1、引入依賴
<dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.14.10</version>
</dependency>
<dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy-agent</artifactId><version>1.14.10</version>
</dependency>
2、搭建基礎(chǔ)框架,此代碼為固定代碼
3、編寫一個(gè)Advice通知描述如何去增強(qiáng)類
package com.itheima.jvm.javaagent.demo05;import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;public class ByteBuddyDemo {public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Foo foo = new Foo();MyClassLoader myClassLoader = new MyClassLoader();Class<? extends Foo> newClazz = new ByteBuddy().subclass(Foo.class).method(ElementMatchers.any()).intercept(Advice.to(MyAdvice.class)).make().load(myClassLoader).getLoaded();Foo foo1 = newClazz.getDeclaredConstructor().newInstance();foo1.test();}
}class MyAdvice {@Advice.OnMethodEnterstatic void onEnter(){System.out.println("方法進(jìn)入");}@Advice.OnMethodExitstatic void onExit(){System.out.println("方法退出");}}
增強(qiáng)后的代碼:
package com.itheima.jvm.javaagent.demo05;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
import org.jd.core.v1.api.loader.Loader;
import org.jd.core.v1.api.loader.LoaderException;
import org.jd.core.v1.api.printer.Printer;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.Scanner;import static net.bytebuddy.matcher.ElementMatchers.isMethod;public class ClassEnhancerCommand {//獲取類信息public static void enhanceClass(Instrumentation inst){Scanner scanner = new Scanner(System.in);System.out.println("請(qǐng)輸入類名:");String next = scanner.next();Class[] allLoadedClasses = inst.getAllLoadedClasses();System.out.println("要查找的類名是:" + next);//匹配類名for (Class clazz : allLoadedClasses) {if(clazz.getName().equals(next)){System.out.println("找到了類,類加載器為:" + clazz.getClassLoader());new AgentBuilder.Default().disableClassFormatChanges().with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).with( //new AgentBuilder.Listener.WithErrorsOnly(new AgentBuilder.Listener.WithTransformationsOnly(AgentBuilder.Listener.StreamWriting.toSystemOut()))//.type(ElementMatchers.isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController"))).type(ElementMatchers.named(clazz.getName())).transform((builder, type, classLoader, module, protectionDomain) ->builder.visit(Advice.to(MyAdvice.class).on(ElementMatchers.any()))
// builder .method(ElementMatchers.any())
// .intercept(MethodDelegation.to(MyInterceptor.class))).installOn(inst);}}}
}
package com.itheima.jvm.javaagent.demo07;import net.bytebuddy.asm.Advice;class MyAdvice {@Advice.OnMethodEnterstatic long enter(@Advice.AllArguments Object[] ary) {if(ary != null) {for(int i =0 ; i < ary.length ; i++){System.out.println("Argument: " + i + " is " + ary[i]);}}return System.nanoTime();}@Advice.OnMethodExitstatic void exit(@Advice.Enter long value) {System.out.println("耗時(shí)為:" + (System.nanoTime() - value) + "納秒");}
}
最后將整個(gè)簡化版的arthas進(jìn)行打包,在服務(wù)器上進(jìn)行測試。使用maven-shade-plugin插件可以將所有依賴打入同一個(gè)jar包中并指定入口main方法。
<!--打包成jar包使用--><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>1.4</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><finalName>itheima-attach-agent</finalName><transformers><!--java -jar 默認(rèn)啟動(dòng)的主類--><transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>com.itheima.jvm.javaagent.AttachMain</mainClass></transformer></transformers></configuration></execution></executions>
</plugin>
3.3 實(shí)戰(zhàn)案例2:APM系統(tǒng)的數(shù)據(jù)采集
Application performance monitor (APM) 應(yīng)用程序性能監(jiān)控系統(tǒng)是采集運(yùn)行程序的實(shí)時(shí)數(shù)據(jù)并使用可視化的方式展示,使用APM可以確保系統(tǒng)可用性,優(yōu)化服務(wù)性能和響應(yīng)時(shí)間,持續(xù)改善用戶體驗(yàn)。常用的APM系統(tǒng)有Apache Skywalking、Zipkin等。
Skywalking官方網(wǎng)站: https://skywalking.apache.org/
需求:
編寫一個(gè)簡化版的APM數(shù)據(jù)采集程序,具備以下幾個(gè)功能:
1、無侵入性獲取spring boot應(yīng)用中,controller層方法的調(diào)用時(shí)間。
2、將所有調(diào)用時(shí)間寫入文件中。
問題:
Java agent 采用靜態(tài)加載模式 還是 動(dòng)態(tài)加載模式?
一般程序啟動(dòng)之后就需要持續(xù)地進(jìn)行信息的采集,所以采用靜態(tài)加載模式。
3.3.1 Java Agent參數(shù)的獲取
在Java Agent中,可以通過如下的方式傳遞參數(shù):
java -javaagent:./agent.jar=參數(shù) -jar test.jar
接下來通過premain參數(shù)中的agentArgs字段獲取:
如果有多個(gè)參數(shù),可以使用如下方式:
java -javaagent:./agent.jar=param1=value1,param2=value2 -jar test.jar
在Java代碼中使用字符串解析出對(duì)應(yīng)的key value。
在Java Agent中如果需要傳遞參數(shù)到Byte Buddy,可以采用如下的方式:
1、綁定Key Value,Key是一個(gè)自定義注解,Value是參數(shù)的值。
2、自定義注解
3、通過注解注入
代碼:
package com.itheima.javaagent;import com.itheima.javaagent.command.ClassCommand;
import com.itheima.javaagent.command.MemoryCommand;
import com.itheima.javaagent.command.ThreadCommand;
import com.itheima.javaagent.enhancer.AgentParam;
import com.itheima.javaagent.enhancer.MyAdvice;
import com.itheima.javaagent.enhancer.TimingAdvice;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;
import java.util.Scanner;public class AgentMain {//premain方法public static void premain(String agentArgs, Instrumentation inst){//使用bytebuddy增強(qiáng)類new AgentBuilder.Default()//禁止byte buddy處理時(shí)修改類名.disableClassFormatChanges()//處理時(shí)使用retransform增強(qiáng).with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)//打印出錯(cuò)誤日志.with(new AgentBuilder.Listener.WithTransformationsOnly(AgentBuilder.Listener.StreamWriting.toSystemOut()))//匹配哪些類.type(ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.web.bind.annotation.RestController").or(ElementMatchers.named("org.springframework.web.bind.annotation.Controller"))))//增強(qiáng),使用MyAdvice通知,對(duì)所有方法都進(jìn)行增強(qiáng).transform((builder, typeDescription, classLoader, module, protectionDomain) ->builder.visit(Advice.withCustomMapping().bind(AgentParam.class,agentArgs).to(TimingAdvice.class).on(ElementMatchers.any()))).installOn(inst);}}
package com.itheima.javaagent.enhancer;import net.bytebuddy.asm.Advice;
import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;//統(tǒng)計(jì)耗時(shí),打印方法名、類名
public class TimingAdvice {//方法進(jìn)入時(shí),返回開始時(shí)間@Advice.OnMethodEnterstatic long enter(){return System.nanoTime();}//方法退出時(shí)候,統(tǒng)計(jì)方法執(zhí)行耗時(shí)@Advice.OnMethodExitstatic void exit(@Advice.Enter long value,@Advice.Origin("#t") String className,@Advice.Origin("#m") String methodName,@AgentParam("agent.log") String fileName){String str = methodName + "@" + className + "耗時(shí)為: " + (System.nanoTime() - value) + "納秒\n";try {FileUtils.writeStringToFile(new File(fileName),str, StandardCharsets.UTF_8,true);} catch (IOException e) {e.printStackTrace();}}
}
修改jar包名字,并重新打包:
啟動(dòng)spring boot服務(wù)時(shí),添加javaagent的路徑,并添加文件名參數(shù):
打印結(jié)果:
3.3.2 總結(jié)
Arthas這款工具用到了什么Java技術(shù),有沒有了解過?
回答:
Arthas主要使用了Java Agent技術(shù),這種技術(shù)可以讓運(yùn)行中的Java程序執(zhí)行Agent中編寫代碼。
Arthas使用了Agent中的動(dòng)態(tài)加載模式,可以選擇讓某個(gè)特定的Java進(jìn)程加載Agent并執(zhí)行其中的監(jiān)控代碼。監(jiān)控方面主要使用的就是JMX提供的一些監(jiān)控指標(biāo),同時(shí)使用字節(jié)碼增強(qiáng)技術(shù),對(duì)某些類和某些方法進(jìn)行增強(qiáng),從而監(jiān)控方法的執(zhí)行耗時(shí)、參數(shù)等內(nèi)容。
APM系統(tǒng)是如何獲取到Java程序運(yùn)行中的性能數(shù)據(jù)的?
回答:
APM系統(tǒng)比如Skywalking主要使用了Java Agent技術(shù),這種技術(shù)可以讓運(yùn)行中的Java程序執(zhí)行Agent中編寫代碼。
Skywalking編寫了Java Agent,使用了Agent中的靜態(tài)加載模式,使用字節(jié)碼增強(qiáng)技術(shù),對(duì)某些類和某些方法進(jìn)行增強(qiáng),從而監(jiān)控方法的執(zhí)行耗時(shí)、參數(shù)等內(nèi)容。比如對(duì)Controller層方法增強(qiáng),獲取接口調(diào)用的時(shí)長信息,對(duì)數(shù)據(jù)庫連接增強(qiáng),獲取數(shù)據(jù)庫查詢的時(shí)長、SQL語句等信息。