現(xiàn)代java開發(fā)指南 第二部分
第二部分:部署适揉、監(jiān)控 & 管理紊婉,性能分析和基準(zhǔn)測試
第一部分,第二部分
歡迎來到現(xiàn)代 Java 開發(fā)指南第二部分韭畸。在第一部分中宇智,我們已經(jīng)展示了有關(guān) Java 新的語言特性,庫和工具胰丁。這些新的工具使 Java 變成了相當(dāng)輕量級的開發(fā)環(huán)境随橘,這個開發(fā)環(huán)境擁有新的構(gòu)建工具、更容易使用的文檔锦庸、富有表現(xiàn)力的代碼還有用戶級線程的并發(fā)机蔗。而在這部分中,我們將比代碼層次更高一層甘萧,討論 Java 的運(yùn)維———— Java 的部署萝嘁、監(jiān)控&管理,性能分析和基準(zhǔn)測試扬卷。盡管這里的例子都會用 Java 來做示意牙言,但是我們討論的內(nèi)容與所有的 JVM 語言都相關(guān),而不僅僅是 Java 語言邀泉。
在開始之前嬉挡,我想簡短地回答一下第一部分讀者的問題,并且澄清一下說的不清楚的地方汇恤。第一部分中最受爭議的地方出現(xiàn)在構(gòu)建工具這一節(jié)庞钢。在那一節(jié)中,我寫到現(xiàn)代的 Java 開發(fā)者使用 Gradle
因谎。有些讀者對此提出異議基括,并且舉出了例子來證明 Maven 同樣也是一個很好的工具。我個人喜歡 Gradle 漂亮 DSL 和能使用指令式代碼來編寫非通用的構(gòu)建操作财岔,同時我也能夠理解喜歡完全聲明式的 Maven 的偏好,即使這樣做需要大量的插件风皿。因此,我承認(rèn):現(xiàn)代的 Java 開發(fā)者可能更喜歡 Maven 而不是 Gradle 匠璧。我還想說桐款,雖然使用 Gradle 不用了解 Groovy ,甚至人們希望在不是那么標(biāo)準(zhǔn)的事情中也不用了解 Groovy 夷恍。但是我不會這樣魔眨,我從 Gradle 的在線例子中已經(jīng)學(xué)習(xí)了很多有用的 Groovy 的語句。
有些讀者指出我在第一部分的代碼示例中使用 Junit 和 Guava 酿雪,意味著我有意推廣它們遏暴。好吧,我確實有這樣的想法指黎。Guava 是一個非常有用的庫朋凉,而 JUnit 是一個很好的單元測試框架。雖然 TestNG 也很好醋安,但是 JUnit 非常常見杂彭,很少有人會選擇別的就算有優(yōu)勢的測試框架墓毒。
同樣,就示例代碼中測試使用 Hamcrest 盖灸,一個讀者指出 AssertJ蚁鳖,可能是一個比 Hamcrest 更好的選擇磺芭。
需要理解到本系列指南并不打算覆蓋到 Java 的方方面面赁炎,能認(rèn)識到這一點很重要。所以當(dāng)然會有很多很好的庫因為沒有在文章中出現(xiàn)钾腺,我們沒有去探索它們徙垫。我寫這份指南的本意就是給大家示意一下現(xiàn)代 Java 開發(fā)可能是什么樣的。
有些讀者表達(dá)了他們更喜歡短的 Javadoc 注釋放棒,這種注釋不必像 Javadoc 標(biāo)準(zhǔn)形式那樣需要把所有的字段都寫上姻报。如下面的例子:
/**
* This method returns the result.
* @return the result
*/
int getResult();
更喜歡這樣:
/**
* Returns the result
*/
int getResult();
我完全同意。我在例子中簡單示范了混合 Markdown 和標(biāo)準(zhǔn)的 Javadoc 標(biāo)簽的使用间螟。這只是用來展示如何使用吴旋,并不是意圖把這種使用方式當(dāng)成指導(dǎo)方針。
最后厢破,關(guān)于 Android 我有一些話要說荣瑟。 Android 系統(tǒng)通過一系列變換之后,能夠執(zhí)行用 java (還有可能是別的 JVM 語言)寫的代碼摩泪,但是 Android 不是 JVM笆焰,并且事實上 Android 無論在正式場合和實際使用中也不完全是 Java (造成這個問題的原因是兩個跨國公司,這里指谷歌和甲骨文见坑,沒有就 Java 的使用達(dá)成一個許可協(xié)議)嚷掠。正因為 Android 不完全是 Java ,所以在第一部分中討論的內(nèi)容對 Android 可能有用或者也可能沒有用荞驴,而且因為 Android 沒有包括 JVM 不皆,所以在這部分討論的內(nèi)容很少能應(yīng)用到 Android 上面。
好了熊楼,現(xiàn)在讓我們回到正文霹娄。
現(xiàn)代 Java 的打包和部署
對于不熟悉 Java 生態(tài)體系的人來說,Java(或者任何 JVM 語言)源文件孙蒙,被編繹成 .class
文件(本質(zhì)上是 Java 二進(jìn)制文件)项棠,每一個類一個文件。打包這些 class 文件的基本機(jī)制就把這些文件打包在一起(這項工作通常由構(gòu)建工具或者IDE來完成)放到JAR(Java存檔)文件挎峦,JAR 文件叫 Java 二進(jìn)制包香追。 JAR 文件僅僅是 Zip 壓縮文件,它包括 class 文件坦胶,還有一個附加的清單文件用來描述內(nèi)容透典,清單中還可以包括其它的關(guān)于分發(fā)的信息(如在被簽名的 JARs 中晴楔,清單可以包括數(shù)字簽名)。如果你打包一個應(yīng)用(與此相反是打包一個庫)到 JAR 中峭咒,清單文件應(yīng)該指出應(yīng)用的主類(也就是 main 函數(shù)所在類)税弃,在這種情況下,應(yīng)用通過命令java -jar app.jar
啟動凑队,我們稱這個 JAR 文件為可執(zhí)行的 JAR 则果。
Java 庫被打包成 JAR 文件,然后部署到 Maven 倉庫中(這個倉庫能被所有的 JVM 構(gòu)建工具使用漩氨,不僅僅是 Maven )西壮。 Maven 倉庫管理這些庫二進(jìn)制文件的版本和依賴(當(dāng)你發(fā)一個請求想從Maven倉庫中加載一個庫,此外你請求了該庫所有的依賴)叫惊。開源 Java 庫經(jīng)常托管在這個中央倉庫中款青,或者其它類似的公開倉庫中。并且組織機(jī)構(gòu)通過 Artifactory 或者 Nexus 等工具霍狰,管理他們私有 Maven 倉庫抡草。你甚至能在 GitHub 上建立自己的 Maven 倉庫。但是 Maven 倉庫在構(gòu)建過程中應(yīng)該能正常使用蔗坯,并且 Maven 倉庫通常托管庫形式 JAR 而不是可執(zhí)行的 JAR 康震。
Java 網(wǎng)站應(yīng)用傳統(tǒng)上應(yīng)該在應(yīng)用服務(wù)器(或者 servlet 容器)中執(zhí)行。這些容器能運(yùn)行多個網(wǎng)站應(yīng)用步悠,能按需加載或卸載應(yīng)用签杈。 Java 網(wǎng)站應(yīng)用以 WAR 的形式部署在 servlet 容器中。WAR 也是 JAR 文件鼎兽,它的內(nèi)容以某種標(biāo)準(zhǔn)形式排好答姥,并且包括額外的配置信息。但是谚咬,正如我們將在第三部分看到一樣鹦付,就現(xiàn)代 Java 開發(fā)而言, Java 應(yīng)用服務(wù)器已死择卦。
Java 桌面應(yīng)用經(jīng)常被打包成與平臺相關(guān)的二進(jìn)制文件敲长,還包括一個平臺相關(guān)的 JVM。 JDK 工具包中有一個打包工具來做這個事情(這里是講的是如何在 NetBeans 中使用它)秉继。第三方工具 Packer 也提供了類似的功能祈噪。對于游戲和桌面應(yīng)用來說,這種打包機(jī)非常好尚辑。但是對于服務(wù)器軟件來說辑鲤,這種打包機(jī)制就不是我想要的。此外杠茬,因為要打包一個 JVM 的拷貝月褥,這種機(jī)制不能以補(bǔ)丁形式安全和平滑地升級應(yīng)用弛随。
對服務(wù)器端代碼,我們想要的是一種簡單宁赤、輕量舀透、能自動的打包和部署的工具。這個工具最好能利用可執(zhí)行 JAR 的簡單和平臺無關(guān)性决左。但是可執(zhí)行 JAR 有幾個不足的地方愕够。每一個庫通常打包到各自的 JAR 文件中,然后和所有的依賴一起打包成單個 JAR 文件哆窿,這一過程可能造成沖突链烈,特別是已打包的資源庫(沒有 class
文件的庫)一起打包時。還有挚躯,一個原生庫在打包時不能直接放到 JAR 中。打包中可能最重要的是擦秽, JVM 配置信息(如 heap
的大新肜蟆)對用戶來說是遺漏的,這個工作必須在命令行下才能做感挥。像 Maven’s Shade plugin 和 Gradle’s Shadow plugin 等工具缩搅,解決了資源沖突的問題,而 One-Jar 支持原生的庫触幼,但是這些工具都可能對應(yīng)用產(chǎn)生影響硼瓣,而且也沒有解決 JVM 參數(shù)配置的問題。 Gradle 能把應(yīng)用打包成一個 ZIP 文件置谦,并且產(chǎn)生一個與系統(tǒng)相關(guān)的啟腳本去配置 JVM 堂鲤,但是這種方法要求安裝應(yīng)用。我們可以做的比這樣更輕量級媒峡。同樣瘟栖,我們有強(qiáng)大的、普遍存在的資源像 Maven 倉庫任我們使用谅阿,如果不充分利用它們是件令人可恥的事半哟。
這一系列博客打算講講用現(xiàn)代 Java 工作是多么簡單和有趣(不需犧牲任何性能),但是當(dāng)我去尋找一種有趣签餐、簡單和輕量級的方法去打包寓涨、分發(fā)和部署服務(wù)器端的 Java 應(yīng)用時,我兩手空空氯檐。所以 Capsule 誕生了(如果你知道有其它更好的選擇戒良,請告訴我)。
Capsule 使用平臺獨立的可執(zhí)行 JAR 包男摧,但是沒有依賴蔬墩,并且(可選的)能整合強(qiáng)大和便捷的 Maven 倉庫译打。一個 capsule 是一個 JAR 文件,它包括全部或者部分的 Capsule 項目 class拇颅,和一個包括部署配置的清單文件奏司。當(dāng)啟動時(java -jar app.jar
), capsule 會依次執(zhí)行以下的動作:解壓縮 JAR 文件到一個緩存目錄中樟插,下載依賴韵洋,尋找一個合適的 JVM 進(jìn)行安裝,然后配置和運(yùn)行應(yīng)用在一個新的JVM進(jìn)程中黄锤。
現(xiàn)在讓我們把 Capsule 拿出來溜一溜搪缨。我們把第一部的 JModern
項目做為開始的項目。這是我們的 build.gradle
文件:
apply plugin: 'java'
apply plugin: 'application'
sourceCompatibility = '1.8'
mainClassName = 'jmodern.Main'
repositories {
mavenCentral()
}
configurations {
quasar
}
dependencies {
compile "co.paralleluniverse:quasar-core:0.5.0:jdk8"
compile "co.paralleluniverse:quasar-actors:0.5.0"
quasar "co.paralleluniverse:quasar-core:0.5.0:jdk8"
testCompile 'junit:junit:4.11'
}
run {
jvmArgs "-javaagent:${configurations.quasar.iterator().next()}"
}
這里是我們的 jmodern.Main
類:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.Channel;
import co.paralleluniverse.strands.channels.Channels;
public class Main {
public static void main(String[] args) throws Exception {
final Channel<Integer> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
for (int i = 0; i < 10; i++) {
Strand.sleep(100);
ch.send(i);
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Integer x;
while((x = ch.receive()) != null)
System.out.println("--> " + x);
}).start().join(); // join waits for this fiber to finish
}
}
為了測試一下我們的程序工作是正常的鸵熟,我們運(yùn)行一下gradle run
副编。
現(xiàn)在,我們來把這個應(yīng)用打包成一個 capsule 流强。在構(gòu)建文件中痹届,我們將增加 capsule
配置。然后打月,我們增加依賴包:
capsule "co.paralleluniverse:capsule:0.3.1"
當(dāng)前 Capsule 有兩種方法來創(chuàng)建 capsule (雖然你也可以混合使用)队腐。第一種方法是創(chuàng)建應(yīng)用時把所有的依賴都加入到 capsule 中;第二種方法是第一次啟動 capsule 時讓它去下載依賴奏篙。我來試一下第一種—— "full" 模式柴淘。我們添加下面的任務(wù)到構(gòu)建文件中:
task capsule(type: Jar, dependsOn: jar) {
archiveName = "jmodern-capsule.jar"
from jar // embed our application jar
from { configurations.runtime } // embed dependencies
from(configurations.capsule.collect { zipTree(it) }) { include 'Capsule.class' } // we just need the single Capsule class
manifest {
attributes(
'Main-Class' : 'Capsule',
'Application-Class' : mainClassName,
'Min-Java-Version' : '1.8.0',
'JVM-Args' : run.jvmArgs.join(' '), // copy JVM args from the run task
'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '), // copy system properties
'Java-Agents' : configurations.quasar.iterator().next().getName()
)
}
}
好了,現(xiàn)在我們輸入gradle capsule
構(gòu)建 capsule 秘通,然后運(yùn)行:
java -jar build/libs/jmodern-capsule.jar
如果你想準(zhǔn)確的知道 Capsule 現(xiàn)在在做什么为严,可以把-jar
換成-Dcapsule.log=verbose
,但是因為它是一個包括依賴的 capsule ,第一次運(yùn)行時充易, Capsule 會解壓 JAR 文件到一個緩存目錄下
(這個目錄是在當(dāng)前用戶的根文件夾中下.capsule/apps/jmodern.Main
)梗脾,然后啟動一個新通過 capsule 清單文件配置好的 JVM 。如果你已經(jīng)安裝好了 Java7 盹靴,你可以使用 Java7 啟動 capsule (通過設(shè)置 JAVA_HOME 環(huán)境變量)炸茧。雖然 capsule 能在 java7 下啟動,但是因為 capsule 指定了最小的 Java 版本是 Java8 (或者是 1.8稿静,同樣的意思)梭冠, capsule 會尋找 Java8 并且用它來跑我們的應(yīng)用。
現(xiàn)在講講第二方法改备。我們將創(chuàng)建一個有外部依賴的 capsule 望拖。為了使創(chuàng)建工作簡單點吁恍,我們先在構(gòu)建文件中增加一個函數(shù)(你不需要理解他奶栖;做成 Gradle 的插件會更好,歡迎貢獻(xiàn)偶翅。但是現(xiàn)在我們手動創(chuàng)建這個 capsule ):
// converts Gradle dependencies to Capsule dependencies
def getDependencies(config) {
return config.getAllDependencies().collect {
def res = it.group + ':' + it.name + ':' + it.version +
(!it.artifacts.isEmpty() ? ':' + it.artifacts.iterator().next().classifier : '')
if(!it.excludeRules.isEmpty()) {
res += "(" + it.excludeRules.collect { it.group + ':' + it.module }.join(',') + ")"
}
return res
}
}
然后,我們改變構(gòu)建文件中capsule
任務(wù)碉渡,讓它能讀:
task capsule(type: Jar, dependsOn: classes) {
archiveName = "jmodern-capsule.jar"
from sourceSets.main.output // this way we don't need to extract
from { configurations.capsule.collect { zipTree(it) } }
manifest {
attributes(
'Main-Class' : 'Capsule',
'Application-Class' : mainClassName,
'Extract-Capsule' : 'false', // no need to extract the capsule
'Min-Java-Version' : '1.8.0',
'JVM-Args' : run.jvmArgs.join(' '),
'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '),
'Java-Agents' : getDependencies(configurations.quasar).iterator().next(),
'Dependencies': getDependencies(configurations.runtime).join(' ')
)
}
}
運(yùn)行gradle capsule
聚谁,再次運(yùn)行:
java -jar build/libs/jmodern-capsule.jar
首次運(yùn)行, capsule 將會下載我們項目的所有依賴到一個緩存目錄下滞诺。其他的 capsule 共享這個目錄形导。 相反你不需要把依賴列在 JAR 清單文件中,取而代之习霹,你可以把項目依賴列在 pom
文件中(如果你使用 Maven 做為構(gòu)建工具朵耕,這將特別有用),然后放在 capsule 的根目錄淋叶。詳細(xì)信息可以查看 Capsule 文檔阎曹。
最后,因為這篇文章的內(nèi)容對于任何 JVM 語言都是有用的爸吮,所以這里有一個小例子用來示意把一個 Node.js 的應(yīng)用打包成一個 capsule 芬膝。這個小應(yīng)用使用了 Avatar ,該項目能夠在 JVM 上運(yùn)行 javascript 應(yīng)用
形娇,就像 Nodejs 一樣。代碼如下:
var http = require('http');
var server = http.createServer(function (request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello World\n");
});
server.listen(8000);
console.log("Server running at http://127.0.0.1:8000/");
應(yīng)用還有兩個 Gradle 構(gòu)建文件筹误。一個用來創(chuàng)建full
模式的 capsule 桐早,另外一個用來創(chuàng)建external
模式的 capsule 。這個例子示范了打包原生庫依賴厨剪。創(chuàng)建該 capsule 哄酝,運(yùn)行:
gradle -b build1.gradle capsule
就得到一個包括所有依賴的 capsule 〉簧牛或者運(yùn)行下面的命令:
gradle -b build2.gradle capsule
就得到一個不包括依賴的 capsule (里面包括 Gradle wrapper陶衅,所以你不需要安裝 Gradle ,簡單的輸入./gradlew
就能構(gòu)建應(yīng)用)直晨。
運(yùn)行它搀军,輸入下面的命令:
java -jar build/libs/hello-nodejs.jar
Jigsaw,原計劃在包括在 Java9 中勇皇。該項目的意圖是解決 Java 部署和一些其它的問題罩句,例如:一個被精減的JVM發(fā)行版,減少啟動時間(這里有一個有趣演講關(guān)于 Jigsaw )敛摘。同時门烂,對于現(xiàn)代 Java 開發(fā)打包和布署,Capsule 是一個非常合適的工具。Capsule 是無狀態(tài)和不用安裝的屯远。
日志
在我們進(jìn)入 Java 先進(jìn)的監(jiān)控特性之前蔓姚,讓我們把日志搞定。據(jù)我所知慨丐,Java 有大量的日志庫坡脐,它們都是建立在 JDK 標(biāo)準(zhǔn)庫之上。如果你需要日志咖气,用不著想太多挨措,直接使用 slf4j 做為日志 API 。它變成了事實上日志 API 的標(biāo)準(zhǔn)崩溪,而且已綁定幾乎所有的日志引擎浅役。一但你使用 SLF4J,你可以推遲選擇日志引擎時機(jī)(你甚至能在部署的時候決定使用哪個日志引擎)伶唯。 SLF4J 在運(yùn)行時選擇日志引擎觉既,這個日志引擎可以是任何一個只要做為依賴添加的庫。大部分庫現(xiàn)在都使用SLF4J乳幸,如果開發(fā)中有一個庫沒有使用SLF4J瞪讼,它會讓你把這個庫的日志導(dǎo)回SLF4J,然后你就可以再選擇你的日志引擎粹断。談?wù)勥x擇日志引擎事符欠,如果你想選擇一個簡單的,那就 JDK 的java.util.logging瓶埋。如果你想選擇一個重型的希柿、高性能的日志引擎,就選擇 Log4j2 (除了你感覺真的有必要嘗試一下其它的日志引擎)养筒。
現(xiàn)在我們來添加日志到我們的應(yīng)用中曾撤。在依賴部分,我們增加:
compile "org.slf4j:slf4j-api:1.7.7" // the SLF4J API
runtime "org.slf4j:slf4j-jdk14:1.7.7" // SLF4J binding for java.util.logging
如果運(yùn)行gradle dependencies
命令晕粪,我們可以看到當(dāng)前的應(yīng)用有哪些依賴挤悉。就當(dāng)前來說,我們依賴了 Log4j 巫湘,這不是我們想要的装悲。因此好得在build.gradle
的配置部分增加一行代碼:
all*.exclude group: "org.apache.logging.log4j", module: "*"
好了,我們來給我們的應(yīng)用添加一些日志:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.Channel;
import co.paralleluniverse.strands.channels.Channels;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) throws Exception {
final Channel<Integer> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
for (int i = 0; i < 100000; i++) {
Strand.sleep(100);
log.info("Sending {}", i); // log something
ch.send(i);
if (i % 10 == 0)
log.warn("Sent {} messages", i + 1); // log something
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Integer x;
while ((x = ch.receive()) != null)
System.out.println("--> " + x);
}).start().join(); // join waits for this fiber to finish
}
}
然后運(yùn)行應(yīng)用(gradle run
)剩膘,你會看見日志打印到標(biāo)準(zhǔn)輸出(這個默認(rèn)設(shè)置衅斩;我們不打算深入配置日志引擎,你想做的話怠褐,可以參考想關(guān)文檔)畏梆。info
和warn
級的日志都默認(rèn)輸出。日志的輸出等級可以在配置文件中設(shè)置(現(xiàn)在我們不打算改了),或者一會可以看到奠涌,我們在運(yùn)行時進(jìn)行修改設(shè)置宪巨,
用jcmd和jstat進(jìn)行監(jiān)控和管理
JDK 中已經(jīng)包括了幾個用于監(jiān)控和管理的工具,而這里我們只會簡短介紹其中的一對工具:jcmd 和 jstat 溜畅。
為了演示它們捏卓,我們要使我們的應(yīng)用程序別那么快的終止。所以我們把for
循環(huán)次數(shù)從10
改成1000000
,然后在終端下運(yùn)行應(yīng)用gradle run
慈格。在另外一個終端中怠晴,我們運(yùn)行jcmd
。如果你的JDK安裝正確并且jcmd
在你的目錄中浴捆,你會看到下面的信息:
22177 jmodern.Main
21029 org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.11 /Users/pron/.gradle/daemon 10800000 86d63e7b-9a18-43e8-840c-649e25c329fc -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8
22182 sun.tools.jcmd.JCmd
上面信息列出了所有正在JVM上運(yùn)行的程序蒜田。再遠(yuǎn)行下面的命令:
jcmd jmodern.Main help
你會看到打印出了特定 JVM 程序的 jcmd 支持的命令列表。我們來試一下:
jcmd jmodern.Main Thread.print
打印出了 JVM 中所有線程的當(dāng)前堆棧信息选泻。試一下這個:
jcmd jmodern.Main PerfCounter.print
這將打印出一長串各種 JVM 性能計數(shù)器(你問問谷歌這些參數(shù)的意思)冲粤。你可以試一下其他的命令(如GC.class_histogram
)。
jstat
對于 JVM 來說就像 Linux 中的 top
页眯,只有它能查看關(guān)于 GC 和 JIT 的活動信息梯捕。假設(shè)我們應(yīng)用的 pid
是95098(可以用 jcmd
和 jps
找到這個值)。現(xiàn)在我們運(yùn)行:
jstat -gc 95098 1000
它將會每 1000 毫秒打印 GC 的信息窝撵】耍看起來像這樣:
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
80384.0 10752.0 0.0 10494.9 139776.0 16974.0 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
80384.0 10752.0 0.0 10494.9 139776.0 16985.1 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
80384.0 10752.0 0.0 10494.9 139776.0 16985.1 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
80384.0 10752.0 0.0 10494.9 139776.0 16985.1 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
這些數(shù)字表示各種 GC 區(qū)域當(dāng)前的容量。想知道每一個的意思碌奉,查看 jsata 文檔锣笨。
使用JMX進(jìn)行監(jiān)控和管理
JVM 最大的一個優(yōu)點就是它能在運(yùn)行時監(jiān)控和管理時,暴露每一個操作的詳細(xì)信息道批。JMX(Java Management Extensions),是 JVM 運(yùn)行時管理和監(jiān)控的標(biāo)準(zhǔn)入撒。 JMX 詳細(xì)說明了 MBeans 隆豹,該對象用來暴露有關(guān) JVM 、 JDK 庫和 JVM 應(yīng)用的監(jiān)控和管理操作方法茅逮。 JMX 還定義了連接 JVM 實例的標(biāo)準(zhǔn)方法璃赡,包括本地連接和遠(yuǎn)程連接的方式。還有定義了如何與 MBeans 交互献雅。實際上碉考, jcmd 就是使用 JMX 獲得相關(guān)的信息的。在本文后面挺身,我們也寫一個自己的 MBeans 侯谁,但是還是首先來看看內(nèi)置的 MBeans 如何使用。
當(dāng)我們的應(yīng)用運(yùn)行在一個終端,運(yùn)行 jvisualvm
命令(該工具是 JDK 的一部分)在另一個終端墙贱。這會啟動 VisualVM 热芹。在我們開始使用之前,還需要裝一些插件惨撇。打開 Tools->Plugins
菜單伊脓,選擇可以可以使用的插件。當(dāng)前的演示魁衙,我們只需要VisualVM-MBeans
报腔,但是你可能除了 VisualVM-Glassfish 和 BTrace Workbench ,其他的插件都裝上∑实恚現(xiàn)在在左邊面板選擇 jmodern.Main
纯蛾,然后選擇監(jiān)控頁。你會看到如下信息:
該監(jiān)控頁把 JMX-MBeans 暴露的使用信息用圖表的型式表達(dá)出來祷蝌。我們也可以通過 Mbeans 選項卡選擇一些 MBeans (有些需要安裝完成插件后才能使用)茅撞,我們能查看和交互已注冊的 MBeans 。例如有個常用的堆圖巨朦,就在 java.lang/Memory
中(雙擊屬性值展開它):
現(xiàn)在我們選擇 java.util.logging/Logging
MBean 米丘。在右邊面板中,屬性 LoggerNames
會列出所有已注冊的 logger 糊啡,包括我們添加到 jmodern.Main
(雙擊屬性值展開它):
MBeans 使我們不僅能夠探測到監(jiān)測值拄查,還可以改變這些值,然后調(diào)用各種管理操作棚蓄。選擇 Operations
選項卡(在右面板中堕扶,位于屬性選項卡的右邊)。我們現(xiàn)在在運(yùn)行時通過 JMX-MBean 改變?nèi)罩镜燃壦笠馈T?setLoggerLevel
屬性中稍算,第一個地方填上 jmodern.Main
,第二個地方填上 WARNING
役拴,載圖如下:
現(xiàn)在糊探,點擊 setLoggerLevel
按鈕, info
級的日志信息不再會打印出來河闰。如果調(diào)整成 SEVERE
科平,就沒有信息打印。 VisualVM 對 MBean 都會生成簡單的 GUI姜性,不用費(fèi)力的去寫界面瞪慧。
我們也可以在遠(yuǎn)程使用 VisualVM 訪問我們的應(yīng)用,只用增加一些系統(tǒng)的設(shè)置部念。在構(gòu)建文件中的run
部分中增加如下代碼:
systemProperty "com.sun.management.jmxremote", ""
systemProperty "com.sun.management.jmxremote.port", "9999"
systemProperty "com.sun.management.jmxremote.authenticate", "false"
systemProperty "com.sun.management.jmxremote.ssl", "false"
(在生產(chǎn)環(huán)境中弃酌,你應(yīng)該打開安全選項)
正如我們所看到的氨菇,除了 MBean 探測, VisualVM 也可以使用 JMX 提供的數(shù)據(jù)創(chuàng)建自定義監(jiān)控視圖:監(jiān)控線程狀態(tài)和當(dāng)前所有線程的堆棧情況矢腻,查看 GC 和通用內(nèi)存使用情況门驾,執(zhí)行堆轉(zhuǎn)儲和核心轉(zhuǎn)儲操作,分析轉(zhuǎn)儲堆和核心堆多柑,還有更多的其它功能奶是。因此,在現(xiàn)代 Java 開發(fā)中竣灌, VisualVM 是最重要的工具之一聂沙。這是 VisualVM 跟蹤插件提供的監(jiān)控信息截圖:
現(xiàn)代 Java 開發(fā)人員有時可能會喜歡一個 CLI 而不是漂亮的 GUI 。 jmxterm 提供了一個 CLI 形式的 JMX-MBeans 初嘹。不幸的是,它還不支持 Java7 和 Java8 及汉,但開發(fā)人員表示將很快來到(如果沒有,我們將發(fā)布一個補(bǔ)丁,我們已經(jīng)有一個分支在做這部分工作了)。
不過屯烦,有一件事是肯定的】浪妫現(xiàn)代 Java 開發(fā)人員喜歡 REST-API (如果沒有其他的原因,因為它們無處不在,并且很容易構(gòu)建 web-GUI )。雖然 JMX 標(biāo)準(zhǔn)支持一些不同的本地和遠(yuǎn)程連接器驻龟,但是標(biāo)準(zhǔn)中沒有包括 HTTP 連接器(應(yīng)該會在 Java9 中)∥旅迹現(xiàn)在,有一個很好的項目 Jolokia翁狐,填補(bǔ)這個空白类溢。它能讓我們使用 RESTful 的方式訪問 MBeans 。讓我們來試一試露懒。將以下代碼合并到build.gradle
文件中:
configurations {
jolokia
}
dependencies {
runtime "org.jolokia:jolokia-core:1.2.1"
jolokia "org.jolokia:jolokia-jvm:1.2.1:agent"
}
run {
jvmArgs "-javaagent:${configurations.jolokia.iterator().next()}=port=7777,host=localhost"
}
(我發(fā)現(xiàn) Gradle 總是要求對于每一個依賴重新設(shè)置 Java agent闯冷,這個問題一直困擾我。)
改變構(gòu)建文件 capsule
任務(wù)的 Java-Agents
屬性懈词,可以讓 Jolokia 在 capsule 中可用蛇耀。代碼如下:
'Java-Agents' : getDependencies(configurations.quasar).iterator().next() +
+ " ${getDependencies(configurations.jolokia).iterator().next()}=port=7777,host=localhost",
通過 gradle run
或者 gradle capsule; java -jar build/libs/jmodern-capsule.jar
運(yùn)行應(yīng)用,然后打開瀏覽器輸入 http://localhost:7777/jolokia/version
坎弯。如果 Jolokia 正常工作蒂窒,會返回一個JSON。現(xiàn)在我們要查看一下應(yīng)用的堆使用情況荞怒,可以這樣做:
curl http://localhost:7777/jolokia/read/java.lang:type\=Memory/HeapMemoryUsage
設(shè)置日志等級,你可以這樣做:
curl http://localhost:7777/jolokia/exec/java.util.logging:type\=Logging/setLoggerLevel\(java.lang.String,java.lang.String\)/jmodern.Main/WARNING
Jolokia 提供了 Http API 秧秉,這就就使用 GET 和 POST 方法進(jìn)行操作褐桌。同時還提供安全訪問的方法。需要更多的信息象迎,請查看文檔荧嵌。
有了 JolokiaHttpAPI 就能通過Web進(jìn)行管理呛踊。這里有一個例子,它使用Cubism為 GUI 進(jìn)行 JMX MBeans進(jìn)行管理啦撮。還有如 hawtio 谭网, JBoss 創(chuàng)建的項目,它使用 JolokiaHttpAPI 構(gòu)建了一個全功能的網(wǎng)頁版的管理應(yīng)用赃春。與 VisualVM 靜態(tài)分析功能不同的是愉择, hawatio 意圖是為生產(chǎn)環(huán)境提供一個持續(xù)監(jiān)控和管理的工具。
寫一個自定義的MBeans
寫一個 Mbeans 并注冊很容易:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import java.lang.management.ManagementFactory;
import java.util.concurrent.atomic.AtomicInteger;
import javax.management.MXBean;
import javax.management.ObjectName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) throws Exception {
final AtomicInteger counter = new AtomicInteger();
final Channel<Object> ch = Channels.newChannel(0);
// create and register MBean
ManagementFactory.getPlatformMBeanServer().registerMBean(new JModernInfo() {
@Override
public void send(String message) {
try {
ch.send(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int getNumMessagesReceived() {
return counter.get();
}
}, new ObjectName("jmodern:type=Info"));
new Fiber<Void>(() -> {
for (int i = 0; i < 100000; i++) {
Strand.sleep(100);
log.info("Sending {}", i); // log something
ch.send(i);
if (i % 10 == 0)
log.warn("Sent {} messages", i + 1); // log something
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Object x;
while ((x = ch.receive()) != null) {
counter.incrementAndGet();
System.out.println("--> " + x);
}
}).start().join(); // join waits for this fiber to finish
}
@MXBean
public interface JModernInfo {
void send(String message);
int getNumMessagesReceived();
}
}
我們添加了一個 JMX-MBean 织中,讓我們監(jiān)視第二個 fiber
收到消息的數(shù)量锥涕,也暴露了一個發(fā)送操作,能將一條消息進(jìn)入 channel
狭吼。當(dāng)我們運(yùn)行應(yīng)用程序時层坠,我們可以在 VisualVM 中看到監(jiān)控的屬性:
雙擊,繪圖:
在 Operations
選項卡中刁笙,使用我們定義在MBean的操作破花,來發(fā)個消息:
使用Metrics進(jìn)行健康和性能監(jiān)控
Metrics 一個簡潔的監(jiān)控 JVM 應(yīng)用性能和健康的現(xiàn)代庫,由 Coda Hale 在 Yammer 時創(chuàng)建的疲吸。 Metrics 庫中包含一些通用的指標(biāo)集和發(fā)布類座每,如直方圖,計時器磅氨,統(tǒng)計議表盤等〕咂埽現(xiàn)在我們來看看如何使用。
首先烦租,我們不需要使用 Jolokia 延赌,把它從構(gòu)建文件中移除掉,然后添加下面的代碼:
compile "com.codahale.metrics:metrics-core:3.0.2"
Metrics 通過 JMX-MBeans 發(fā)布指標(biāo)叉橱,你可以將這些指標(biāo)值寫入 CSV 文件挫以,或者做成 RESTful 接口,還可以發(fā)布到 Graphite 和 Ganglia
中窃祝。在這里只是簡單發(fā)布到 JMX (第三部分中討論到 Dropwizard 時掐松,會使用 HTTP )。這是我們修改后的 Main.class
:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import com.codahale.metrics.*;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.*;
public class Main {
public static void main(String[] args) throws Exception {
final MetricRegistry metrics = new MetricRegistry();
JmxReporter.forRegistry(metrics).build().start(); // starts reporting via JMX
final Channel<Object> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
Meter meter = metrics.meter(name(Main.class, "messages" , "send", "rate"));
for (int i = 0; i < 100000; i++) {
Strand.sleep(ThreadLocalRandom.current().nextInt(50, 500)); // random sleep
meter.mark(); // measures event rate
ch.send(i);
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Counter counter = metrics.counter(name(Main.class, "messages", "received"));
Timer timer = metrics.timer(name(Main.class, "messages", "duration"));
Object x;
long lastReceived = System.nanoTime();
while ((x = ch.receive()) != null) {
final long now = System.nanoTime();
timer.update(now - lastReceived, NANOSECONDS); // creates duration histogram
lastReceived = now;
counter.inc(); // counts
System.out.println("--> " + x);
}
}).start().join(); // join waits for this fiber to finish
}
}
在例子中粪小,使用了 Metrics 記數(shù)器〈蠡牵現(xiàn)在運(yùn)行應(yīng)用,啟動 VisualVM :
性能分析
性能分析是一個應(yīng)用是否滿足我們對性能要求的關(guān)鍵方法探膊。只有經(jīng)過性能分析我們才能知道哪一部分代碼影響了整體執(zhí)行速度杠愧,然后集中精力只改進(jìn)這一部分代碼。一直以來逞壁,Java 都有很好的性能分析工具流济,它們有的在 IDE 中锐锣,有的是一個單獨的工具。而最近 Java 的性能分析工具變得更精確和輕量級绳瘟,這要得益于 HotSpot 把 JRcokit
JVM 中的代碼合并自己的代碼中雕憔。在這部分討論的工具不是開源的,在這里討論它們是因為這些工具已經(jīng)包括在標(biāo)準(zhǔn)的 OracleJDK 中糖声,你可以在開發(fā)環(huán)境中自由使用(但是在生產(chǎn)環(huán)境中你需要一個商業(yè)許可)斤彼。
開始一個測試程序,修改后的代碼:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import com.codahale.metrics.*;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.*;
public class Main {
public static void main(String[] args) throws Exception {
final MetricRegistry metrics = new MetricRegistry();
JmxReporter.forRegistry(metrics).build().start(); // starts reporting via JMX
final Channel<Object> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
Meter meter = metrics.meter(name(Main.class, "messages", "send", "rate"));
for (int i = 0; i < 100000; i++) {
Strand.sleep(ThreadLocalRandom.current().nextInt(50, 500)); // random sleep
meter.mark();
ch.send(i);
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Counter counter = metrics.counter(name(Main.class, "messages", "received"));
Timer timer = metrics.timer(name(Main.class, "messages", "duration"));
Object x;
long lastReceived = System.nanoTime();
while ((x = ch.receive()) != null) {
final long now = System.nanoTime();
timer.update(now - lastReceived, NANOSECONDS);
lastReceived = now;
counter.inc();
double y = foo(x);
System.out.println("--> " + x + " " + y);
}
}).start().join();
}
static double foo(Object x) { // do crazy work
if (!(x instanceof Integer))
return 0.0;
double y = (Integer)x % 2723;
for(int i=0; i<10000; i++) {
String rstr = randomString('A', 'Z', 1000);
y *= rstr.matches("ABA") ? 0.5 : 2.0;
y = Math.sqrt(y);
}
return y;
}
public static String randomString(char from, char to, int length) {
return ThreadLocalRandom.current().ints(from, to + 1).limit(length)
.mapToObj(x -> Character.toString((char)x)).collect(Collectors.joining());
}
}
foo
方法進(jìn)行了一些沒有意義的計算姨丈,不用管它畅卓。當(dāng)運(yùn)行應(yīng)用(gradle run
)時,你會注意到 Quasar
發(fā)出了警告蟋恬,警告說有一個 fiber
占用了過多的 CPU
時間翁潘。為了弄清楚發(fā)生了什么,我們開始進(jìn)行性能分析:
我們使用的分析器能夠統(tǒng)計非常精確的信息,同時具有非常低的開銷歼争。該工具包括兩個組件:第一個是 Java Flight Recorder 已經(jīng)嵌入到 HotSpotVM 中拜马。它能記錄 JVM 中發(fā)生的事件,可以和 jcmd
配合使用沐绒,在這部分我們通過第二個工具來控制它俩莽。第二個工具是 JMC
(Java Mission Control),也在 JDK 中乔遮。它的作用等同于 VisualVM 扮超,只是它比較難用。在這里我們用 JMC 來控制 Java Flight Recorder 蹋肮,分析記錄的信息(我希望 Oracle 能把這部分功能移到 VisualVM 中)出刷。
Flight Recorder 在默認(rèn)已經(jīng)加入到應(yīng)用中,只是不會記錄任何信息也不會影響性能坯辩。先停止應(yīng)用馁龟,然后把這行代碼加到 build.gradle
中的 run
:
jvmArgs "-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"
UnlockCommercialFeatures
標(biāo)志是必須的,因為 Flight Recorder 是商業(yè)版的功能漆魔,不過可以在開發(fā)中自由使用】篱荩現(xiàn)在,我們重新啟動應(yīng)用改抡。
在另一個終端中矢炼,我們使用 jmc
打開 Mission Control 。在左邊的面板中阿纤,右擊 jmodern.Main
裸删,選擇 Start Flight Recording…
。在引導(dǎo)窗口中選擇 Event settings
下拉框阵赠,點擊 Profiling - on server
涯塔,然后 Next >
,注意不是 Finish
清蚀。
接下來匕荸,選擇 Heap Statistics
和 Allocation Profiling
,點擊 Finish
:
JMC 會等 Flight Recorder 記錄結(jié)束后枷邪,打開記錄文件進(jìn)行分析榛搔,在那時你可以關(guān)掉你的應(yīng)用。
在 Code
部分的 Hot Methods
選項卡中东揣,可以看出 randomString
是罪魁禍?zhǔn)准螅加昧顺绦驁?zhí)行時間的 90%:
在 Memory
部分的 Garbage Collection
選項卡中,展示了在記錄期間堆的使用情況:
在 GC 時間選項卡中嘶卧,顯示了GC的回收情況:
也可以查看內(nèi)存分配的情況:
應(yīng)用堆的內(nèi)容:
Java Flight Recorder
還有一個不被支持的API尔觉,能記錄應(yīng)用事件。
高級話題:使用Byteman進(jìn)行性能分析和調(diào)試
像第一部分一樣芥吟,我們用高級話題來結(jié)束本期話題侦铜。首先討論的是用 Byteman 進(jìn)行性能分析和調(diào)試。我在第一部分提到钟鸵, JVM 最強(qiáng)大的特性之一就是在運(yùn)行時動態(tài)加載代碼(這個特性遠(yuǎn)超本地原生應(yīng)用加載動態(tài)鏈接庫)钉稍。不只這個,JVM 還給了我們來回變換運(yùn)行時代碼的能力棺耍。
JBoss 開發(fā)的 Byteman 工具能充分利用 JVM 的這個特性贡未。 Byteman 能讓我們在運(yùn)行應(yīng)用時注入跟蹤、調(diào)試和性能測試相關(guān)代碼蒙袍。這個話題之所以是一個高級話題俊卤,是因為當(dāng)前 Byteman 只支持 Java7 ,對 Java8 的支持還不可靠左敌,需要打補(bǔ)丁才能工作瘾蛋。這個項目當(dāng)前開發(fā)活躍,但是正在落后矫限。因此在這里使用一些 Byteman 非巢负撸基礎(chǔ)的代碼。
這是主類:
package jmodern;
import java.util.concurrent.ThreadLocalRandom;
public class Main {
public static void main(String[] args) throws Exception {
for (int i = 0;; i++) {
System.out.println("Calling foo");
foo(i);
}
}
private static String foo(int x) throws InterruptedException {
long pause = ThreadLocalRandom.current().nextInt(50, 500);
Thread.sleep(pause);
return "aaa" + pause;
}
}
foo
模擬調(diào)用服務(wù)器操作叼风,這些操作要花費(fèi)一定時間進(jìn)行取董。
接下來,把下面的代碼合并到構(gòu)建文件中:
configurations {
byteman
}
dependencies {
byteman "org.jboss.byteman:byteman:2.1.4.1"
}
run {
jvmArgs "-javaagent:${configurations.byteman.iterator().next()}=listener:true,port:9977"
// remove the quasar agent
}
想在 capsule 中試一試 Byteman 使用无宿,在構(gòu)建文件中改一下 Java-Agents
屬性:
'Java-Agents' : "${getDependencies(configurations.byteman).iterator().next()}=listener:true,port:9977",
現(xiàn)在茵汰,從這里下載 Byteman ,因為需要使用 Byteman 中的命令行工具孽鸡,解壓文件蹂午,設(shè)置環(huán)境變量 BYTEMAN_HOME
指向 Byteman 的目錄栏豺。
啟動應(yīng)用gradle run
。打印結(jié)果如下:
Calling foo
Calling foo
Calling foo
Calling foo
Calling foo
我們想知道每次調(diào)用 foo
需要多長有時間豆胸,但是我們沒有測量并記錄這個信息“峦荩現(xiàn)在使用 Byteman
在運(yùn)行時插入相關(guān)日志記錄信息。
打開編輯器晚胡,在項目目錄中創(chuàng)建文件 jmodern.btm
:
RULE trace foo entry
CLASS jmodern.Main
METHOD foo
AT ENTRY
IF true
DO createTimer("timer")
ENDRULE
RULE trace foo exit
CLASS jmodern.Main
METHOD foo
AT EXIT
IF true
DO traceln("::::::: foo(" + $1 + ") -> " + $! + " : " + resetTimer("timer") + "ms")
ENDRULE
上面列的是 Byteman rules
灵奖,就是當(dāng)前我們想應(yīng)用在程序上的 rules
。我們在另一個終端中運(yùn)行命令:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 jmodern.btm
之后估盘,運(yùn)行中的應(yīng)用打印信息:
Calling foo
::::::: foo(152) -> aaa217 : 217ms
Calling foo
::::::: foo(153) -> aaa281 : 281ms
Calling foo
::::::: foo(154) -> aaa282 : 283ms
Calling foo
::::::: foo(155) -> aaa166 : 166ms
Calling foo
::::::: foo(156) -> aaa160 : 161ms
查看哪個 rules
正在使用:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977
卸載 Byteman
腳本:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 -u
運(yùn)行該命令之后瓷患,注入的日志代碼就被移出。
Byteman 是在 JVM 靈活代碼變換的基礎(chǔ)上創(chuàng)建的一個相當(dāng)強(qiáng)大的工具遣妥。你可以使用這個工具來檢查變量和日志事件擅编,插入延遲代碼等操作,甚至還可以輕松設(shè)置一些自定義的 Byteman 行為燥透。更多的信息沙咏,參考Byteman documentation。
高級話題:使用JMH進(jìn)行基準(zhǔn)測試
當(dāng)代硬件構(gòu)架和編譯技術(shù)的進(jìn)步使考察代碼性能的唯一方法就是基準(zhǔn)測試班套。一方面肢藐,由于現(xiàn)代 CPU 和編譯器非常聰明(可以看這里),它能為代碼(可以是 c吱韭,甚至是匯編)自動地創(chuàng)建一個理論上非常高效的運(yùn)行環(huán)境吆豹,就像 90 年代末一些游戲程序員做的那些非常不可思議的事一樣。另一方面理盆,正是因為聰明的 CPU 和編譯器痘煤,讓微基準(zhǔn)測試非常困難,因為這樣的話猿规,代碼的執(zhí)行速度非常依賴具體的執(zhí)行環(huán)境(如:代碼速度受 CPU 緩存狀態(tài)的影響衷快,而 CPU 緩存狀態(tài)又受其它線程操作的影響)。而對一個 Java 進(jìn)行微基準(zhǔn)測試又會更加的困難姨俩,因為 JVM 有 JIT 蘸拔,而 JIT 是一個以性能優(yōu)化為導(dǎo)向的編繹器,它能在運(yùn)行時影響代碼執(zhí)行的上下文環(huán)境环葵。因此在 JVM 中调窍,同一段代碼在微基準(zhǔn)測試和實際程序中執(zhí)行時間可能不一樣,有時可能快张遭,有時也可能慢邓萨。
JMH 是由 Oracle 創(chuàng)建的 Java 基準(zhǔn)測試工具。你可以相信由 JMH 測試出來的數(shù)據(jù)(可以看看這個由 JMH 主要作者Aleksey Shipilev的演講,幻燈片)缔恳。 Google 也做了一個基準(zhǔn)測試的工具叫 Caliper
宝剖,但是這個工具很不成熟,有時還會有錯誤的結(jié)果歉甚。不要使用它诈闺。
我們馬上來使用一下 JMH ,但是在這之前首先有一個忠告:過早優(yōu)化是萬惡之源铃芦。在基測試中,兩種算法或者數(shù)據(jù)結(jié)構(gòu)中襟雷,一種比另一種快 100 倍刃滓,而這個算法只占你應(yīng)用運(yùn)行時間的 1% ,這樣測試是沒有意義的耸弄。因為就算你把這個算法改進(jìn)的非尺只ⅲ快行但也只能加快你的應(yīng)用 2% 時間〖瞥剩基準(zhǔn)測試只能是已經(jīng)對應(yīng)用進(jìn)行了性能測試后砰诵,用來發(fā)現(xiàn)哪一個小部分改變能得到最大的加速成果。
增加依賴:
testCompile 'org.openjdk.jmh:jmh-core:0.8'
testCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.8'
然后增加bench
任務(wù):
task bench(type: JavaExec, dependsOn: [classes, testClasses]) {
classpath = sourceSets.test.runtimeClasspath // we'll put jmodern.Benchamrk in the test directory
main = "jmodern.Benchmark";
}
最后捌显,把測試代碼放到 src/test/java/jmodern/Benchmark.java
文件中茁彭。我之前提到過 90 年代的游戲程序員,是為了說明古老的技術(shù)現(xiàn)在仍然有用扶歪,這里我們測試一個開平方根的計算理肺,使用 fast inverse square root algorithm(平方根倒數(shù)速算法,這是 90 年代的程序):
package jmodern;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.parameters.TimeValue;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmark {
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(Benchmark.class.getName() + ".*")
.forks(1)
.warmupTime(TimeValue.seconds(5))
.warmupIterations(3)
.measurementTime(TimeValue.seconds(5))
.measurementIterations(5)
.build()).run();
}
private double x = 2.0; // prevent constant folding
@GenerateMicroBenchmark
public double standardInvSqrt() {
return 1.0/Math.sqrt(x);
}
@GenerateMicroBenchmark
public double fastInvSqrt() {
return invSqrt(x);
}
static double invSqrt(double x) {
double xhalf = 0.5d * x;
long i = Double.doubleToLongBits(x);
i = 0x5fe6ec85e7de30daL - (i >> 1);
x = Double.longBitsToDouble(i);
x = x * (1.5d - xhalf * x * x);
return x;
}
}
隨便說一下善镰,像第一部分中討論的 Checker 一樣妹萨, JMH 使用使用注解處理器。但是不同 Checker 炫欺, JMH 做的不錯乎完,你能在所有的 IDE 中使用它。在下面的圖中品洛,我們可以看到树姨, NetBeans 中,一但忘加 @State
注解毫别, IDE 就會報錯:
寫入命令 gradle bench
娃弓,運(yùn)行基準(zhǔn)測試。會得到以下結(jié)果:
Benchmark Mode Samples Mean Mean error Units
j.Benchmark.fastInvSqrt avgt 10 2.708 0.019 ns/op
j.Benchmark.standardInvSqrt avgt 10 12.824 0.065 ns/op
很漂亮吧岛宦,但是你得知道 fast-inv-sqrt
結(jié)果是一個粗略近似值台丛, 只在需要大量開平方的地方適用(如圖形計算中)。
在下面的例子中, JMH 用來報到 GC 使用的時間和方法棧的調(diào)用時間:
package jmodern;
import java.util.*;
import java.util.concurrent.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.parameters.TimeValue;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmark {
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(Benchmark.class.getName() + ".*")
.forks(2)
.warmupTime(TimeValue.seconds(5))
.warmupIterations(3)
.measurementTime(TimeValue.seconds(5))
.measurementIterations(5)
.addProfiler(GCProfiler.class) // report GC time
.addProfiler(StackProfiler.class) // report method stack execution profile
.build()).run();
}
@GenerateMicroBenchmark
public Object arrayList() {
return add(new ArrayList<>());
}
@GenerateMicroBenchmark
public Object linkedList() {
return add(new LinkedList<>());
}
static Object add(List<Integer> list) {
for (int i = 0; i < 4000; i++)
list.add(i);
return list;
}
}
這是 JMH 的打印出來的信息:
Iteration 3: 33783.296 ns/op
GC | wall time = 5.000 secs, GC time = 0.048 secs, GC% = 0.96%, GC count = +97
|
Stack | 96.9% RUNNABLE jmodern.generated.Benchmark_arrayList.arrayList_AverageTime_measurementLoop
| 1.8% RUNNABLE java.lang.Integer.valueOf
| 1.3% RUNNABLE java.util.Arrays.copyOf
| 0.0% (other)
|
JMH 是一個功能非常豐富的框架挽霉。不幸的是防嗡,在文檔方面有些薄弱,不過有一個相當(dāng)好代碼示例教程,用來展示 Java 中微基測試的陷阱死遭。你也可以讀讀這篇介紹 JMH 的入門文章干发。
目前為止我們已經(jīng)學(xué)了什么?
在這篇文章中他嫡,我們討論了在 JVM 管理、監(jiān)控和性能測試方面最好的幾個工具庐完。 JVM 除了很好的性能外钢属,它還非常深思熟慮地提供了能深度洞察它運(yùn)行狀態(tài)的能力,這就是我不會用其它的技術(shù)來取代 JVM 做為重要的门躯、長時間運(yùn)行的服務(wù)器端應(yīng)用平臺的主要原因淆党。
此外,我們還見識到了當(dāng)使用 Byteman 等工具修改運(yùn)行時代碼時讶凉, JVM 是多么強(qiáng)大染乌。
我們還介紹了 Capsule ,一個輕量級的懂讯、單文件荷憋、無狀態(tài)、不用安裝的部署工具域醇。另外台谊,通過一個公開或者組織內(nèi)部的 Maven 倉庫,它還支持整個Java應(yīng)用自動升級譬挚,或者還是僅僅升級一個依賴庫锅铅。
在第三部分中,我們將討論如何使用 Dropwizard 减宣, Comsat , Web Actors ,和 DI 來寫一個輕量級盐须、可擴(kuò)展的http服務(wù)。
水平有限漆腌,如果看不懂請直接看英文版贼邓。