Java虚拟机的基本认识

虚拟机,从字面上的意义来理解就是一台虚拟的计算机,虚拟机上运行的软件都受限于虚拟机提供的资源中。

虚拟机可以分为两类,系统虚拟机和程序虚拟机,例如VMware提供了一个可以运行的操作系统的软件平台,这就是典型的系统虚拟机,我们可以像使用物理的计算机一样来使用VMware中提供的虚拟机。而Java虚拟机就是为了执行某个计算机程序而被设计出来的程序虚拟机,在Java虚拟机中执行的指令被称为Java字节码指令。

一个Java程序被编译成字节码,就可以通过Java虚拟机运行在各个操作系统平台中;也就是说Java程序通过Java虚拟机实现了跨平台的特性,如下图所示:

正是因为有了Java虚拟机,我们的Java程序在不同操作系统平台中运行时不需要重新编译,只要Java程序被编译为在Java虚拟机上运行的字节码,就可以在不同操作系统平台上运行。

Java虚拟机基本结构

Java虚拟机的结构由以下几部分组成:

类加载器子系统

类加载器子系统负责从文件或者网络中加载Class信息,加载后的类信息存放在方法区中。

对于任意一个类,都需要由加载他的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

类加载器子系统中的类加载器一共有三种类型,分别是启动类加载器、扩展类加载器和应用程序类加载器,各加载器的作用如下:

  • 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。该加载器由C++语言实现。
  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径上的指定类库,这是程序中默认的类加载器。

当一个类加载器收到了类加载的请求后,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。因此所有的加载请求最终都会到顶层的启动类加载器中,只有当父加载器的加载范围中不需要加载这个类时,子加载器才会尝试自己去加载,这就是类加载器中的双亲委派模型。

双亲委任模型的实现代码实现在java.lang.ClassLoader中的loadClass方法之中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//父类抛出异常表示无法完成加载请求
}

if (c == null) {
//调用自身的findClass()方法进行加载
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分成若干个不同的数据区域,这些数据区域都有各自的用途,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

Java堆是Java程序主要的内存工作区域,堆中存放了Java的对象实例,且堆的空间是所有的线程共享的。

Java堆也是垃圾收集器管理的主要区域。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;且新生代一般还以8:1:1的空间划分出了一个Eden区和两个Survivor区。

方法区

方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java栈

每个线程都会有一个私有的Java栈,Java栈的生命周期与线程相同。每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈

本地方法栈与Java栈类似,其区别在于Java栈用于Java方法的调用,而本地方法栈则用于本地方法的调用。本地方法一般使用C语言编写。

PC寄存器

PC寄存器也是每个线程私有的空间,Java虚拟机会为每一个线程都创建PC寄存器。

如果线程正在执行的是一个Java方法,PC寄存器就会指向当前正在被执行的指令;如果正在执行的是Natvie方法,PC寄存器的值则为undefined。

直接内存

直接内存并不是虚拟机运行时数据区的一部分。但是这部分内存也被频繁的使用,并且也可能导致内存溢出。

在JDK1.4中新加入NIO类,引入了一种基于Channel与Buffer的IO方式,它可以直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样避免了在Java堆和Native对中来回复制数据,能在一些场景中提高性能。

由于直接内存是在堆外的,因此直接内存的大小不会受限于堆的大小,但是系统的物理内存也是有限的,直接内存和堆内存的综合依然受限于操作系统中的最大内存。

执行引擎

在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行和编译执行,也可能两者兼备。

但所有的Java虚拟机的执行引擎的核心功能都是输入字节码文件,然后将其编译为机器码后进行执行。

Java虚拟机配置参数

在启动Java程序时,可以设置特定的Java虚拟机参数来进行系统调优或进行故障排查。

Java虚拟机中的配置参数可以分为三类,分别为标准参数、非标准参数和不稳定参数。

标准参数

Java虚拟机中的标准参数,所有的Java虚拟机实现都必须实现这些参数的功能,并且向后兼容。

可以在命令行中使用 java -help 检索出所有的标准参数:

可以使用标准参数 -version 查看java虚拟机版本信息:

非标准参数

Java虚拟机中的-X参数是非标准参数,Java虚拟机会实现这些参数的功能,但是并不保证所有Java虚拟机实现都满足,且不保证向后兼容;可以通过 java -X 检索出所有的非标准参数:

最常使用的两个非标准参数为-Xms-Xmx分别是设置堆的初始化大小和最大大小。适当的调整jvm的内存大小,可以充分利用服务器资源,让程序得到性能的提升。

不稳定参数

Java虚拟机中的-XX参数是不稳定参数。而且如果在新版本有什么改动也不会发布通知。这些参数中有很多对于我们来讲是非常有用的。

-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型

boolean类型的选项为-XX:+ 打开, -XX:- 关闭。例如-XX:+DisableExplicitGC 表示禁用手动调用gc操作。

非boolean类型的选项通过-XX:= 设定。例如-XX:NewRatio=1 表示新生代和老年代的比值。

事实上不稳定参数也是我们在调优时最经常使用的参数,不稳定参数中的参数可以分为跟踪参数,堆参数及非堆参数3种类别。

跟踪参数

在程序运行的时候,虚拟机提供了一些跟踪系统状态的参数,这些参数能对我们的故障排查有一定帮助;使用给定的参数就可以在程序运行时打印相关日志来帮助我们用于问题的分析。

我们可以添加一些跟踪参数来查看垃圾回收的效果。

简单的打印GC日志的参数为-XX:+PrintGC,在启动参数上添加后,则会打印简单的GC日志。

这个日志中一共进行了4次GC,在第一次GC时,在GC前堆空间的使用量为83133K,在GC后堆空间的使用量为21057K,当前可用的堆空间大小为129536K,最后显示的为本次GC所消耗的时间。

如果需要打印更详细的GC日志信息,可以使用-XX:+PrintGCDetails参数,该参数的输出例子如下:

堆参数

堆空间在Java程序中是非常重要的部分,所以对于堆参数的配置也是对我们的程序性能有着重大影响的。

例如我们可以使用-XX:SurvivorRatio来设置年轻代中Eden区和Survivor区的比例大小以及参数-XX:NewRatio来设置新生代和老年代的比例。

有时在程序的运行过程中,可能会出现内存溢出的异常,可以使用相关参数来打印内存溢出时的堆信息,首先使用参数-XX:+HeapDumpOnOutOfMemoryError开启内存溢出时导出堆相关信息,然后在参数-XX:HeapDumpPath中配置导出堆信息时的路径或文件名。

非堆参数

除了堆内存,我们还可以配置方法区、栈及直接内存的内存参数,合理的配置这些参数,也能对我们的应用程序性能的提高和稳定性产生作用。

使用-XX:MaxMetaspaceSize参数可以设置在JDK1.8中的元数据空间大小。

使用-XX:MaxDirectMemorySize参数可以设置直接内存的最大大小,默认为堆空间的最大大小,如果直接内存的内存溢出也会引起系统的OOM。

使用-XX:+DoEscapeAnalysis参数可以开启或关闭栈空间中对象的逃逸分析。

性能监控

性能是无论如何在什么时候都需要关注的重要指标,所以我们需要有效的监控和诊断性能问题。

Linux下的性能监控

很多Java程序都运行在Linux下,所以我们需要介绍下如何在Linux平台下进行性能监控。

系统整体资源使用情况

top命令经常用来监控Linux的系统状况,比如cpu、内存的使用及进程的信息等等,top命令的输出样例如下:

top命令输出的信息中,可以分为两个部分,上半部分是系统统计信息,下半部分是进程信息。

在系统统计信息中,第一行是任务队列信息,左边部分为系统当前时间,系统运行时间和当前登录用户数,右边的load average表示系统的平均负载,3个值分别表示1分钟、5分钟、15分钟到现在的平均值。

第二行是进程的统计信息,首先显示了所有的进程数,以及运行中的进程和睡眠的进程、停止状态的进程及僵尸进程。

第三行是CPU统计信息,这里显示了不同模式下所占用CPU时间的占用率,这些不同模式分别表示:

  • us:用户空间的CPU占用率
  • sy:内核空间的CPU占用率
  • ni:改变过优先级的用户进程CPU占用率
  • id:CPU空闲的占用率
  • wa:等待输入输出的CPU占用率,也就是等待IO的CPU占用率
  • hi:处理硬件中断的CPU占用率
  • si:处理软件中断的CPU占用率

第四行的Mem及第五行的Swap是内存统计信息,Mem是物理内存使用,Swap是虚拟内存使用。total为全部可用内存,free为空闲内存,used为已使用内存,最后为缓冲区内存。

top命令的下半部分则是进程信息,显示了系统内的进程资源使用情况,其中主要字段的含义如下:

  • PID:进程ID
  • USER:进程所有者的用户名
  • PR:进程的调度优先级
  • NI:进程的nice值,负值表示高优先级,正值表示低优先级
  • VIRT:进程使用的虚拟内存总量,VIRT=SWAP+RES
  • RES:驻留内存大小。驻留内存是进程使用的、未被交换的物理内存大小。
  • SHR:共享内存大小。
  • S:进程状态。
  • %CPU:从上次更新到现在的CPU时间占用率。
  • %MEM:进程使用的物理内存百分比。
  • TIME+:进程使用的CPU时间,精确到百分之一秒。
  • COMMAND:运行进程所使用的命令。命令名/命令行

内存和CPU使用情况

vmstat命令也可以用来监控Linux系统内存及CPU的使用情况,且可以指定采样周期及采用次数。

例如使用vmstat 1 5每秒采样一次,共计5次。

Procs(进程)

  • r:等待运行的进程数
  • b:处在非中断睡眠状态的进程数

Memory(内存)

  • swpd:虚拟内存使用情况
  • free:空闲内存
  • buff:用作缓冲区的内存大小
  • cache:用作缓存的内存大小

Swap

  • si:每秒从磁盘交换区交换到内存的交换页
  • so:每秒从内存交换到磁盘交换区的交换页

IO

  • bi:每秒读取的块数
  • bo:每秒写入的块数

System

  • in:每秒中断数,包括时钟中断
  • cs:每秒上下文切换数

CPU

  • us:用户空间CPU占用率
  • sy:内核空间CPU占用率
  • id:CPU空闲占用率
  • wa:等待输入输出的CPU占用率,也就是等待IO的CPU占用率

IO使用情况

iostat命令将对系统的磁盘操作活动进行监视。它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况。

同样的,iostat命令也可以指定采样周期及采用次数。以下例子采用了每秒采样一次,共计3次。

以上输出信息显示了CPU的使用概况及磁盘IO的信息。

CPU的属性值说明如下:

  • %user:用户空间的CPU占用率
  • %nice:改变过优先级的用户进程CPU占用率
  • %system:内核空间的CPU占用率
  • %iowait:等待输入输出的CPU占用率,也就是等待IO的CPU占用率
  • %idle:CPU空闲的占用率

磁盘IO属性值说明如下:

  • tps:该设备每秒的传输次数
  • kB_read/s:每秒从设备读取的数据量
  • kB_wrtn/s:每秒向设备写入的数据量
  • kB_read: 读取的总数据量
  • kB_wrtn:写入的总数据量

Java程序性能监控

在JDK开发包中提供了一些的辅助工具,这些辅助工具可以帮助我们更好的去解决Java应用程序中出现的问题,在JDK安装目录下的bin目录中可以看到这些辅助工具。

查看Java进程

jps命令与 linux 的 ps 命令有些类似,但是jps命令只会列出系统中的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。

如果直接运行jps命令,那么可以列出Java程序的进程ID及Main函数短名称。

jps命令的参数选项如下:

  • -q:只输出进程 ID
  • -m:输出传入 main 方法的参数
  • -l:输出完全的包名,应用主类名,jar的完全路径名
  • -v:输出jvm参数

查看堆内存使用情况

jstat命令可以对Java应用程序的资源和性能进行实时的命令行的监控,包括了对堆大小和垃圾回收情况的监控。

jstat命令的参数选项如下:

  • option:参数选项,这个参数的选项如下:
    • -class :显示ClassLoad的相关信息;
    • -compiler :显示JIT编译的相关信息;
    • -gc:显示和gc相关的堆信息;
    • -gccapacity:显示各个代的容量以及使用情况;
    • -gcmetacapacity:显示metaspace的大小
    • -gcnew:显示新生代信息;
    • -gcnewcapacity:显示新生代大小和使用情况;
    • -gcold:显示老年代和永久代的信息;
    • -gcoldcapacity :显示老年代的大小;
    • -gcutil:显示垃圾收集信息;
    • -gccause:显示垃圾回收的相关信息,同时显示最后一次或当前正在发生的垃圾回收的原因;
  • -t:显示系统运行的时间
  • -h:在周期性数据数据的时候,指定输出多少行以后输出一次表头信息
  • vmid:指定进程的 pid
  • interval:输出统计信息的周期,单位为毫秒
  • count:输出统计信息的次数

查看class加载统计信息

以下示例输出进程PID为17397的Java程序类加载信息。

输出信息说明如下:

  • Loaded:加载类的数量
  • Bytes:加载类的大小
  • Unloaded:卸载类的数量
  • Bytes:卸载类的大小
  • Time:消耗时间

查看JIT编译统计信息

输出信息说明如下:

  • Compiled:编译执行次数。
  • Failed:编译失败次数
  • Invalid:编译不可用次数
  • Time:消耗时间
  • FailedType:最后一次失败类型
  • FailedMethod:最后一次失败的类名及方法

查看GC统计信息

输出信息说明如下:

  • S0C:第一个Survivor区的大小(KB)
  • S1C:第二个Survivor区的大小(KB)
  • S0U:第一个Survivor区的使用大小(KB)
  • S1U:第二个Survivor区的使用大小(KB)
  • EC:Eden区的大小(KB)
  • EU:Eden区的使用大小(KB)
  • OC:老年代大小 (KB)
  • OU:老年代使用大小 (KB)
  • MC:方法区大小(KB)
  • MU:方法区使用大小(KB)
  • CCSC:压缩类空间大小(KB)
  • CCSU:压缩类空间使用大小(KB)
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:Full GC次数
  • FGCT:Full GC消耗时间
  • GCT:垃圾回收消耗总时间

导出堆信息及分析

通过jstat命令可以对堆的内存进行统计分析,而jmap命令有更多功能,例如查看内存使用情况的汇总、对象实例的统计信息,导出堆的信息等等。

jmap中一个比较重要的功能就是将当前的堆信息快照保存到文件中,如下图:

将堆信息的快照导出后,可以使用各种工具对该文件进行分析,例如jhat命令工具或其他工具等等。

使用jhat命令可以用于分析导出的堆信息文件内容。

分析完成后,可以使用浏览器访问服务器的7000端口展示其分析结果,结果如下图所示:

该页面中显示了所有类信息,所有类的实例信息,点击链接进入后可以查看选中类的超类,类加载器及该类的实例信息等等信息。且在页面底部可以使用OQL进行查询。

可视化性能监控工具

VisualVM工具能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,查看分配的堆栈信息等等,是一个功能强大的可视化监控工具,且它的使用非常简单,可以完全替代其他JDK自带辅助工具的功能。

在JDK安装目录中的bin目录下可以直接启动VisualVM工具。

我们可以使用VisualVM工具连接本地Java程序或通过远程JMX连接Java程序。

VisualVM工具的主界面如下:

连接到本地进程后,可以查看进程中的JVM参数信息及版本信息:

以及查看CPU、堆、类和线程的统计信息:

以及查看线程信息:

并且点击线程Dump按钮后可以将线程信息导出:

并且可以使用抽样器对程序中的方法进行监控,有CPU和内存两个抽样器,通过CPU抽样器可以看出CPU中最消耗CPU资源的方法,通过内存抽样器可以动态的显示各个实例数据的大小: