乐趣区

Java程序员进阶必备 – JVM快速入门

这是我在公司给团队小伙伴一次技术小分享。新手司机可以收藏、学习,老司机可以批评指正。ps: 内容参考了众多优秀博文、书籍,部分图片来源于博文,如有侵权请联系删除。
1. 前言
为什么 Java 可以实现所谓的“一次编写,到处运行”,主要是因为虚拟机的存在。Java 虚拟机负责 Java 程序设计语言的安全特性和平台无关性。Java 虚拟机屏蔽了与具体操作系统平台相关的信息,使得 Java 语言编译器只需要生成在 Java 虚拟机上运行的字节码,就可以在多种平台上不加修改地运行。Java 虚拟机使得 Java 摆脱了具体机器的束缚,使跨越不同平台编写程序成为了可能。
Java 虚拟机基本上都是 JDK 自带的虚拟机 HotSpot,这款虚拟机也是目前商用虚拟中市场份额最大的一款虚拟机,可以通过在命令行程序中输入 java -version 来查看:

其实市面上还有很多别的优秀的虚拟机。Sun 公司除了有大名鼎鼎的 HotSpot 外,还有 KVM、Squawk VM、Maxine VM,BEA 公司有 JRockit VM、IBM 公司有 J9 VM 等等。
2. 内存模型(JMM)
Java 虚拟机(JVM)内部定义了程序在运行时需要使用到的内存区域。内存区域主要分为主内存和工作内存。主内存即主机物理内存,工作内存按作用域可划分为线程独享区和线程共享区。
宏观来看是这样子的,如下图:

Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量和主内存副本拷贝,线程对变量所有的操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,
Jvm 运行时内存模型,不包含主内存。如下图:

下面将会逐一详细介绍上面的内存区域。
2.1 线程独享区

虚拟机栈,即 Java stack。声明周期和线程相同,方法执行会创建栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。
本地方法栈,即 Native method stack。作用和虚拟机栈一样,不过面向的是本地方法。不属于 jvm 规范,hotspot 没有这块区域。
程序计数器,即 Program counter register。区域小,是线程执行的字节码的行号指示器,相当于存的是一条条的指令。

2.2 线程共享区

堆用于存放对象实例,是所有内存区域中最大的一块。实际上这块内存还被划分的更细:新生代和老年代,空间占用比例为 1 : 2,新生代再细致一点有:Eden 空间、From Survivor(S0)、To Survivor(S1),空间占用比例为 8 : 1 : 1。进一步划分的目的是更好地回收内存,或者更快地分配内存。

方法区用于存放虚拟机加载的类信息、常量(常量池)、静态变量、即使编译器编译后的代码等数据,即“HotSpot”的永久代。在 JDK 7 之后,我们使用的 HotSpot 应该就没有永久代这个概念,采用的是 Native Memory 来实现方法区的规划。

2.3 直接内存
直接内存,即主内存,并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,可以直接使用 Native 函数库直接分配堆外内存,这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
3. 垃圾回收(GC)
哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何确定要回收的对象,以及采用什么样的策略去回收,适合什么样的场景,这是我们要关注的几个点。
3.1 确定对象算法
了解一个对象满足什么样的条件就认为是可被回收的对象是重要的一环。
3.1.1 引用计数法
给对象添加一个引用计数器,每当一个地方引用这个对象时,计数器值 +1;当引用失效时,计数器值 -1。当计数值为 0 的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java 中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。
public class ReferenceCountingGC{
public static void main(String[] args){
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}
3.1.2 可达性分析法
这个算法的基本思想是通过一系列称为 GC Roots 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链(即 GC Roots 到对象不可达)时,则证明此对象是不可用的。在 Java 语言中可以作为 GC Roots 的对象包括:

虚拟机栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即 Native 方法)引用的对象

3.2 回收算法
3.2.1 复制算法
采用内存空间比例为 1 : 1 的 2 块内存上,只使用其中一块,当需要回收时,将存活的对象复制到另外一块,原有的那一块内存空间直接全部清除。这种算法比较简单粗暴,缺点也很明显,内存只能使用 1 /2。

3.2.2 标记 - 清除算法
对标识为可清理的对象直接进行清理操作,不会发生复制或者移动,相对复制算法成本比较小。缺点:对标记的对象清除之后,由于未移动过对象,将产生大量不连续的内存碎片,当大对象出现时,由于没有足够的连续内存导致不得不对碎片进行整理,也就是 Full GC。

3.2.3 标记 - 整理算法
标记 - 整理算法能够解决标记 - 清除算法带来的碎片化问题

3.3 垃圾收集器
根据上面提到的回收算法,jvm 内置了拥有众多的收集器来适应不同的场景。根据运行环境的物理配置信息,会自动的选择使用 client 模式、server 模式的垃圾收集器,还可以继续根据运行时数据的情况来筛选适合当前场景的垃圾收集器。

上图展示了新生代和老年代的几种垃圾收集器,其中有连线的代表是可以组合使用的。
4. jvm 参数
4.1 参数规则

标准参数:例如 javap -verbose

- X 参数:所有的这类参数都以 - X 开始,例如常用的 -Xmx,
布尔类型的参数: + 或 -,然后才设置 JVM 选项的实际名称。例如,-XX:+ 类似 true,表启用,-XX:- 类似 false。
非布尔值的参数:如 string 或者 integer,我们先写参数的名称,后面加上 =,最后赋值。例如 -XX:ParamName=Value

4.2 常用参数清单

-Xms 即 -XX:InitialHeapSize 的缩写,指定 JVM 的初始内存大小
-Xms20M 设置 JVM 启动内存的最小值为 20M,必须以 M 为单位

-Xmx 即 -XX:MaxHeapSize 的缩写,指定 JVM 的最大堆内存大小
-Xmx20M 表示设置 JVM 启动内存的最大值为 20M,单位为 M,将 -Xmx 和 -Xms 设置为一样可以避免 JVM 内存自动扩展。

-verbose:gc 输出虚拟机中 GC 的详细情况

-Xss128k 设置虚拟机栈的大小为 128k

-Xoss128k 设置本地方法栈的大小为 128k。HotSpot 不区分虚拟机栈和本地方法栈,因此对于 HotSpot 这个参数无效。

-XX:PermSize=10M JVM 初始分配的永久代的容量,必须以 M 为单位

-XX:MaxPermSize=10M JVM 允许分配的永久代的最大容量,必须以 M 为单位,大部分情况下这个参数默认为 64M

-Xnoclassgc 关闭 JVM 对类的垃圾回收

-XX:+TraceClassLoading 查看类的加载信息

-XX:+TraceClassUnLoading 查看类的卸载信息

-XX:NewRatio=4 设置年轻代:老年代的大小比值为 1:4,这意味着年轻代占整个堆的 1 /5

-XX:SurvivorRatio=8 设置 2 个 Survivor 区:1 个 Eden 区的大小比值为 2:8,这意味着 Survivor 区占整个年轻代的 1 /5,这个参数默认为 8

-Xmn20M 设置年轻代的大小为 20M

-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时 Dump 出当前的堆内存转储快照

-XX:+UseG1GC 让 JVM 使用 G1 垃圾收集器

-XX:+PrintGCDetails 在控制台上打印出 GC 具体细节

-XX:+PrintGC 在控制台上打印出 GC 信息

-XX:PretenureSizeThreshold=3145728 对象大于 3145728(3M)时直接进入老年代分配,单位为 byte

-XX:MaxTenuringThreshold=1 对象年龄大于 1,自动进入老年代

-XX:CompileThreshold=1000 一个方法被调用 1000 次之后,会被认为是热点代码,并触发即时编译

-XX:+PrintHeapAtGC 可以看到每次 GC 前后堆内存布局

-XX:+PrintTLAB 可以看到 TLAB 的使用情况

-XX:+UseSpining 开启自旋锁

-XX:PreBlockSpin 更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁

4.3 使用参数

命令行
java -jar projectName.jar -verbose:gc -Xms20M -Xmx20M

Eclipse

IDEA

5. 常用工具
所谓工具,就是通过一些简便的脚本去执行程序去呈现结果数据。这里涉及到一些语法格式。
统一语法都类似这种形式:$ cmd [option id[ pid | vmid |hostid]]
其中 hostid 为可选项,默认为 localgost, vmid/pid 依赖 jps 获取
5.1 jps
jps 是 Java Process Status 的缩写,查看当前 java 进程的运行状态快照。理解为 linux 命令 ps 的 java 版本

-m 运行时传入的参数

-v 虚拟机参数

-l 运行的主类全限定名或 jar 包名称

示例
jps -mlv

5.2 jstat
jstat 是 JVM Statistics Monitoring Tool 的缩写,查看虚拟机统计信息监控数据,如类信息、内存、垃圾收集、JIT 编译等

-gc 显示 gc 的信息,查看 gc 次数以及时间

-class 监视类装载、卸载数量、总空间以及类装载所耗费的时间

-gc 监视 Java 堆状况,包括 Eden 区、两个 Survivor 区、老年代、永久带等的容量、已用空间、GC 时间合计等信息

-gccapacity 监视内容基本与 -gc 相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间

-gcutil 监视内容基本与 -gc 相同,但输出主要关注已使用的空间占总空间的百分比

-gccause 与 -gcutil 功能一样,但是会额外输出导致上一次 GC 产生的原因

-gcnew 监视新生代 GC 状况

-gcnewcapacity 监视内容基本与 -gcnew 相同,但输出主要关注使用到的最大、最小空间

-gcold 监视老年代 GC 状况

-gcoldcapacity 监视内容基本与 -gcold 相同,但输出主要关注使用到的最大、最小空间

-gcpermcapacity 输出永久代使用到的最大、最小空间

-compiler 输出 JIT 编译器编译过的方法、耗时等信息

-printcompilation 输出已经被 JIT 编译的方法

jstat -gcutil pid 依赖 jps 获得 pid 查看类装载、内存、垃圾收集、jit 编译信息

示例

jstat -gcutil 3333 1000 10 对 pid 为 3333 的进程每隔 1 秒打印 1 次,总打印 10 次

5.3 jinfo
jinfo 即 Configuration Info for Java,实时查看和调整 jvm 参数

-flag <name> 打印 jvm 参数的值

-flag [+|-]<name> 启用 / 禁用 jvm 参数

-flag <name>=<value> 修改 jvm 参数值

-flags <pid> 打印所有 jvm 参数值

-sysprops <pid> 打印 java 系统属性

<no option> <pid> 打印上面所有信息

示例

jinfo -flags 7298 打印 pid 为 7298 的虚拟机运行时的所有参数
xxx

5.4 jmap
jmap 即 Memory Map for Java,内存映像工具用于生成堆转存快照

-dump 生成 Java 堆转储快照。格式为 -dump:[live,]format=b,file=<filename>,其中 live 自参数说明是否只 dump 出存活的对象

-finalizerinfo 显示在 F -Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux 和 Solaris 系统有效

-heap 显示 Java 堆详细信息,如使用哪种收集器、参数配置、分代状况等。只在 Linux 和 Solaris 系统有效

-histo 显示堆中对象统计信息,包括类、实例数量、合计容量

-permstat 以 ClassLoader 为统计口径显示永久代内存状态。只在 Linux 和 Solaris 系统下有效

-F 当虚拟机进行对 -dump 选项没有响应时,可使用这个选项强制生成 dump 快照。只在 Linux 和 Solaris 系统下有效

示例

jmap -dump:live,format=b,file=heap.bin 7298 将 pid 为 7298 的虚拟机内活对象导出为 heap.bin 二进制文件

5.5 jhat
jhat 即 JVM Heap Analysis Tool,虚拟机堆分析工具

xxx

示例

jhat /data/dump.bin 分析导出的堆快照

5.6 jstack
jstack 即 Stack Trace for Java,堆栈跟踪工具,查看虚拟机线程快照。目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。

-F 即 force,强制打印线程快照信息

-m 即 mixed mode,同时打印 java 框架信息和本地库信息

-l 即 long listing,打印更长(更多)的列信息

示例

jstack -F 7298
jstack -l 7298
jstack -m 7298

退出移动版