===================html
歡迎來到現代 Java 開發指南第二部分。在第一部分中,咱們已經展現了有關 Java 新的語言特性,庫和工具。這些新的工具使 Java 變成了至關輕量級的開發環境,這個開發環境擁有新的構建工具、更容易使用的文檔、富有表現力的代碼還有用戶級線程的併發。而在這部分中,咱們將比代碼層次更高一層,討論 Java 的運維———— Java 的部署、監控&管理,性能分析和基準測試。儘管這裏的例子都會用 Java 來作示意,可是咱們討論的內容與全部的 JVM 語言都相關,而不只僅是 Java 語言。java
在開始以前,我想簡短地回答一下第一部分讀者的問題,而且澄清一下說的不清楚的地方。第一部分中最受爭議的地方出如今構建工具這一節。在那一節中,我寫到現代的 Java 開發者使用 Gradle
。有些讀者對此提出異議,而且舉出了例子來證實 Maven 一樣也是一個很好的工具。我我的喜歡 Gradle 漂亮 DSL 和能使用指令式代碼來編寫非通用的構建操做,同時我也可以理解喜歡徹底聲明式的 Maven 的偏好,即便這樣作須要大量的插件。所以,我認可:現代的 Java 開發者可能更喜歡 Maven 而不是 Gradle 。我還想說,雖然使用 Gradle 不用瞭解 Groovy ,甚至人們但願在不是那麼標準的事情中也不用瞭解 Groovy 。可是我不會這樣,我從 Gradle 的在線例子中已經學習了不少有用的 Groovy 的語句。node
有些讀者指出我在第一部分的代碼示例中使用 Junit 和 Guava ,意味着我有意推廣它們。好吧,我確實有這樣的想法。Guava 是一個很是有用的庫,而 JUnit 是一個很好的單元測試框架。雖然 TestNG 也很好,可是 JUnit 很是常見,不多有人會選擇別的就算有優點的測試框架。
一樣,就示例代碼中測試使用 Hamcrest ,一個讀者指出 AssertJ,多是一個比 Hamcrest 更好的選擇。git
須要理解到本系列指南並不打算覆蓋到 Java 的方方面面,能認識到這一點很重要。因此固然會有不少很好的庫由於沒有在文章中出現,咱們沒有去探索它們。我寫這份指南的本意就是給你們示意一下現代 Java 開發多是什麼樣的。程序員
有些讀者表達了他們更喜歡短的 Javadoc 註釋,這種註釋沒必要像 Javadoc 標準形式那樣須要把全部的字段都寫上。以下面的例子:github
/** * This method returns the result. * @return the result */ int getResult();
更喜歡這樣:web
/** * Returns the result */ int getResult();
我徹底贊成。我在例子中簡單示範了混合 Markdown 和標準的 Javadoc 標籤的使用。這只是用來展現如何使用,並非意圖把這種使用方式當成指導方針。算法
最後,關於 Android 我有一些話要說。 Android 系統經過一系列變換以後,可以執行用 java (還有多是別的 JVM 語言)寫的代碼,可是 Android 不是 JVM,而且事實上 Android 不管在正式場合和實際使用中也不徹底是 Java (形成這個問題的緣由是兩個跨國公司,這裏指谷歌和甲骨文,沒有就 Java 的使用達成一個許可協議)。正由於 Android 不徹底是 Java ,因此在第一部分中討論的內容對 Android 可能有用或者也可能沒有用,並且由於 Android 沒有包括 JVM ,因此在這部分討論的內容不多能應用到 Android 上面。apache
好了,如今讓咱們回到正文。
對於不熟悉 Java 生態體系的人來講,Java(或者任何 JVM 語言)源文件,被編繹成 .class
文件(本質上是 Java 二進制文件),每個類一個文件。打包這些 class 文件的基本機制就把這些文件打包在一塊兒(這項工做一般由構建工具或者IDE來完成)放到JAR(Java存檔)文件,JAR 文件叫 Java 二進制包。 JAR 文件僅僅是 Zip 壓縮文件,它包括 class 文件,還有一個附加的清單文件用來描述內容,清單中還能夠包括其它的關於分發的信息(如在被簽名的 JARs中,清單能夠包括數字簽名)。若是你打包一個應用(與此相反是打包一個庫)到 JAR 中,清單文件應該指出應用的主類(也就是 main 函數所在類),在這種狀況下,應用經過命令java -jar app.jar
啓動,咱們稱這個 JAR 文件爲可執行的 JAR 。
Java 庫被打包成 JAR 文件,而後部署到 Maven 倉庫中(這個倉庫能被全部的 JVM 構建工具使用,不只僅是 Maven )。 Maven 倉庫管理這些庫二進制文件的版本和依賴(當你發一個請求想從Maven倉庫中加載一個庫,此外你請求了該庫全部的依賴)。開源 Java 庫常常託管在這個中央倉庫中,或者其它相似的公開倉庫中。而且組織機構經過 Artifactory 或者 Nexus 等工具,管理他們私有 Maven 倉庫。你甚至能在 GitHub 上創建本身的 Maven 倉庫。可是 Maven 倉庫在構建過程當中應該能正常使用,而且 Maven 倉庫一般託管庫形式 JAR 而不是可執行的 JAR 。
Java 網站應用傳統上應該在應用服務器(或者 servlet 容器)中執行。這些容器能運行多個網站應用,能按需加載或卸載應用。 Java 網站應用以 WAR 的形式部署在 servlet 容器中。WAR 也是 JAR 文件,它的內容以某種標準形式排好,而且包括額外的配置信息。可是,正如咱們將在第三部分看到同樣,就現代 Java 開發而言,Java 應用服務器已死。
Java 桌面應用常常被打包成與平臺相關的二進制文件,還包括一個平臺相關的 JVM。 JDK 工具包中有一個打包工具來作這個事情(這裏是講的是如何在 NetBeans 中使用它)。第三方工具 Packer 也提供了相似的功能。對於遊戲和桌面應用來講,這種打包機很是好。可是對於服務器軟件來講,這種打包機制就不是我想要的。此外,由於要打包一個 JVM 的拷貝,這種機制不能以補丁形式安全和平滑地升級應用。
對服務器端代碼,咱們想要的是一種簡單、輕量、能自動的打包和部署的工具。這個工具最好能利用可執行 JAR 的簡單和平臺無關性。可是可執行 JAR 有幾個不足的地方。每個庫一般打包到各自的 JAR 文件中,而後和全部的依賴一塊兒打包成單個 JAR 文件,這一過程可能形成衝突,特別是已打包的資源庫(沒有 class
文件的庫)一塊兒打包時。還有,一個原生庫在打包時不能直接放到 JAR 中。打包中可能最重要的是, JVM 配置信息(如 heap
的大小)對用戶來講是遺漏的,這個工做必須在命令行下才能作。像 Maven’s Shade plugin 和 Gradle’s Shadow plugin 等工具,解決了資源衝突的問題,而 One-Jar 支持原生的庫,可是這些工具均可能對應用產生影響,並且也沒有解決 JVM 參數配置的問題。 Gradle 能把應用打包成一個 ZIP 文件,而且產生一個與系統相關的啓腳本去配置 JVM ,可是這種方法要求安裝應用。咱們能夠作的比這樣更輕量級。一樣,咱們有強大的、廣泛存在的資源像 Maven 倉庫任咱們使用,若是不充分利用它們是件使人可恥的事。
這一系列博客打算講講用現代 Java 工做是多麼簡單和有趣(不需犧牲任何性能),可是當我去尋找一種有趣、簡單和輕量級的方法去打包、分發和部署服務器端的 Java 應用時,我兩手空空。因此 Capsule 誕生了(若是你知道有其它更好的選擇,請告訴我)。
Capsule 使用平臺獨立的可執行 JAR 包,可是沒有依賴,而且(可選的)能整合強大和便捷的 Maven 倉庫。一個 capsule 是一個 JAR 文件,它包括所有或者部分的 Capsule 項目 class,和一個包括部署配置的清單文件。當啓動時(java -jar app.jar
), capsule 會依次執行如下的動做:解壓縮 JAR 文件到一個緩存目錄中,下載依賴,尋找一個合適的 JVM 進行安裝,而後配置和運行應用在一個新的JVM進程中。
如今讓咱們把 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 } }
爲了測試一下咱們的程序工做是正常的,咱們運行一下gradle run
。
如今,咱們來把這個應用打包成一個 capsule 。在構建文件中,咱們將增長 capsule
配置。而後,咱們增長依賴包:
capsule "co.paralleluniverse:capsule:0.3.1"
當前 Capsule 有兩種方法來建立 capsule (雖然你也能夠混合使用)。第一種方法是建立應用時把全部的依賴都加入到 capsule 中;第二種方法是第一次啓動 capsule 時讓它去下載依賴。我來試一下第一種—— "full" 模式。咱們添加下面的任務到構建文件中:
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() ) } }
好了,如今咱們輸入gradle capsule
構建 capsule ,而後運行:
java -jar build/libs/jmodern-capsule.jar
若是你想準確的知道 Capsule 如今在作什麼,能夠把-jar
換成-Dcapsule.log=verbose
,可是由於它是一個包括依賴的 capsule ,第一次運行時, Capsule 會解壓 JAR 文件到一個緩存目錄下
(這個目錄是在當前用戶的根文件夾中下.capsule/apps/jmodern.Main
),而後啓動一個新經過 capsule 清單文件配置好的 JVM 。若是你已經安裝好了 Java7 ,你可使用 Java7 啓動 capsule (經過設置 JAVA_HOME 環境變量)。雖然 capsule 能在 java7 下啓動,可是由於 capsule 指定了最小的 Java 版本是 Java8 (或者是 1.8,一樣的意思), capsule 會尋找 Java8 而且用它來跑咱們的應用。
如今講講第二方法。咱們將建立一個有外部依賴的 capsule 。爲了使建立工做簡單點,咱們先在構建文件中增長一個函數(你不須要理解他;作成 Gradle 的插件會更好,歡迎貢獻。可是如今咱們手動建立這個 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 } }
而後,咱們改變構建文件中capsule
任務,讓它能讀:
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(' ') ) } }
運行gradle capsule
,再次運行:
java -jar build/libs/jmodern-capsule.jar
首次運行, capsule 將會下載咱們項目的全部依賴到一個緩存目錄下。其餘的 capsule 共享這個目錄。 相反你不須要把依賴列在 JAR 清單文件中,取而代之,你能夠把項目依賴列在 pom
文件中(若是你使用 Maven 作爲構建工具,這將特別有用),而後放在 capsule 的根目錄。詳細信息能夠查看 Capsule 文檔。
最後,由於這篇文章的內容對於任何 JVM 語言都是有用的,因此這裏有一個小例子用來示意把一個 Node.js 的應用打包成一個 capsule 。這個小應用使用了 Avatar ,該項目可以在 JVM 上運行 javascript 應用
,就像 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/");
應用還有兩個 Gradle 構建文件。一個用來建立full
模式的 capsule ,另外一個用來建立external
模式的 capsule 。這個例子示範了打包原生庫依賴。建立該 capsule ,運行:
gradle -b build1.gradle capsule
就獲得一個包括全部依賴的 capsule 。或者運行下面的命令:
gradle -b build2.gradle capsule
就獲得一個不包括依賴的 capsule (裏面包括 Gradle wrapper,因此你不須要安裝 Gradle ,簡單的輸入./gradlew
就能構建應用)。
運行它,輸入下面的命令:
java -jar build/libs/hello-nodejs.jar
Jigsaw,原計劃在包括在 Java9 中。該項目的意圖是解決 Java 部署和一些其它的問題,例如:一個被精減的JVM發行版,減小啓動時間(這裏有一個有趣演講關於 Jigsaw )。同時,對於現代 Java 開發打包和佈署,Capsule 是一個很是合適的工具。Capsule 是無狀態和不用安裝的。
在咱們進入 Java 先進的監控特性以前,讓咱們把日誌搞定。據我所知,Java 有大量的日誌庫,它們都是創建在 JDK 標準庫之上。若是你須要日誌,用不着想太多,直接使用 slf4j 作爲日誌 API 。它變成了事實上日誌 API 的標準,並且已綁定幾乎全部的日誌引擎。一但你使用 SLF4J,你能夠推遲選擇日誌引擎時機(你甚至能在部署的時候決定使用哪一個日誌引擎)。 SLF4J 在運行時選擇日誌引擎,這個日誌引擎能夠是任何一個只要作爲依賴添加的庫。大部分庫如今都使用SLF4J,若是開發中有一個庫沒有使用SLF4J,它會讓你把這個庫的日誌導回SLF4J,而後你就能夠再選擇你的日誌引擎。談談選擇日誌引擎事,若是你想選擇一個簡單的,那就 JDK 的java.util.logging。若是你想選擇一個重型的、高性能的日誌引擎,就選擇 Log4j2 (除了你感受真的有必要嘗試一下其它的日誌引擎)。
如今咱們來添加日誌到咱們的應用中。在依賴部分,咱們增長:
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
若是運行gradle dependencies
命令,咱們能夠看到當前的應用有哪些依賴。就當前來講,咱們依賴了 Log4j ,這不是咱們想要的。所以好得在build.gradle
的配置部分增長一行代碼:
all*.exclude group: "org.apache.logging.log4j", module: "*"
好了,咱們來給咱們的應用添加一些日誌:
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 } }
而後運行應用(gradle run
),你會看見日誌打印到標準輸出(這個默認設置;咱們不打算深刻配置日誌引擎,你想作的話,能夠參考想關文檔)。info
和warn
級的日誌都默認輸出。日誌的輸出等級能夠在配置文件中設置(如今咱們不打算改了),或者一會能夠看到,咱們在運行時進行修改設置,
JDK 中已經包括了幾個用於監控和管理的工具,而這裏咱們只會簡短介紹其中的一對工具:jcmd 和 jstat 。
爲了演示它們,咱們要使咱們的應用程序別那麼快的終止。因此咱們把for
循環次數從10
改爲1000000
,而後在終端下運行應用gradle run
。在另一個終端中,咱們運行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上運行的程序。再遠行下面的命令:
jcmd jmodern.Main help
你會看到打印出了特定 JVM 程序的 jcmd 支持的命令列表。咱們來試一下:
jcmd jmodern.Main Thread.print
打印出了 JVM 中全部線程的當前堆棧信息。試一下這個:
jcmd jmodern.Main PerfCounter.print
這將打印出一長串各類 JVM 性能計數器(你問問谷歌這些參數的意思)。你能夠試一下其餘的命令(如GC.class_histogram
)。
jstat
對於 JVM 來講就像 Linux 中的 top
,只有它能查看關於 GC 和 JIT 的活動信息。假設咱們應用的 pid
是95098(能夠用 jcmd
和 jps
找到這個值)。如今咱們運行:
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
這些數字表示各類 GC 區域當前的容量。想知道每個的意思,查看 jsata 文檔。
JVM 最大的一個優勢就是它能在運行時監控和管理時,暴露每個操做的詳細信息。JMX(Java Management Extensions),是 JVM 運行時管理和監控的標準。 JMX 詳細說明了 MBeans ,該對象用來暴露有關 JVM 、 JDK 庫和 JVM 應用的監控和管理操做方法。 JMX 還定義了鏈接 JVM 實例的標準方法,包括本地鏈接和遠程鏈接的方式。還有定義瞭如何與 MBeans 交互。實際上, jcmd 就是使用 JMX 得到相關的信息的。在本文後面,咱們也寫一個本身的 MBeans ,可是仍是首先來看看內置的 MBeans 如何使用。
當咱們的應用運行在一個終端,運行 jvisualvm
命令(該工具是 JDK 的一部分)在另外一個終端。這會啓動 VisualVM 。在咱們開始使用以前,還須要裝一些插件。打開 Tools->Plugins
菜單,選擇能夠可使用的插件。當前的演示,咱們只須要VisualVM-MBeans
,可是你可能除了 VisualVM-Glassfish 和 BTrace Workbench ,其餘的插件都裝上。如今在左邊面板選擇 jmodern.Main
,而後選擇監控頁。你會看到以下信息:
該監控頁把 JMX-MBeans 暴露的使用信息用圖表的型式表達出來。咱們也能夠經過 Mbeans 選項卡選擇一些 MBeans (有些須要安裝完成插件後才能使用),咱們能查看和交互已註冊的 MBeans 。例若有個經常使用的堆圖,就在 java.lang/Memory
中(雙擊屬性值展開它):
如今咱們選擇 java.util.logging/Logging
MBean 。在右邊面板中,屬性 LoggerNames
會列出全部已註冊的 logger ,包括咱們添加到 jmodern.Main
(雙擊屬性值展開它):
MBeans 使咱們不只可以探測到監測值,還能夠改變這些值,而後調用各類管理操做。選擇 Operations
選項卡(在右面板中,位於屬性選項卡的右邊)。咱們如今在運行時經過 JMX-MBean 改變日誌等級。在 setLoggerLevel
屬性中,第一個地方填上 jmodern.Main
,第二個地方填上 WARNING
,載圖以下:
如今,點擊 setLoggerLevel
按鈕, info
級的日誌信息再也不會打印出來。若是調整成 SEVERE
,就沒有信息打印。 VisualVM 對 MBean 都會生成簡單的 GUI,不用費力的去寫界面。
咱們也能夠在遠程使用 VisualVM 訪問咱們的應用,只用增長一些系統的設置。在構建文件中的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"
(在生產環境中,你應該打開安全選項)
正如咱們所看到的,除了 MBean 探測, VisualVM 也可使用 JMX 提供的數據建立自定義監控視圖:監控線程狀態和當前全部線程的堆棧狀況,查看 GC 和通用內存使用狀況,執行堆轉儲和核心轉儲操做,分析轉儲堆和核心堆,還有更多的其它功能。所以,在現代 Java 開發中, VisualVM 是最重要的工具之一。這是 VisualVM 跟蹤插件提供的監控信息截圖:
現代 Java 開發人員有時可能會喜歡一個 CLI 而不是漂亮的 GUI 。 jmxterm 提供了一個 CLI 形式的 JMX-MBeans 。不幸的是,它還不支持 Java7 和 Java8 ,但開發人員表示將很快來到(若是沒有,咱們將發佈一個補丁,咱們已經有一個分支在作這部分工做了)。
不過,有一件事是確定的。現代 Java 開發人員喜歡 REST-API (若是沒有其餘的緣由,由於它們無處不在,而且很容易構建 web-GUI )。雖然 JMX 標準支持一些不一樣的本地和遠程鏈接器,可是標準中沒有包括 HTTP 鏈接器(應該會在 Java9 中)。如今,有一個很好的項目 Jolokia,填補這個空白。它能讓咱們使用 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" }
(我發現 Gradle 老是要求對於每個依賴從新設置 Java agent,這個問題一直困擾我。)
改變構建文件 capsule
任務的 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
運行應用,而後打開瀏覽器輸入 http://localhost:7777/jolokia/version
。若是 Jolokia 正常工做,會返回一個JSON。如今咱們要查看一下應用的堆使用狀況,能夠這樣作:
curl http://localhost:7777/jolokia/read/java.lang:type\=Memory/HeapMemoryUsage
設置日誌等級,你能夠這樣作:
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 方法進行操做。同時還提供安全訪問的方法。須要更多的信息,請查看文檔。
有了 JolokiaHttpAPI 就能經過Web進行管理。這裏有一個例子,它使用Cubism爲 GUI 進行 JMX MBeans進行管理。還有如 hawtio , JBoss 建立的項目,它使用 JolokiaHttpAPI 構建了一個全功能的網頁版的管理應用。與 VisualVM 靜態分析功能不一樣的是, hawatio 意圖是爲生產環境提供一個持續監控和管理的工具。
寫一個 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 ,讓咱們監視第二個 fiber
收到消息的數量,也暴露了一個發送操做,能將一條消息進入 channel
。當咱們運行應用程序時,咱們能夠在 VisualVM 中看到監控的屬性:
雙擊,繪圖:
在 Operations
選項卡中,使用咱們定義在MBean的操做,來發個消息:
Metrics 一個簡潔的監控 JVM 應用性能和健康的現代庫,由 Coda Hale 在 Yammer 時建立的。 Metrics 庫中包含一些通用的指標集和發佈類,如直方圖,計時器,統計議錶盤等。如今咱們來看看如何使用。
首先,咱們不須要使用 Jolokia ,把它從構建文件中移除掉,而後添加下面的代碼:
compile "com.codahale.metrics:metrics-core:3.0.2"
Metrics 經過 JMX-MBeans 發佈指標,你能夠將這些指標值寫入 CSV 文件,或者作成 RESTful 接口,還能夠發佈到 Graphite 和 Ganglia
中。在這裏只是簡單發佈到 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 記數器。如今運行應用,啓動 VisualVM :
性能分析是一個應用是否知足咱們對性能要求的關鍵方法。只有通過性能分析咱們才能知道哪一部分代碼影響了總體執行速度,而後集中精力只改進這一部分代碼。一直以來,Java 都有很好的性能分析工具,它們有的在 IDE 中,有的是一個單獨的工具。而最近 Java 的性能分析工具變得更精確和輕量級,這要得益於 HotSpot 把 JRcokit
JVM 中的代碼合併本身的代碼中。在這部分討論的工具不是開源的,在這裏討論它們是由於這些工具已經包括在標準的 OracleJDK 中,你能夠在開發環境中自由使用(可是在生產環境中你須要一個商業許可)。
開始一個測試程序,修改後的代碼:
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
方法進行了一些沒有意義的計算,不用管它。當運行應用(gradle run
)時,你會注意到 Quasar
發出了警告,警告說有一個 fiber
佔用了過多的 CPU
時間。爲了弄清楚發生了什麼,咱們開始進行性能分析:
咱們使用的分析器可以統計很是精確的信息,同時具備很是低的開銷。該工具包括兩個組件:第一個是 Java Flight Recorder 已經嵌入到 HotSpotVM 中。它能記錄 JVM 中發生的事件,能夠和 jcmd
配合使用,在這部分咱們經過第二個工具來控制它。第二個工具是 JMC
(Java Mission Control),也在 JDK 中。它的做用等同於 VisualVM ,只是它比較難用。在這裏咱們用 JMC 來控制 Java Flight Recorder ,分析記錄的信息(我但願 Oracle 能把這部分功能移到 VisualVM 中)。
Flight Recorder 在默認已經加入到應用中,只是不會記錄任何信息也不會影響性能。先中止應用,而後把這行代碼加到 build.gradle
中的 run
:
jvmArgs "-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"
UnlockCommercialFeatures
標誌是必須的,由於 Flight Recorder 是商業版的功能,不過能夠在開發中自由使用。如今,咱們從新啓動應用。
在另外一個終端中,咱們使用 jmc
打開 Mission Control 。在左邊的面板中,右擊 jmodern.Main
,選擇 Start Flight Recording…
。在引導窗口中選擇 Event settings
下拉框,點擊 Profiling - on server
,而後 Next >
,注意不是 Finish
。
接下來,選擇 Heap Statistics
和 Allocation Profiling
,點擊 Finish
:
JMC 會等 Flight Recorder 記錄結束後,打開記錄文件進行分析,在那時你能夠關掉你的應用。
在 Code
部分的 Hot Methods
選項卡中,能夠看出 randomString
是罪魁禍首,它佔用了程序執行時間的 90%:
在 Memory
部分的 Garbage Collection
選項卡中,展現了在記錄期間堆的使用狀況:
在 GC 時間選項卡中,顯示了GC的回收狀況:
也能夠查看內存分配的狀況:
應用堆的內容:
Java Flight Recorder
還有一個不被支持的API,能記錄應用事件。
像第一部分同樣,咱們用高級話題來結束本期話題。首先討論的是用 Byteman 進行性能分析和調試。我在第一部分提到, JVM 最強大的特性之一就是在運行時動態加載代碼(這個特性遠超本地原生應用加載動態連接庫)。不僅這個,JVM 還給了咱們來回變換運行時代碼的能力。
JBoss 開發的 Byteman 工具能充分利用 JVM 的這個特性。 Byteman 能讓咱們在運行應用時注入跟蹤、調試和性能測試相關代碼。這個話題之因此是一個高級話題,是由於當前 Byteman 只支持 Java7 ,對 Java8 的支持還不可靠,須要打補丁才能工做。這個項目當前開發活躍,可是正在落後。所以在這裏使用一些 Byteman 很是基礎的代碼。
這是主類:
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
模擬調用服務器操做,這些操做要花費必定時間進行。
接下來,把下面的代碼合併到構建文件中:
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 使用,在構建文件中改一下 Java-Agents
屬性:
'Java-Agents' : "${getDependencies(configurations.byteman).iterator().next()}=listener:true,port:9977",
如今,從這裏下載 Byteman ,由於須要使用 Byteman 中的命令行工具,解壓文件,設置環境變量 BYTEMAN_HOME
指向 Byteman 的目錄。
啓動應用gradle run
。打印結果以下:
Calling foo Calling foo Calling foo Calling foo Calling foo
咱們想知道每次調用 foo
須要多長有時間,可是咱們沒有測量並記錄這個信息。如今使用 Byteman
在運行時插入相關日誌記錄信息。
打開編輯器,在項目目錄中建立文件 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
,就是當前咱們想應用在程序上的 rules
。咱們在另外一個終端中運行命令:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 jmodern.btm
以後,運行中的應用打印信息:
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
運行該命令以後,注入的日誌代碼就被移出。
Byteman 是在 JVM 靈活代碼變換的基礎上建立的一個至關強大的工具。你可使用這個工具來檢查變量和日誌事件,插入延遲代碼等操做,甚至還能夠輕鬆設置一些自定義的 Byteman 行爲。更多的信息,參考Byteman documentation。
當代硬件構架和編譯技術的進步使考察代碼性能的惟一方法就是基準測試。一方面,因爲現代 CPU 和編譯器很是聰明(能夠看這裏),它能爲代碼(能夠是 c,甚至是彙編)自動地建立一個理論上很是高效的運行環境,就像 90 年代末一些遊戲程序員作的那些很是難以想象的事同樣。另外一方面,正是由於聰明的 CPU 和編譯器,讓微基準測試很是困難,由於這樣的話,代碼的執行速度很是依賴具體的執行環境(如:代碼速度受 CPU 緩存狀態的影響,而 CPU 緩存狀態又受其它線程操做的影響)。而對一個 Java 進行微基準測試又會更加的困難,由於 JVM 有 JIT ,而 JIT 是一個以性能優化爲導向的編繹器,它能在運行時影響代碼執行的上下文環境。所以在 JVM 中,同一段代碼在微基準測試和實際程序中執行時間可能不同,有時可能快,有時也可能慢。
JMH 是由 Oracle 建立的 Java 基準測試工具。你能夠相信由 JMH 測試出來的數據(能夠看看這個由 JMH 主要做者Aleksey Shipilev的演講,幻燈片)。 Google 也作了一個基準測試的工具叫 Caliper
,可是這個工具很不成熟,有時還會有錯誤的結果。不要使用它。
咱們立刻來使用一下 JMH ,可是在這以前首先有一個忠告:過早優化是萬惡之源。在基測試中,兩種算法或者數據結構中,一種比另外一種快 100 倍,而這個算法只佔你應用運行時間的 1% ,這樣測試是沒有意義的。由於就算你把這個算法改進的很是快行但也只能加快你的應用 2% 時間。基準測試只能是已經對應用進行了性能測試後,用來發現哪個小部分改變能獲得最大的加速成果。
增長依賴:
testCompile 'org.openjdk.jmh:jmh-core:0.8' testCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.8'
而後增長bench
任務:
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 年代的遊戲程序員,是爲了說明古老的技術如今仍然有用,這裏咱們測試一個開平方根的計算,使用fast inverse square root algorithm(平方根倒數速算法,這是 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
,運行基準測試。會獲得如下結果:
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
結果是一個粗略近似值, 只在須要大量開平方的地方適用(如圖形計算中)。
在下面的例子中, JMH 用來報到 GC 使用的時間和方法棧的調用時間:
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 是一個功能很是豐富的框架。不幸的是,在文檔方面有些薄弱,不過有一個至關好代碼示例教程,用來展現 Java 中微基測試的陷阱。你也能夠讀讀這篇介紹 JMH 的入門文章。
在這篇文章中,咱們討論了在 JVM 管理、監控和性能測試方面最好的幾個工具。 JVM 除了很好的性能外,它還很是深思熟慮地提供了能深度洞察它運行狀態的能力,這就是我不會用其它的技術來取代 JVM 作爲重要的、長時間運行的服務器端應用平臺的主要緣由。
此外,咱們還見識到了當使用 Byteman 等工具修改運行時代碼時, JVM 是多麼強大。
咱們還介紹了 Capsule ,一個輕量級的、單文件、無狀態、不用安裝的部署工具。另外,經過一個公開或者組織內部的 Maven 倉庫,它還支持整個Java應用自動升級,或者仍是僅僅升級一個依賴庫。
在第三部分中,咱們將討論如何使用 Dropwizard , Comsat , Web Actors ,和 DI 來寫一個輕量級、可擴展的http服務。
水平有限,若是看不懂請直接看英文版。