毫无疑问,Java是世界上最大的技术平台之一,拥有大约900万到1000万开发人员(根据Oracle的数据)。按照设计,许多开发人员不需要知道他们所使用的平台的底层复杂性。这导致了开发人员只有在客户抱怨性能时,才会想到去接触Java调优。
然而,对于对性能感兴趣的开发人员来说,理解JVM技术堆栈的基础知识是很重要的。理解JVM技术使开发人员能够编写更好的软件,并为研究与性能相关的问题提供必要的理论背景。
本章介绍JVM如何执行Java,以便为本书后面对这些主题的深入研究提供基础。特别是,第9章将深入讨论字节码。读者的一个策略可能是现在就阅读这一章,然后在理解了其他一些主题之后,再结合第9章重读这一章。
2.1 Interpreting and Classloading
根据定义Java虚拟机(通常称为VM规范)的规范,JVM是基于堆栈的解释机器。这意味着它不使用寄存器(如物理硬件CPU),而是使用部分结果的执行堆栈,并通过对堆栈的顶值(或多个值)进行操作来执行计算。
JVM解释器的基本行为本质上可以看作是“while循环中的一个switch”——独立于最后一个操作码处理程序的每个操作码,使用计算堆栈保存中间值。
NOTE:当我们深入研究Oracle/OpenJDK VM (HotSpot)的内部时,我们将看到,对于真正的生产级Java解释器来说,情况可能更加复杂,但是switch-inside-while是目前可以接受的心理模型。
当我们使用java HelloWorld命令启动应用程序时,操作系统将启动虚拟机进程(即java二进制文件)。这将设置Java虚拟环境,并初始化将实际执行HelloWorld类文件中的用户代码的堆栈机器。
应用程序的入口点将是HelloWorld.class的main()方法。为了将控制权交给这个类,它必须在开始执行之前由虚拟机加载。
为此,使用了Java类加载机制。当一个新的Java进程初始化时,将使用一个类加载器链。初始加载器称为引导类加载器【Bootstrap classloader】,它包含核心Java运行时【runtime】中的类。在Java8以及之前的版本中,这些都是从rt.jar加载的。在Java9和更高版本中,运行时【runtime】被模块化,类加载的概念有所不同。
引导类加载器【Bootstrap classloader】的主要目的是获得最小的类集(包括java.lang.Object、Class和Classloader等基本类),以允许其他类加载器启动系统的其余部分。
NOTE:Java将类加载器建模为其自身运行时和类型系统中的对象,因此需要某种方式来创建初始类集。否则,在定义什么是类装入器时就会出现循环问题。
接下来创建扩展类加载器【Extension classloader】;它将其父类定义为引导类加载器【Bootstrap classloader】,并在需要时将委托给其父类加载器。该扩展类加载器并没有被广泛使用,但是可以为特定的操作系统和平台提供重写【override】和本地【native】代码。值得注意的是,Java 8中引入的Nashorn JavaScript runtime是由扩展类加载器【Extension classloader加载的。
最后,创建应用类加载器【Application classloader】;它负责从定义的classpath下加载用户类。不幸的是,有些文本将其称为“系统【System】”类加载器。应该避免使用这个术语,原因很简单,它不加载系统类(这是引导类加载器【Bootstrap classloader的职责)。应用类加载器【Application classloader】非常常见,它的父类是扩展类加载器【Extension classloader】。
当在程序执行过程中第一次遇到新类时,Java会加载它们的依赖项。如果类加载器找不到类,通常会将查找委托给父类。如果查找链到达引导类加载器【Bootstrap classloader】,仍然没有找到该类,则会抛出ClassNotFoundException开发人员使用的构建过程必须有效地使用与生产环境中使用的类路径完全相同的类路径进行编译,因为这有助于缓解这个潜在的问题。
在正常情况下,Java只加载一个类一次,并在运行时环境【runtime environment】中创建一个Class对象来表示该类。然而,重要的是要认识到同一个类可能会被不同的类加载器加载两次。因此,系统中的类由用于加载它的类加载器以及完全限定的类名(包括包名)来唯一标识。
2.2 Executing Bytecode
重要的是要理解Java源代码在执行之前经历了大量的转换。第一个转换就是使用Java编译器javac的编译步骤,它通常作为较大的构建过程的一部分而被调用。
javac的工作是将Java代码转换成包含字节码的.class文件。它通过对Java源代码进行相当简单的转换来实现这一点,如图2-1所示。在javac编译期间很少进行优化,在反汇编工具(如标准的javap)中查看时,得到的字节码仍然是可读的和可识别的Java代码。
图2-1 Java class file compilation
字节码是一种不绑定到特定机器架构的中间表示形式。与机器架构的解耦提供了可移植性,这意味着已经开发(或编译)的软件可以在JVM支持的任何平台上运行,并提供了来自Java语言的抽象。这为我们了解JVM执行代码的方式提供了第一个重要视角。
NOTE:Java语言和Java虚拟机现在在一定程度上是独立的,因此JVM中的J可能有点误导人,因为JVM可以执行任何可以生成有效类文件的JVM语言。实际上,图2-1可以很容易地显示Scala编译器scalac生成字节码以便在JVM上执行。
无论使用何种源代码编译器,生成的类文件都具有VM规范指定的定义良好的结构(表2-1)。在允许运行之前,JVM加载的任何类都将被验证是否符合预期的格式。
Table 2-1 类文件的剖析
Component | Description |
---|---|
Magic number | 0xCAFEBABE |
类文件格式的版本 | 类文件的次要版本和主要版本 |
Constant pool:常量池 | 类的常量池 |
Access flags:访问标识 | 类是否是抽象的、静态的等等 |
This class:该类 | 当前类的名字 |
Superclass:超类 | 超类的名字 |
Interfaces:接口 | 类中的任何接口 |
Fields:字段属性 | 类中的任何字段属性 |
Methods:方法 | 类中的任何方法 |
Attributes | 类的任何属性(例如,源文件的名称,等等) |
每个类文件都以数字0xCAFEBABE开始,十六进制中的前4个字节表示与类文件格式的一致性。下面的4个字节表示用于编译类文件的次要版本和主要版本,将对它们进行检查,以确保目标JVM的版本不低于用于编译类文件的版本。类加载器检查主版本和次版本,以确保兼容性;如果它们不兼容,将在运行时抛出UnsupportedClassVersionError,表明运行时【runtime】的版本比编译后的类低。
NOTE:Magic numbers为Unix环境提供了一种识别文件类型的方法(而Windows通常使用文件扩展名)。因此,一旦做出决定,就很难改变。不幸的是,这意味着在可预见的将来,Java将无法使用令人尴尬且带有性别歧视的0xCAFEBABE,尽管Java 9为模块文件引入了神奇的数字0xCAFEDADA。
常量池在代码中保存常量值:例如,类、接口和字段的名字。当JVM执行代码时,常量池表用于引用值,而不是必须在运行时依赖于内存布局。
访问标志用于确定应用于类的修饰符。标志的第一部分用于标识一般属性,比如类是否是公共的,然后是它是否是final的,不能被子类化。该标志还确定类文件是表示的接口还是抽象类。标志的最后一部分指出类文件是否表示的是源代码中不存在的合成类、注释类型或枚举。
This class【该类】、Superclass【超类】和Interfaces【接口】项是常量池中的索引,用于标识属于该类的类型层次结构。Fields【字段属性】和Methods【方法】定义类似签名的结构,包括应用于字段属性或方法的修饰符。然后使用一组Attributes【属性】来表示更复杂和非固定大小结构的结构化项。例如,方法使用Code属性来表示与该特定方法关联的字节码。
图2-2 提供了用于记住结构的助记符
图2 - 2 类文件结构的助记符
在这个非常简单的代码示例中,可以观察运行javac的效果:
public class HelloWorld {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("Hello World");
}
}
}
Java附带了一个名为javap的类文件反汇编器,以允许我们检查.class文件。获取HelloWorld类文件并运行javap -c HelloWorld,会得到以下输出:
public class HelloWorld {
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 22
8: getstatic #2 // Field java/lang/System.out ...
11: ldc #3 // String Hello World
13: invokevirtual #4 // Method java/io/PrintStream.println ...
16: iinc 1, 1
19: goto 2
22: return
}
这个布局描述了文件HelloWorld.class的字节码。对于更详细的信息,javap还有一个-v选项,提供完整的类文件头信息和常量池详细信息。该类文件包含两个方法,尽管源文件中只提供了一个main()方法;这是javac自动向类中添加默认构造函数的结果。
构造函数中执行的第一个指令是aload_0,它将此引用放在堆栈的第一个位置。然后调用invokspecial命令,该命令调用一个实例方法,该方法对调用超类构造函数和创建对象具有特定的处理。在默认构造函数中,调用与Object的默认构造函数匹配,因为未提供覆盖。
NOTE:JVM中的操作码【Opcodes 】非常简洁,表示类型、操作以及本地变量、常量池和堆栈之间的交互。
回到main()方法,iconst_0将整数常量0推入计算堆栈。istore_1以偏移量1(在循环中表示为i)将这个常量值存储到局部变量中。局部变量偏移量从0开始,但是对于实例方法,第0项总是这样的。然后将偏移量为1的变量加载回堆栈,并使用if_icmpge(“if integer compare greater or equal”)与压入的常量10进行比较。只有当当前整数为>= 10时,该测试才会成功。
对于最初的几个迭代,这个比较测试均是false,因此我们继续指令8。这里是来自System.out的静态方法,然后从常量池加载“Hello World”字符串。下一个调用是invokevirtual,调用基于类的实例方法。然后对整数进行递增,并调用goto返回到指令2。
这个过程一直持续到if_icmpge比较最终返回true(循环变量为>= 10);在循环的那个迭代中,控制传递给指令22,方法返回。
2.3 HotSpot简介
1999年4月,Sun引入了Java在性能方面的最大变化之一。HotSpot虚拟机是Java的一个关键特性,它的性能可以与C和c++等语言相媲美(或者更好)(参见图2-3)。为了解释这是如何实现的,让我们深入研究用于应用程序开发的语言设计。
图2-3 The HotSpot JVM
语言和平台设计常常涉及在所需功能之间做出决策和权衡。
C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
零开销原则在理论上听起来很棒,但它要求所有语言的用户都要处理操作系统和计算机实际工作的底层现实。对于那些不把原始性能作为主要目标的开发人员来说,这是一个巨大的额外认知负担。
不仅如此,它还要求在构建时将源代码编译为特定于平台的机器码——通常称为提前编译【Ahead-of-Time】(AOT)。这是因为其他执行模型,如解释器、虚拟机和可移植层,几乎都不是零开销的。
该原则还在那句短语“what you do use, you couldn’t hand code any better”中隐藏了一个大麻烦。这需要很多条件,尤其是开发人员能够生成比自动化系统更好的代码。这根本不是一个安全的假设。很少有人想用汇编语言编写代码了,因此使用自动化系统(如编译器)来生成代码显然对大多数程序员有好处。
Java从来没有采用过零开销的抽象哲学。相反,HotSpot虚拟机所采用的方法是分析程序的运行时行为,并智能地应用最有利于性能的优化。HotSpot VM的目标是让您能够编写您所习惯的Java代码并遵循良好的设计原则,而不是为了适应VM而扭曲您的程序。
即时编译JIT【 Just-in-Time】简介
Java程序在字节码解释器中开始执行,其中的指令在虚拟堆栈机器上执行。这种来自CPU的抽象提供了类文件可移植性的好处,但是要获得最大的性能,您的程序必须直接在CPU上执行,并利用其本机特性。
HotSpot通过将程序单元从解释的字节码编译为本机代码来实现此目的。 HotSpot VM中的编译单元是方法和循环。 这称为即时(JIT)编译。
JIT编译的工作原理是在应用程序以解释模式运行时监视它,并观察最经常执行的代码部分。在此分析过程中,将捕获程序化跟踪信息,以便进行更复杂的优化。一旦某个特定方法的执行通过了一个阈值,分析器就会寻找编译和优化该特定代码段的方法。
JIT编译方法有许多优点,但其中最主要的优点之一是,它将编译器优化决策基于解释阶段收集的跟踪信息,从而使HotSpot能够进行更明智的优化。
不仅如此,HotSpot还拥有数百年(或更长时间)的工程开发历史,几乎每个新版本都添加了新的优化和好处。这意味着任何运行在HotSpot新版本之上的Java应用程序都可以利用VM中提供的新性能优化,甚至不需要重新编译。
TIP:在将Java源代码转换为字节码并执行了(JIT)编译的另一个步骤之后,实际执行的代码与编写时的源代码相比发生了很大的变化。这是一个关键的见解,它将推动我们处理与性能相关的调查的方法。在JVM上执行的JIT编译的代码可能与原始Java源代码完全不同.
一般的情况是,像c++这样的语言(以及Rust)往往具有更可预测的性能,但代价是将大量低层次的复杂性强加给用户。
请注意,“更可预测”并不一定意味着“更好”。AOT编译器生成的代码可能可能需要运行很多种处理器,并且可能无法假定特定的处理器特性是可用的。
使用配置文件引导优化(profile-guided optimization, PGO)的环境,如Java,有可能以大多数AOT平台根本不可能的方式使用运行时信息。这可以提高性能,比如动态内联【dynamic inlining】和优化远程虚拟调用【optimizing away virtual calls】。HotSpot甚至可以在VM启动时检测它所运行的CPU类型,并且可以使用这些信息来支持针对特定处理器特性的优化(如果有的话)。
TIP:检测精确的处理器的功能的技术称为JVM内嵌原语【JVM intrinsics】,不要与synchronized关键字引入的内部锁混淆。
关于PGO和JIT编译的完整讨论可以在第9章和第10章中找到。
HotSpot采用的复杂方法对大多数普通开发人员来说是一个很大的好处,但是这种折衷(放弃零开销抽象)意味着在高性能Java应用程序的特定情况下,开发人员必须非常小心地避免对Java应用程序实际执行方式的“常识”推理和过于简单的心理模型。
NOTE:分析一小部分Java代码(微基准测试)的性能通常比分析整个应用程序更困难,而且是大多数开发人员不应该承担的一项非常专门化的任务。我们将在第五章讨论这个问题。
HotSpot的编译子系统【compilation subsystem】是虚拟机提供的两个最重要的子系统之一。 另一种是自动内存管理【automatic memory management】,这是Java早期的主要卖点之一。
2.4 JVM Memory Management:JVM内存管理
在C、c++和Objective-C等语言中,程序员负责管理内存的分配和释放。管理内存和对象生命周期的好处是更具确定性的性能,并且可以将资源生命周期与对象的创建和删除联系起来。 但是这些好处的代价也是巨大的 - 为了准确性,开发人员必须能够准确地计算内存。
不幸的是,几十年的实践经验表明,许多开发人员对内存管理的习惯用法和模式理解不足。c++和Objective-C的后续版本使用标准库中的智能指针风格的用法改进了这一点。然而,在创建Java时,糟糕的内存管理是应用程序错误的主要原因。这导致开发人员和管理人员担心花在处理语言特性而不是为业务交付价值上的时间。
Java希望通过使用称为垃圾收集(GC)的进程引入自动管理的堆内存来帮助解决这个问题。简单地说,垃圾收集是一个非确定性的处理,当JVM需要更多内存进行分配时,它会被触发以恢复和重用不再需要的内存。
然而,GC背后的故事并不是那么简单,在Java的历史进程中已经开发并应用了各种垃圾收集算法。GC是有代价的:当它运行时,它通常会停止整个世界【stops the world】,这意味着在GC进行时应用程序会暂停。通常,这些暂停时间被设计得非常小,但是当应用程序受到压力时,这个时间会增加。
垃圾收集是Java性能优化中的一个主要主题,因此我们将在第6、7和8章详细介绍Java GC。
2.5 Threading and the Java Memory Model:线程和Java内存模型
Java在其第一个版本中带来的主要进步之一是内置了对多线程编程的支持。Java平台允许开发人员创建新的执行线程。例如,在Java 8语法中:
Thread t = new Thread(() -> {System.out.println("Hello World!");});
t.start();
不仅如此,Java环境本身就是多线程的,JVM也是。这在Java程序的行为中产生了额外的、不可减少的复杂性,并且使性能分析人员的工作更加困难。
在大多数主流JVM实现中,每个Java应用程序线程都精确地对应于一个专用的操作系统线程。另一种方法是使用共享线程池来执行所有Java应用程序线程(该方法也被称为green threads),事实证明这种方法不能提供可接受的性能配置文件,并且增加了不必要的复杂性。
NOTE:可以安全地假设每个JVM应用程序线程都由一个惟一的OS线程支持,这个OS线程是在相应的线程对象上调用start()方法时创建的。
Java的多线程方法可以追溯到20世纪90年代末,它具有以下基本的设计原则:
- Java进程中的所有线程共享一个公共的被垃圾收集的堆。
- 一个线程创建的任何对象都可以被具有该对象引用的任何其他线程访问。
- 对象在默认情况下是可变的;也就是说,对象字段中保存的值可以更改,除非程序员显式地使用final关键字将其标记为不可变。
Java内存模型(JMM)是一个正式的内存模型,它解释了不同的执行线程如何看到对象中保存的值的变化。也就是说,如果线程A和B都有对象obj的引用,并且线程A更改了它,那么线程B中观察到的值会发生什么变化?
这个看似简单的问题实际上比它看起来更复杂,因为操作系统的调度器(我们将在第3章中讨论)可以强制从CPU核【CPU cores】中清除线程。这可能导致另一个线程在原始线程完成处理之前开始执行和访问对象,并可能看到该对象处于损坏或无效的状态。
在并发代码执行期间,Java核心提供的针对这种潜在对象损坏的惟一防御方式是互斥锁,在实际应用程序中使用这种锁可能非常复杂。第12章详细介绍了JMM的工作原理,以及使用线程和锁的实用性。
2.6 Meet the JVMs
许多开发人员可能只熟悉Oracle生成的Java实现。我们已经遇到了来自Oracle实现的虚拟机HotSpot。然而,我们将在本书中以不同深度讨论其他几种实现:
- OpenJDK:OpenJDK是一个有趣的特例。它是一个开源(GPL)项目,提供了Java的参考实现。该项目由Oracle领导和支持,并为其Java版本提供了基础。
- Oracle:Oracle的Java是最广为人知的实现。它基于OpenJDK,但在Oracle的专有许可下进行了重新授权。对Oracle Java的几乎所有更改都是从提交到OpenJDK公共存储库开始的(尚未公开的安全修复除外)。
- Zulu:Zulu是一个免费的(gpl许可的)OpenJDK实现,完全通过java认证,由Azul Systems提供。它不受专有许可证的限制,可以自由地重新分发。Azul是为数不多的为OpenJDK提供付费支持的供应商之一。
- IcedTea:Red Hat是第一个基于OpenJDK生成完全经过认证的Java实现的非oracle供应商。IcedTea是完全认证的,可以重新分销。
- Zing:Zing是一种高性能专有JVM。它是经过完全认证的Java实现,由Azul Systems生产。它只支持64位Linux,并且是为具有大堆(10s的100 GB)和大量CPU的服务器类系统设计的。
- J9:IBM的J9最初是作为专有JVM出现的,但在其生命的中途是开源的(就像HotSpot一样)。它现在构建在Eclipse开放运行时项目(OMR)的基础上,并形成了IBM专有产品的基础。它完全符合Java认证。
- Avian:就认证而言,Avian实现并不是100%符合Java。之所以将其包含在这个列表中,是因为它是一个有趣的开源项目,并且对于那些希望了解JVM如何工作的细节(而不是作为一个100%可生产的解决方案)的开发人员来说,它是一个很好的学习工具。
- Android:谷歌的Android项目有时被认为是“基于Java的”。然而,实际情况要复杂一些。Android最初使用不同的Java类库实现(来自clean-room Harmony项目)和交叉编译器来为非jvm虚拟机转换不同的(.dex)文件格式。
在这些实现中,本书的大部分重点放在HotSpot上。这些内容同样适用于Oracle Java、Azul Zulu、Red Hat IcedTea和所有其他openjdk派生的jvm。
NOTE:在比较类似的版本时,各种基于HotSpot的实现之间基本上没有性能相关的差异。
我们还包括一些与IBM J9和Azul Zing相关的材料。 这旨在提供对这些替代方案的认识,而不是明确的指南。 有些读者可能希望更深入地探索这些技术,并鼓励他们以通常的方式设定绩效目标,然后进行衡量和比较。
Android正在转向使用OpenJDK 8类库,并在Android运行时直接支持。 由于这个技术堆栈与其他示例相差甚远,因此我们不会在本书中进一步考虑Android。
关于许可的说明
我们将讨论的几乎所有JVM都是开源的,事实上,其中大多数都来自GPL许可的HotSpot。 例外的是IBM的Open J9,它是Eclipse许可的,而Azul Zing是商业的(尽管Azul的Zulu产品是GPL)。
Oracle Java(从Java 9开始)的情况稍微复杂一些。 尽管源自OpenJDK代码库,但它是专有的,并非开源软件。 Oracle通过让OpenJDK的所有贡献者签署许可协议来实现这一目标,该许可协议允许双重许可他们对OpenJDK的GPL和Oracle的专有许可的贡献。
Oracle Java的每个更新版本都被视为OpenJDK主线的一个分支,然后在未来的版本中不会在分支上进行修补。 这可以防止Oracle和OpenJDK的分歧,并且可以解释Oracle JDK和基于相同源的OpenJDK二进制文件之间缺乏有意义的差异。
这意味着Oracle JDK和OpenJDK之间唯一真正的区别就是许可证。 这看起来似乎无关紧要,但Oracle许可证包含一些开发人员应该注意的条款:
- Oracle不授予在您自己的组织之外重新分发其二进制文件的权利(例如,作为Docker映像)。
- 在没有得到Oracle二进制文件的同意(通常是指支持合同)的情况下,不允许对其应用二进制补丁。
Oracle还提供了其他一些商业特性和工具,这些特性和工具只适用于Oracle的JDK,并且在其许可的范围内。但是,随着将来Oracle发布Java版本,这种情况将会发生变化,我们将在第15章中对此进行讨论。
在计划新的绿色部署时,开发人员和架构师应该仔细考虑他们对JVM供应商的选择。一些大型组织,特别是Twitter和阿里巴巴,甚至维护他们自己的OpenJDK私有构建,尽管许多公司无法完成所需的工程工作。
2.7 Monitoring and Tooling for the JVM
JVM是一个成熟的执行平台,它为运行中的应用程序的检测、监视和可观察性提供了许多技术选择。这些类型的JVM应用程序工具可用的主要技术有:
- Java Management Extensions (JMX)
- Java agents
- The JVM Tool Interface (JVMTI)
- The Serviceability Agent (SA)
JMX是一种功能强大的通用技术,用于控制和监视jvm及其上运行的应用程序。它提供了从客户机应用程序以一般方式更改参数和调用方法的能力。不幸的是,完整的论述超出了本书的范围。然而,JMX(及其相关的网络传输,RMI)是JVM管理功能的一个基本方面。
Java代理是用Java编写的工具组件(因此得名),它使用Java .lang中的接口。用来修改字节码的方法。要安装代理,请向JVM提供一个启动标志:
-javaagent:<path-to-agent-jar>=<options>
该代理JAR必须包含一个清单并包含Premain-Class属性。此属性包含代理类的名称,代理类必须实现充当Java代理注册钩子的公共静态premain()方法。
如果Java插装的API不够,则可以使用JVMTI。 这是JVM的本机接口,因此使用它的代理必须使用本机编译语言(本质上是C或C ++)编写。 它可以被认为是一种通信接口,允许本机代理监视JVM并通知事件。 要安装本机代理,请提供稍微不同的标志:
-agentlib:<agent-lib-name>=<options>
或者
-agentpath:<path-to-agent>=<options>
JVMTI代理必须用本机代码编写,这意味着编写可能破坏正在运行的应用程序甚至使JVM崩溃的代码要容易得多。
在可能的情况下,通常最好在JVMTI代码上编写Java代理。代理的编写要简单得多,但是一些信息不能通过Java API获得,访问这些数据JVMTI可能是惟一的可能。
最后一种方法是可服务性代理。这是一组api和工具,可以公开Java对象和HotSpot数据结构。
SA不需要在目标VM中运行任何代码。HotSpot SA使用符号查找和进程内存读取等原语来实现调试功能。SA能够调试实时Java进程和核心文件(也称为崩溃转储文件)。
VisualVM
JDK附带了许多有用的附加工具以及著名的二进制文件,如javac和java。一个经常被忽略的工具是VisualVM,它是一个基于NetBeans平台的图形化工具。
TIP:jvisualvm是早期Java版本中现已过时的jconsole工具的替代品。如果您仍然使用jconsole,那么您应该迁移到VisualVM(有一个兼容性插件允许jconsole插件在VisualVM中运行)。
Java的最新版本已经提供了VisualVM的稳定版本,JDK中提供的版本现在通常已经足够了。但是,如果需要使用最新版本,可以从http://visualvm.java.net/下载最新版本。下载之后,您必须确保visualvm二进制文件被添加到您的路径中,否则您将获得JRE默认二进制文件。
TIP:从Java 9开始,VisualVM将从主发行版中删除,因此开发人员必须单独下载二进制文件。
当VisualVM第一次启动时,它将对正在运行的机器进行校准,因此不应该有其他应用程序在运行,这可能会影响性能校准。校准后,VisualVM将完成启动,并显示一个闪屏。VisualVM最熟悉的视图是Monitor屏幕,类似于图2-4所示。
图2 - 4 VisualVM显示屏
VisualVM用于对正在运行的进程进行实时监视,它使用JVM的附加机制。 根据进程是本地进程还是远程进程,这种方式略有不同。
本地流程相当简单。 VisualVM将它们列在屏幕的左侧。 双击其中一个会使其在右侧窗格中显示为新选项卡。
要连接到远程进程,远程端必须接受入站连接(通过JMX)。 对于标准Java进程,这意味着jstatd必须在远程主机上运行(有关更多详细信息,请参阅jstatd的手册页)。
NOTE:许多应用服务器和执行容器直接在服务器中提供与jstatd等效的功能。这样的进程不需要单独的jstatd进程。
要连接到远程进程,请输入选项卡上使用的主机名和显示名。要连接的默认端口是1099,但这可以很容易地更改。
VisualVM为用户提供了五个选项卡:
- Overview:提供有关Java进程的信息摘要。这包括传入的完整标志和所有系统属性。它还显示正在执行的Java版本。
- Monitor:这是与遗留JConsole视图最相似的选项卡。它展示了JVM的高级遥测,包括CPU和堆的使用情况。它还显示了加载和卸载的类的数量,以及运行的线程数量的概述。
- Threads:正在运行的应用程序中的每个线程都显示一个时间轴。这包括应用程序线程和VM线程。可以通过少量历史记录看到每个线程的状态。如果需要,还可以生成线程转储。
- Sampler and Profiler:在这些视图中,可以访问简化的CPU和内存利用率抽样。这将在第13章中详细讨论。
VisualVM的插件体系结构允许将其他工具轻松添加到核心平台,以增强核心功能。其中包括允许与JMX控制台交互并桥接到遗留JConsole的插件,以及非常有用的垃圾收集插件VisualGC。
2.8 Summary
在这一章中,我们快速浏览了JVM的总体结构。它只可能触及到一些最重要的主题,实际上这里提到的每个主题背后都有一个丰富、完整的故事,值得进一步研究。
在第3章中,我们将讨论操作系统和硬件如何工作的一些细节。这为Java性能分析人员理解观察到的结果提供了必要的背景。我们还将更详细地查看时序子系统,作为VM和本机子系统如何交互的完整示例。