===================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服务。
水平有限,若是看不懂请直接看英文版。