JVM|Java虚拟机|JVM【适合初学者入门】


Java虚拟机|JVM【适合初学者入门】

  • 0. 前言
  • 1. 学习JVM的目的
  • 2. 主要的虚拟机
  • 3. 什么是虚拟机
  • 4. 源代码到机器码的过程
  • 5. 字节码文件的结构
  • 6. Java虚拟机内存结构
  • 7. JVM类的加载机制
  • 8. JVM垃圾回收机制
  • 9. JVM垃圾回收期
  • 10. 垃圾回收的几种类型
  • 11. JVM参数之堆栈空间配置
  • 12. JVM参数之查看JVM参数
  • 13. JVM参数之追踪类信息
  • 14. JVM参数之GC日志配置
  • 15. JDK性能监控命令

0. 前言 为什么要有标题0?不要问,问就是程序员数数都是从0开始的。
前段时间正在看《深入理解Java虚拟机》这本书,看完之后颇有感受,这本书写的非常好,我本人也很喜欢周志明老师的风格,真心佩服周老师对虚拟机的理解这么透彻,但从书的标题也可以看得出“深入”二字,如同书名,该书内容确实对新手来说有些晦涩。所以在这里我总结了一篇Java虚拟机的博文,大家可以把它当做阅读这本书的前奏,让自己在心里有一些Java虚拟机的概念,并有一定的理解。如果喜欢的话,可以点个赞和收藏哦!
本文章参考了博主陈树义的JVM专栏,并在已经过博主本人同意的情况下发布这篇文章。
1. 学习JVM的目的
  • 深入地理解 Java 这门语言
  • 为线上排查问题打下基础
2. 主要的虚拟机
  • 虚拟机的始祖:Sun Classic
  • 无疾而终:Sun Exact VM
  • 武林盟主:Sun HotSpot VM
  • 百家争鸣:BEA JRockit / IBM J9 VM
  • 武林外传(那些无名虚拟机):Apache Harmony、Google Android Dalvik VM、Mircosoft JVM等等
3. 什么是虚拟机 我们知道不同的操作系统底层的实现是不一样的。因此在一个操作系统上编译的机器码不能在另一个操作系统上被识别。所以和其他语言不同,Java语言不直接编译成与系统有关的机器码,而是编译字节码,再通过不同的系统上提前安装好的Java虚拟机分别解释成机器码。
4. 源代码到机器码的过程 编译器:
  • 前端编译器:源代码到字节码,代表:Sun的javac
    • 编译器将Java源代码编译成为字节码文件(A.java–>A.class),字节码文件是由16进制数字组成
  • 【JVM|Java虚拟机|JVM【适合初学者入门】】JIT 编译器:从字节码到机器码,代表:HotSpot VM的C1、C2
    • 分类
      • 使用 Java 解释器解释执行字节码,启动速度快但运行速度慢
      • 使用 JIT 编译器(即时编译器)将字节码转化为本地机器代码,启动速度慢但运行速度快
        • Client Compiler(C1 编译器)
        • Server Compiler(C2 编译器)
    • 运行模式
      • 混合模式
        • C1 和 C2 两种模式混合起来使用(默认方式)
        • 如果想单独使用 C1 模式或 C2 模式,使用 -client-server 打开
      • 解释模式
        • 所有代码都解释执行
        • 使用 -Xint 参数打开
      • 编译模式
        • 优先采用编译,但是无法编译时也会解释执行
        • 使用 -Xcomp 参数打开
  • AOT 编译器:源代码到机器码,代表:GNU Compiler for the Java(GCJ)

对比:
  • 编译速度上,解释执行 > AOT 编译器 > JIT 编译器。
  • 编译质量上,JIT 编译器 > AOT 编译器 > 解释执行。
5. 字节码文件的结构 字节码文件由以下七个部分组成
  • 魔数与Class文件版本
  • 常量池
  • 访问标志
  • 类索引、父类索引、接口索引
  • 字段表集合
  • 方法表集合
  • 属性表集合
字节码文件中的十六进制数字以若干位为单位,分别代表着以上的信息。
具体内容可查看这篇文章:https://www.cnblogs.com/chanshuyi/p/jvm_serial_05_jvm_bytecode_analysis.html
6. Java虚拟机内存结构
  • 虚拟机内存结构(官方也叫运行时数据区)
    • 公有:所有线程都共享一个,包含公有数据
      • Java堆:几乎所有的实例对象
        • 年轻代
          • Eden区
          • From Survivor 0区
          • From Survivor 1区
        • 老年代
      • 方法区(1.7版本称为永久代(Permanent Space),1.8版本称为元空间(MetaSpace)):每个类的结构信息,例如:运行时常量池、字段和方法数据、构造方法等
      • 常量池:常量池其实是存放在方法区中的
    • 私有:每个线程都有一个,包含私有数据
      • PC寄存器(Program Counter 寄存器):保存线程当前正在执行的方法
        • 如果是native方法,保存的值是undefined
        • 如果不是native方法,保存的值是Java虚拟机正在执行的字节码指令地址。
      • Java虚拟机栈
        • 与线程同时创建,用来存储栈帧,即存储局部变量与一些过程结果的地方。
        • 栈帧存储的数据包括:局部变量表、操作数栈。
      • 本地方法栈
        • Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,会使用到本地方法栈。

当有一个对象需要分配时,先分配到年轻代的Eden区,等到Eden 区域内存不够时,Java 虚拟机会启动垃圾回收(GC)。此时 Eden 区中没有被引用的对象的内存就会被回收,而一些存活时间较长的对象则会进入到老年代。在JVM中-XX:MaxTenuringThreshold参数用来设置晋升到老年代所需要经历的GC次数。即一个对象分配进来后,如果经历这么多次的GC,它都还没有被作为垃圾回收,也就是一直有被引用,那么这个对象到指定的GC次数之后就会晋升到老年代。
PC寄存器保存的是某个线程当前正在执行的方法,由于一个线程在某一时刻执行的方法只有唯一一个,而这个方法被叫做该线程的当前方法。
在JVM中除了这几个内存外,其实还有直接内存、栈帧等,但用的比较少。

问:为什么给对象分配空间也需要分为年轻代和老年代呢?意义是什么?
根据经验,有些对象的存活时间很长,而有些对象的存活时间很短,如果我们把它们混在一起,那么必然会导致有部分对象一直被扫描,但又一直不是垃圾,这就很浪费时间。那采取的措施就是扫描若干次之后,某个对象仍然不是垃圾,那就把它移动到老年区。
问:Eden:from:to分区的比例是多少?
默认的虚拟机配置是 Eden:from :to = 8:1:1。这是IBM公司统计的结果,他们发现80%的对象存活的时间都很短,于是将Eden区设置为80%。
问:什么是native方法?
看以下文章:https://blog.csdn.net/qq_23501635/article/details/78902721
7. JVM类的加载机制 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。
  • 加载:把字节码数据加载到内存中。
  • 验证:加载完Class文件并在方法区创建对应的Class对象后,JVM会对字节码流进行校验
    • JVM规范校验 例如是否以cafe babe开头,主次版本号是否在当前虚拟机处理范围之内等。
    • 代码逻辑校验 方法传入参数是否与方法定义时相同,返回参数类型是否与方法定义相同,引用了某个类,那这个类有没有声明等等。
  • 准备:为「类变量」分配内存并初始化
    • 内存分配对象:Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
    • 初始化的类型:在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。
  • 解析:JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。
    • 作用:将其在常量池中的符号引用替换成直接其在内存中的直接引用。
  • 初始化(最常见的就是我们new一个对象和反射这两种情况。这个时候会为「类成员变量」分配内存。类成员变量不包括方法
    • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
    • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
  • 使用:当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码
  • 卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
8. JVM垃圾回收机制 我们都在说回收垃圾,那么到底什么是垃圾?
事实上,如果一个对象不可能再被引用,那么这个对象就是垃圾,应该被回收。

那么怎么找到垃圾并回收呢?
首先我们会想到,用计数的方式来判断。即当一个对象被引用时计数加一,被去除引用时减一。这样,当计数为0时,我们就认为是垃圾。
这种方法有一个弊端,就是当A 引用了 B,B 引用了 C,C 引用了 A,它们各自的引用计数都为 1。但是它们三个对象却从未被其他对象引用,只有它们自身互相引用。从垃圾的判断思想来看,它们三个确实是不被其他对象引用的,但是此时它们的引用计数却不为零。这就是引用计数法存在的循环引用问题。

所以现在Java虚拟机使用的是GC Root Tracing 算法。其大概的过程是这样:从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾,最后形成一个被引用对象集合。

那么拥有了这种算法之后,如何回收垃圾呢?
这个时候就要用到垃圾回收算法了,主要有三种:
  • 标记清除算法(缺点:产生空间碎片)
    • 标记阶段
      • 标记所有被引用的对象,此时所有未被引用的对象就是垃圾对象
    • 清除阶段
      • 清除所有未被标记的对象
  • 复制算法(缺点:内存空间折半)
    • 将内存分为两块,每次只用一块内存,垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。然后清除正在使用的内存中的所有对象,之后交换内存块的角色(注意:是交换角色,不是交换两块内存里面的对象!)
  • 标记压缩算法:标记清除算法的优化版
    • 标记结算
      • 从 GC Root 引用集合触发去标记所有对象
    • 压缩阶段
      • 将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。

三者比较:
标记清除算法:会产生内存碎片,但是不需要移动太多对象,比较适合在存活对象比较多的情况。
复制算法:虽然需要将内存空间折半,并且需要移动存活对象,但是其清理后不会有空间碎片,比较适合存活对象比较少的情况。
标记压缩算法:标记清除算法的优化版,减少了空间碎片。

综上所述:每种算法都有自己的优缺点,最好的方法当然是分情况灵活使用它们。而其实JVM虚拟机正是如此。因此,出现了分代算法。
所谓分代算法,就是根据 JVM 内存的不同内存区域,采用不同的垃圾回收算法。
(举个例子:老年代中对象的存活率几乎可以是100%,这个时候如果使用复制算法,工作量巨大!而对于新生代来说,很多对象都是没有被引用的垃圾,所以适合使用复制算法。因此,像前面说到的,新生代是有分区的,即Eden 区域、from 区域、to 区域,并且比例是8:1:1,那么为什么要这么分呢?实际上前面已经讲过,因为很多对象都是垃圾,所以复制之后的对象其实很少,所以我们先在Eden 区域、from 区域使用GC算法,并将存活对象复制到to区域,然后删除Eden 区域、from 区域的所有对象,最后,交换from和to区域的角色并等待下一次GC)

实际上,除了分代的概念,还有分区思想。即将整个堆空间划分成连续的不同小区间,每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间,可以较好地控制 GC 时间。
9. JVM垃圾回收期 Java 虚拟机的垃圾回收器可以分为四大类别:
  • 串行回收器
    • 特点:单线程,在并发能力较弱的计算机上,性能较好,会触发 Stop-The-World 现象,即其他线程都需要暂停,等待垃圾回收完成。
    • 开启命令:
      • -XX:UseSerialGC:新生代、老年代都使用串行回收器
      • -XX:UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器
      • -XX:UseParallelGC:新生代使用 ParallelGC 回收器,老年代使用串行回收器
    • 分类:
      • 新生代串行回收器
        • 特点:最古老的一种、 JDK 中最基本的垃圾回收器之一
        • 算法:复制算法
      • 老年代串行回收器
        • 特点:
        • 算法:标记压缩算法
  • 并行回收器
    • 特点:对比串行回收器有所改进,使用多线程进行垃圾回收,对于并行能力强的机器,可以有效缩短垃圾回收所使用的时间,会触发 Stop-The-World 现象,即其他线程都需要暂停,等待垃圾回收完成,但因为是多线程,所以停顿时间要短于串行回收器
    • 开启命令
      • -XX:+UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器。
      • -XX:UseConcMarkSweepGC:新生代使用 ParNew 回收器,老年代使用 CMS。
      • -XX:ParallelGCThreads:指定 ParNew 回收器的工作线程数量。
      • -XX:+UseParallelGC:新生代使用 Parallel 回收器,老年代使用串行回收器。
      • -XX:+UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
    • 分类:
      • 新生代 ParNew 回收器
        • 特点:只是简单地将串行回收器多线程化,其余一样
        • 算法:复制算法
      • 新生代 Parallel GC 回收器
        • 特点:与新生代 ParNew 回收器类似,不同点是:其注重系统的吞吐量
  • CMS 回收器
    • 特点:关注系统停顿时间、多线程并行。
    • 算法:标记清除算法
  • G1 回收器
    • 特点:是 JDK 1.7 中使用的全新垃圾回收器,依然使用了分代垃圾回收,但增加了分区算法,从而使得Eden 区、From 区、Survivor 区和老年代等各块内存不必连续。
    • 目的:为了取代CMS回收器
    • 开启命令:
      • 打开 G1 收集器,我们可以使用参数:-XX:+UseG1GC
      • 设置目标最大停顿时间,可以使用参数:-XX:MaxGCPauseMillis
      • 设置 GC 工作线程数量,可以使用参数:-XX:ParallelGCThreads
      • 设置堆使用率触发并发标记周期的执行,可以使用参数:-XX:InitiatingHeapOccupancyPercent
    • 工作流程:
      • 新生代 GC
      • 并发标记周期
      • 混合收集
      • 如果需要,可能进行 FullGC
10. 垃圾回收的几种类型 Minor GC:从年轻代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。
Major GC:从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。
Young GC:如上
Old GC:如上
Full GC:Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)
Stop-The-World:是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。
11. JVM参数之堆栈空间配置
  • 堆空间:
    • 年轻代:java -Xms20m -Xmn10M GCDemo
    • Eden区
  • 永久代(JDK1.7叫法,原方法区)
  • 元空间(JDK1.8叫法,原方法区)
  • 栈空间
  • 直接内存

堆空间:java -Xms20m -Xmx30m GCDemo 设置 JVM 的初始堆大小为 20M,最大堆空间为 30M
年轻代:java -Xms20m -Xmn10M GCDemo 设置 JVM 堆初始大小为20M,其中年轻代的大小为 10M,剩下的自然为老年代的,有10M。
Eden区: java -Xms20m -Xmn10M -XX:SurvivorRatio=2 -XX:+PrintGCDetails GCDemo 我们在前面说过,年轻代分为eden 空间、from 空间、to 空间。这里我们设置堆初始大小为 20M,年轻代大小为 10M,年轻代的 SurvivorRatio 比例为 2,意思是eden/from=eden/to=2。那么最终分配的结果将会是:年轻代 10M,其中 Eden 区 5M、From 区 2.5M、To 区 2.5 M,老年代 10M。
永久代:java -XX:PermSize10m -XX:MaxPermSize50m -XX:+PrintGCDetails GCDemo 设置永久代初始大小为 10M,最大大小为 50M。
元空间:java -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=50m -XX:+PrintGCDetails GCDemo设置的是元空间发生 GC 的初始阈值为10M,设置元空间的最大大小为50M。
栈空间:java -Xss2m GCDemo 设置最大栈空间为 2M
直接内存:java -XX:MaxDirectMemorySize=50m GCDemo 设置直接内存最大值为 50M,默认为最大堆空间
12. JVM参数之查看JVM参数 程序运行时,打印虚拟机接收到的命令行显式参数 -XX:+PrintVMOptions
输入命令: java -XX:+UseSerialGC -XX:+PrintVMOptions Demo

运行结果: VM option '+UseSerialGC' VM option '+PrintVMOptions' Hello, I'm chenshuyi

程序运行时,打印传递给虚拟机的显式和隐式参数 -XX:+PrintCommandLineFlags
输入命令: java -XX:+UseSerialGC -XX:+PrintCommandLineFlags Demo

运行结果: -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC Hello, I'm chenshuyi

程序运行时,打印所有系统参数-XX:+PrintFlagsFinal
输入命令: java-XX:+UseSerialGC -XX:+PrintFlagsFinal Demo > jvm_flag_final.txt

运行结果放在了jvm_flag_final.txt 文件,打开后部分内容如下: ... uintx InitialHeapSize := 134217728 { product} ... uintx MaxMetaspaceSize = 18446744073709547520 { product} ... uintx MetaspaceSize = 21807104 { pd product}

13. JVM参数之追踪类信息 跟踪类的加载和卸载-verbose:class
输入以下命令:
java -verbose:class Demo > class_load_info.txt

打开 class_load_info.txt 文件
...省略... [Loaded java.util.ArrayList from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar] ...省略... [Loaded java.util.HashMap from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar] ...省略... [Loaded com.chenshuyi.ClassLoadDemo from file:/Users/yurongchan/Yosemite/Code/practice/target/classes/] ...省略...

跟踪类的加载-XX:+TraceClassLoading
跟踪类的卸载-XX:+TraceClassUnloading
14. JVM参数之GC日志配置 Java 虚拟机的GC(Garbage Collection)日志系统。
参数 含义
-XX:PrintGC 打印GC日志
-XX:+PrintGCDetails 打印详细的GC日志。还会在退出前打印堆的详细信息。
-XX:+PrintHeapAtGC 每次GC前后打印堆信息。
-XX:+PrintGCTimeStamps 打印GC发生的时间。
-XX:+PrintGCApplicationConcurrentTime 打印应用程序的执行时间
-XX:+PrintGCApplicationStoppedTime 打印应用由于GC而产生的停顿时间
-XX:+PrintReferenceGC 跟踪软引用、弱引用、虚引用和Finallize队列。
-XLoggc 将GC日志以文件形式输出。
15. JDK性能监控命令 查看虚拟机进程:jps 命令
虚拟机统计信息:jstat 命令
查看虚拟机参数:jinfo 命令
导出堆到文件:jmap 命令
堆分析工具:jhat 命令
查看线程堆栈:jstack 命令
远程主机信息收集:jstatd 命令
多功能命令行:jcmd 命令
性能统计工具:hprof 命令

    推荐阅读