写点什么

Java 虚拟机知识 - JVM 入门

用户头像
小马哥
关注
发布于: 2021 年 01 月 22 日
Java虚拟机知识 - JVM入门

1, 架构师面对 JVM 调优, 能做什么?

JVM 调优目的: 让系统运行更快, 更稳定.

背景问题: 1, 高并发; 2, 高吞吐;

2, 再次认识 Java


  1. Java 技术体系

JDK: Java 程序设计语言; Java 虚拟机; Java 类库. 是支持 Java 程序开发的最小环境.

JRE: JavaSE API(类库)和 Java 虚拟机统称为 JRE, 是支持 Java 程序运行的标准环境.


  1. Java 发展历史

1995 正是发布 Java, Write Once, Run Anywhere 的特点;

1996JDK1.0

1999HotSpot 虚拟机诞生;

2004JDK5

2014JDK8: 支持 Lambda, 移除了 HotSpot 永久代

2017 起, JDK 每年 3 月和 9 月发布一个大版本, 每六个大版本画出一个长期支持版 LTS, 三年的支持和更新

2018JDK8 - LTS 版本


  1. JVM 虚拟机种类


HotSpot: OracleJDK 和 OpenJDK 中默认的 Java 虚拟机, 也是使用最广泛的 Java 虚拟机, 热点代码探测技术. Oracle 收购 Sun 之后, 将 JRockit 的优秀特性融合到 HotSpot 之中.


JRockit: 来自 BEA, 定位于专门为服务器硬件和服务端应用场景, 做了专门优化, 不关注程序启动速度, 因此 JRockit 内部不包含解释器实现, 全部代码都靠即时编译器编译后执行.号称世界上速度最快的 Java 虚拟机.


IBM J9: IBM 开源, 捐献给了 Eclipse 基金会管理.


3,JVM 虚拟机内存管理

为什么要了解 JVM 的内存管理?

对于 Java 程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个 new 操作去写配对的 delete/free 代码,不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切都很美好。

不过,也正是因为 Java 程序员把控制内存的权力交给了 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。


3.1 整体架构

JVM 主要组成: 类加载系统, 运行时数据区, 执行引擎, 本地方法库与本地库接口

3.1.1 类加载系统


​ 作用: 加载 class 文件, 形成可以被虚拟机直接使用的 Java 类型


​ 1, 将 Class 文件加载到内存;


​ 2, 数据校验, 转换解析和初始化;

3.1.2 运行时数据区


​ 作用: JVM 管理的内存, 划分为若干不同的数据区域, 用于存储 Java 程序的数据.

3.1.3 执行引擎

​ 作用:

​ 1, 用于执行 JVM 字节码指令(解释执行和编译执行)

​ 2, 垃圾回收器自动管理运行数据区的内存, 将无用的内存占用进行清除, 释放内存资源.


3.1.4 本地方法库与本地库接口

​ 作用: JDK 底层用于调用系统本地方法的方法或者接口


3.2 运行时数据区

运行时数据区是 jvm 中最为重要的部分,也是我们在调优时需要重点关注的区域

3.2.1 程序计数器

作用: 当前线程执行字节码指令的指示器. 指向下一条需要执行的字节码指令. 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

​ 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

​ 由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存.


代码展示:


public class Demo1_ProgramCounter {    public void show(){        System.out.println("Method is running");        for (int i = 0; i < 3; i++) {            System.out.println("Hello JVM "+i);        }        System.out.println("Love JVM");    }
public static void main(String[] args) { Demo1_ProgramCounter counter = new Demo1_ProgramCounter(); counter.show(); }}
复制代码


反编译该 Java 代码的字节码文件:


命令行中, 打开字节码 class 文件所在位置, 执行: javap -c Demo1_ProgramCounter.class > ProgramCounter.txt


Compiled from "Demo1_ProgramCounter.java"public class jvm.stu.Demo1_ProgramCounter {  public jvm.stu.Demo1_ProgramCounter();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return
public void show(); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Method is running 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: iconst_0 9: istore_1 10: iload_1 11: iconst_3 12: if_icmpge 46 15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 18: new #5 // class java/lang/StringBuilder 21: dup 22: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 25: ldc #7 // String Hello JVM 27: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 30: iload_1 31: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 34: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 37: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: iinc 1, 1 43: goto 10 46: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 49: ldc #11 // String Love JVM 51: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 54: return
public static void main(java.lang.String[]); Code: 0: new #12 // class jvm/stu/Demo1_ProgramCounter 3: dup 4: invokespecial #13 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #14 // Method show:()V 12: return}
复制代码


可以看到将 class 文件中字节码进行反汇编,得到上面的代码,其中 code 所对应的编号就可以理解为计数器中所记录的执行编号

3.2.2 Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。Java 虚拟机栈描述的是**Java方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧**,用于存储局部变量表操作数栈动态连接方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 局部变量表

  • 操作数栈

  • 动态连接

  • 方法出口

3.2.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

3.2.4 Java 虚拟机堆

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的**唯一目的就是存放对象实例**,Java 世界里“几乎”所有的对象实例都在这里分配内存。需要注意的是,《Java 虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以使用最多的 HotSpot 虚拟机为例进行讲解。

Java 堆是垃圾收集器管理的内存区域,因此它也被称作“GC 堆”,这就是我们做 JVM 调优的重点区域部分。


Young 年轻区(代)

Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区,其中,Survivor 区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在 Eden 区间变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾收集后,任然存活于 Survivor 的对象将被移动到 Tenured 区间。


Tenured 年老区

Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区,一般如果系统中用了 application 级别的缓存,缓存中的对象往往会被转移到这一区间。


Perm 永久区(注意 JDK8 中永久代使用元数据空间进行了替换, 也就是永久区使用了机器内存而不是 jvm 内存)

Perm 代主要保存 class,method,filed 对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的 class 没有被卸载掉,这样就造成了大量的 class 对象保存在了 perm 中,这种情况下,一般重新启动应用服务器可以解决问题。


Virtual 区

最大内存和初始内存的差值,就是 Virtual 区。

3.2.5 方法区


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

  • 《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,它却有一个别名叫作“非堆”(Non-Heap),目的是与 Java 堆区分开来。

  • JDK8 之前将 HotSpot 虚拟机把收集器的分代设计扩展至方法区,所以可以将永久代看做是方法区,JDK8 之后废弃永久代,用元空间来代替。


4, 虚拟机性能相关工具

4.1 JVM 的运行参数


  • 标准参数

jvm 的标准参数,一般都是很稳定的,在未来的 JVM 版本中不会改变,可以使用 java -help 检索出所有的标准参数。


  • -X 参数(非标准参数)

jvm 的-X 参数是非标准参数,在不同版本的 jvm 中,参数可能会有所不同,可以通过 java -X 查看非标准参数。


  • -XX 参数(使用率较高)

4.2 参数: -X

​ jvm 的-X 参数是非标准参数,在不同版本的 jvm 中,参数可能会有所不同,可以通过 java -X 查看非标准参数。


D:\>java -X    -Xmixed           混合模式执行(默认)    -Xint             仅解释模式执行    -Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>                      设置引导类和资源的搜索路径    -Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>                      附加在引导类路径末尾    -Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>                      置于引导类路径之前    -Xdiag            显示附加诊断消息    -Xnoclassgc        禁用类垃圾收集    -Xincgc           启用增量垃圾收集    -Xloggc:<file>    将 GC 状态记录在文件中(带时间戳)    -Xbatch           禁用后台编译    -Xms<size>        设置初始 Java 堆大小    -Xmx<size>        设置最大 Java 堆大小    -Xss<size>        设置 Java 线程堆栈大小    -Xprof            输出 cpu 分析数据    -Xfuture          启用最严格的检查,预计会成为将来的默认值    -Xrs              减少 Java/VM 对操作系统信号的使用(请参阅文档)    -Xcheck:jni       对 JNI 函数执行其他检查    -Xshare:off       不尝试使用共享类数据    -Xshare:auto      在可能的情况下使用共享类数据(默认)    -Xshare:on        要求使用共享类数据,否则将失败。    -XshowSettings    显示所有设置并继续    -XshowSettings:system                      (仅限 Linux)显示系统或容器                      配置并继续    -XshowSettings:all                      显示所有设置并继续    -XshowSettings:vm 显示所有与 vm 相关的设置并继续    -XshowSettings:properties                      显示所有属性设置并继续    -XshowSettings:locale                      显示所有与区域设置相关的设置并继续
-X 选项是非标准选项。如有更改,恕不另行通知。
复制代码

4.3 参数: -XX

​ -XX 参数也是非标准参数,主要用于 jvm 的调优和 debug 操作。

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


使用方法:

  • boolean 类型使用: 格式:-XX:[+-]<name> 表示启用或禁用<name>属性

​ 如:-XX:+DisableExplicitGC 表示禁用手动调用 gc 操作,也就是说调用 System.gc()无效

  • 非 boolean 类型使用: 格式:-XX:<name>=<value> 表示<name>属性的值为<value>

​ 如:-XX:NewRatio=4 表示新生代和老年代的比值为 1:4

4.4 参数: -Xms 与-Xmx


**-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。**

**-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。**

**-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。**

**适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快**

4.5 查看正在运行的 jvm 参数

1, 启动一个 java 程序;

​ 在 IDE 或者通过命令行启动一个带时延的程序,

public class Demo2_JVM_args {    public static void main(String[] args) {        System.out.println("启动一个java程序.");        try {            System.out.println("下面时延500秒, 用于在命令行中查看java进程.");            Thread.sleep(500000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("程序运行结束, 命令行中看不到进程.");    }}
复制代码

启动该程序...

2, 查找该应用进程编号;

​ 作用: 找到进程编号, 供第 3 步查看参数使用

​ 操作: 命令行中执行jps -l命令

D:\>jps -l16404 jvm.stu.Demo2_JVM_args19236 org.jetbrains.jps.cmdline.Launcher1093612968 sun.tools.jps.Jps
复制代码


3, 查看运行参数

D:\>jinfo -flags 16404Attaching to process ID 16404, please wait...Debugger attached successfully.Server compiler detected.JVM version is 25.271-b09Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=199229440 -XX:MaxHeapSize=3183476736 -XX:MaxNewSize=1061158912 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=66060288 -XX:OldSize=133169152 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGCCommand line:  -javaagent:D:\engineering\install\JetBrains\IntelliJ IDEA 2020.3\lib\idea_rt.jar=8095:D:\engineering\install\JetBrains\IntelliJ IDEA 2020.3\bin -Dfile.encoding=UTF-8
复制代码


D:\>jinfo -flag MaxHeapSize 16404-XX:MaxHeapSize=3183476736
复制代码

4.6 jstat


jstat 命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:

jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]


继续启动上面程序

1.查看 class 加载统计


D:\>jstat -class 16404Loaded  Bytes  Unloaded  Bytes     Time   625  1258.8        0     0.0       0.13
复制代码

2.查看编译统计


D:\>jstat -compiler 16404Compiled Failed Invalid   Time   FailedType FailedMethod      70      0       0     0.03          0
复制代码

3.垃圾回收统计


D:\>jstat -gc 16404 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT7680.0 7680.0  0.0    0.0   49152.0   4915.4   130048.0     0.0     4480.0 776.5  384.0   76.6       0    0.000   0      0.000    0.000
复制代码


设置每 500 毫秒打印 1 次, 一共打印 5 次


D:\>jstat -gc 16404 500 5 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT7680.0 7680.0  0.0    0.0   49152.0   4915.4   130048.0     0.0     4480.0 776.5  384.0   76.6       0    0.000   0      0.000    0.0007680.0 7680.0  0.0    0.0   49152.0   4915.4   130048.0     0.0     4480.0 776.5  384.0   76.6       0    0.000   0      0.000    0.0007680.0 7680.0  0.0    0.0   49152.0   4915.4   130048.0     0.0     4480.0 776.5  384.0   76.6       0    0.000   0      0.000    0.0007680.0 7680.0  0.0    0.0   49152.0   4915.4   130048.0     0.0     4480.0 776.5  384.0   76.6       0    0.000   0      0.000    0.0007680.0 7680.0  0.0    0.0   49152.0   4915.4   130048.0     0.0     4480.0 776.5  384.0   76.6       0    0.000   0      0.000    0.000
复制代码


字段解释如下


S0C:第一个 Survivor 区的大小(KB)

S1C:第二个 Survivor 区的大小(KB)

S0U:第一个 Survivor 区的使用大小(KB)

S1U:第二个 Survivor 区的使用大小(KB)

EC:Eden 区的大小(KB)

EU:Eden 区的使用大小(KB)

OC:Old 区大小(KB)

OU:Old 使用大小(KB)

MC:方法区大小(KB)

MU:方法区使用大小(KB)

CCSC:压缩类空间大小(KB)

CCSU:压缩类空间使用大小(KB)

YGC:年轻代垃圾回收次数

YGCT:年轻代垃圾回收消耗时间

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间


发布于: 2021 年 01 月 22 日阅读数: 900
用户头像

小马哥

关注

自强不息,厚德载物 2018.12.22 加入

像一棵竹子那样, 不断的扎根积累, 活出节节高的人生!

评论

发布
暂无评论
Java虚拟机知识 - JVM入门