Java虚拟机知识 - JVM入门

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

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

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

2, 再次认识Java

  1. Java技术体系

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

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

  2. Java发展历史

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

    1996JDK1.0

    1999HotSpot虚拟机诞生;

    2004JDK5

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

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

    2018JDK8 - LTS版本

  3. 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:[±] 表示启用或禁用属性

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

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

    ​ 如:-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 -l
16404 jvm.stu.Demo2_JVM_args
19236 org.jetbrains.jps.cmdline.Launcher
10936
12968 sun.tools.jps.Jps

3, 查看运行参数

D:\>jinfo -flags 16404
Attaching to process ID 16404, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.271-b09
Non-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:+UseParallelGC
Command 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 16404
Loaded  Bytes  Unloaded  Bytes     Time
   625  1258.8        0     0.0       0.13
2.查看编译统计
D:\>jstat -compiler 16404
Compiled 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     GCT
7680.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     GCT
7680.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
7680.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
7680.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
7680.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
7680.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:垃圾回收消耗总时间

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

能力工场小马哥

如果对您有帮助, 请打赏支持~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值