探秘 JVM:监控与诊断工具

JDK 在给提供基础的 java 依赖库的同时,也在 bin 目录下提供了一系列的小工具,除了我们常用的 java 和 javac 以外,还包含许多对 JVM 进行性能监控和故障诊断的工具。这些工具能够为我们日常程序开发和问题排查提供极大的便利,主要包含以下几种:

工具 描述
jps 显示系统运行中的 JVM 进程列表
jstat 用于收集 JVM 各方面的运行数据(类加载、GC、JIT 编译等)
jinfo 查看和编辑 JVM 配置信息(JVM 启动参数、系统环境变量等)
jmap 生成 JVM 的堆转储快照(heapdump 文件)
jhat 用于分析 jmp 命令生成的 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果
jstack 生成 JVM 的线程转储快照(通常所说的 threaddump 文件或 javacore 文件),用于判断是否存在死锁、死循环,以及阻塞等情况
jconsole 可视化 java 监视与管理控制台,基于 JMX
jvisualvm 多合一故障处理工具,jconsole 的增强版,自 JDK 9 开始不再随 JDK 默认提供,需要独立下载,地址:http://visualvm.github.io/
阅读全文

探秘 JVM:编译与优化

以 java 语言为例,JVM 针对 java 程序的优化可以发生在编译期和运行期,相应的优化操作分别发生在 javac 编译器在将 java 源程序编译成字节码期间,以及运行时即时编译器(JIT: Just In Time Compiler)将字节码编译成本地机器码期间。此外,还有一类编译器可以将源程序直接编译成与目标机器指令集相关的二进制代码,此类编译器称为提前编译器。运行期依赖于 JIT 的编译优化的主要目的在于提升程序的执行效率,而编译期优化的主要目的在于支持 java 语法糖,提升语言的易用性和编码效率。如果以字节码所处的位置作为参考线,那么编译期的编译可以称为前端编译,而即时编译和提前编译合起来则可以称为后端编译。

阅读全文

探秘 JVM:字节码执行引擎

在不同的虚拟机实现中,执行引擎在执行 java 代码时可能会有 解释执行 (通过解释器执行)和 编译执行 (通过 JIT 生成本地代码执行)两种选择,也可能是二者兼备,但不管采用哪种方式执行,当我们调用一个方法的时候,都需要确定目标方法的具体版本,因为在面向对象语言中存在封装、继承和多态的三大特性,一个方法可能因为重载或覆盖而存在多个版本。

方法调用阶段的主要工作是确定被调用方法的版本,而非执行具体的方法。字节码文件中存储的是方法的符号引用,只有将符号引用解析成直接引用(运行时方法的入口内存地址)才能确定具体调用的方法是谁,这个映射的过程有的发生在类加载的解析阶段,有的则发生在运行期间。

阅读全文

探秘 JVM:类加载机制

JVM 的类加载机制描述了类数据从字节码文件加载到内存,并对其进行校验、解析、初始化,并最终成为能够直接被 JVM 使用的 java 数据类型的过程。

类的整个生命周期可以分为 加载、连接(验证、准备、解析)、初始化、使用、卸载 5 个阶段(加载、连接和初始化构成了类加载全过程),其中连接又可以细分为验证、准备、解析 3 个阶段。如下图所示:

image

阅读全文

探秘 JVM:字节码文件结构与指令

Class 文件(也叫字节码文件)与 JVM 一起支撑着 java 程序的跨平台运行,虽然两者目前主要服务于 java 语言,但是其最初的设计是为编程语言构建一个跨平台的基础运行环境,任何语言只要可以被编译为字节码文件,就可以依托于这一套基础运行环境,实现平台无关性。

字节码文件结构

字节码文件是一组以 8 位字节为基础单位的二进制流,各个数据项目(见下图)严格按照顺序紧凑排列,中间没有分隔符,当遇到需要占用 8 位字节以上空间的数据项时,则按照 高位优先 的方式分割成若干个 8 位字节进行存储。

阅读全文

探秘 JVM:垃圾收集机制

JVM 中的程序计数器、java 虚拟机栈、本地方法栈均属于线程私有,其生命周期控制在线程生命周期范围内,并且 java 虚拟机栈和本地方法栈中的的栈帧会随着对应方法的执行而出栈和入栈,由生到灭,所以这些区域的内存使用是确定性的(编译期已知)。然而,Java 堆和方法区是线程共享的,对象的创建也是动态的,所以垃圾收集的主战场集中在 java 堆和方法区。

相对于 java 堆而言,方法区的垃圾收集性价比要低很多,JVM 规范甚至不强制要求 JVM 在方法区实现垃圾收集。不过对于复杂应用,尤其是大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP,以及 OSGi 这类频繁自定义类加载器的场景中,对于方法区实现垃圾收集还是有必要的。方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。对于一个类型是否不再被使用,JVM 在判定时需要同时满足以下 3 个条件:

  1. 该类所有的实例都已经被回收了,也就是说堆中不存在该类及其派生子类的任何实例。
  2. 加载该类的类加载器已经被回收了。
  3. 该类对应的 Class 类对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
阅读全文

探秘 JVM:运行时数据区

JVM 内存区域从概念模型上主要分为 堆、元空间、java 虚拟机栈、本地方法栈、程序计数器 五大模块,其中前两者属于线程共享,而后三者属于线程私有,如下图(以 HotSpot 虚拟机为例):

image

说明:元空间在 java 8 中引入,替换之前的方法区。

阅读全文

Java 语言实现单例模式的若干种方式

在面向对象程序设计中,只要内存允许我们通常都可以为一个对象创建任意多个实例,但是一些场景下这不一定是一件好的事情。考虑一个文件类,在被使用之前需要从磁盘加载一定量的数据,我们肯定不希望每次调用该对象都去执行数据加载的操作,不仅费时,而且同样的数据因为一个对象的实例化操作就要在内存中重复存储一份,显然是对内存的一种浪费。这个时候我们就希望对数据的加载操作只执行一次,后面所有的调用都是对这份数据的复用,而这也正式单例模式的应用场景。

如果可以任意的创建对象,那么当我们希望内存中仅保有一份实例,就必须让所有的程序开发人员维持一个约定,只实例化该对象一次,然而现实是对象是可以任意被实例化的,约定开发人员是不现实的。这个时候我们就需要从开发人员手中剥夺对目标对象实例化的权利,而由单例模式去控制对象的创建,并暴露给开发人员一个获取对象实例的入口。

阅读全文

Java 8th 函数式编程:默认接口方法

Java 8th 可以看做是 java 版本更新迭代过程中变化最大的几个版本之一(与时俱进,方能不灭),但是经过这么多年的发展和迭代,java 的源码俨然已是一个庞然大物,要在这样庞大的体积上大动干戈必定不易。所以当第一次看到默认接口方法的时候,我第一感觉就是这是设计人员在填自己之前挖的坑。

从前几篇的讲解中我们知道 8th 在现有的接口上添加了许多方法,比如 List 的 sort(Comparator<? super E> c) 方法。如果按照 8th 之前接口的设计思路,当给一个接口添加方法声明的时候,实现该接口的类都必须为该新添加的方法添加相应的实现(或将自己设置为抽象类)。考虑兼容性这样是不可取的,所以说这是一个坑,而新的特性又要求不得不为接口添加一些新的方法,为了兼得鱼和熊掌,设计人员提出了默认接口方法的概念。

阅读全文

Java 8th 函数式编程:流式数据处理

第一次接触到流式数据处理的时候,第一感觉是流式数据处理让集合操作变得简洁了许多,通常我们需要多行代码才能完成的操作,借助于流式数据处理可以在一行中实现。比如我们希望对一个包含整数的集合筛选出所有的偶数,并将其封装成为一个新的集合返回,那么在 8th 之前,我们需要通过如下代码实现:

1
2
3
4
5
6
List<Integer> evens = new ArrayList<>();
for (final Integer num : nums) {
if (num % 2 == 0) {
evens.add(num);
}
}
阅读全文