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 这门语言
- 为线上排查问题打下基础
- 虚拟机的始祖:Sun Classic
- 无疾而终:Sun Exact VM
- 武林盟主:Sun HotSpot VM
- 百家争鸣:BEA JRockit / IBM J9 VM
- 武林外传(那些无名虚拟机):Apache Harmony、Google Android Dalvik VM、Mircosoft JVM等等
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 编译器 > 解释执行。
- 魔数与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)):每个类的结构信息,例如:运行时常量池、字段和方法数据、构造方法等
- 常量池:常量池其实是存放在方法区中的
- Java堆:几乎所有的实例对象
- 私有:每个线程都有一个,包含私有数据
- PC寄存器(Program Counter 寄存器):保存线程当前正在执行的方法
- 如果是native方法,保存的值是undefined
- 如果不是native方法,保存的值是Java虚拟机正在执行的字节码指令地址。
- Java虚拟机栈
- 与线程同时创建,用来存储栈帧,即存储局部变量与一些过程结果的地方。
- 栈帧存储的数据包括:局部变量表、操作数栈。
- 本地方法栈
- Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,会使用到本地方法栈。
- PC寄存器(Program Counter 寄存器):保存线程当前正在执行的方法
- 公有:所有线程都共享一个,包含公有数据
当有一个对象需要分配时,先分配到年轻代的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
开头,主次版本号是否在当前虚拟机处理范围之内等。 - 代码逻辑校验 方法传入参数是否与方法定义时相同,返回参数类型是否与方法定义相同,引用了某个类,那这个类有没有声明等等。
- JVM规范校验 例如是否以
- 准备:为「类变量」分配内存并初始化
- 内存分配对象: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 也退出内存。
事实上,如果一个对象不可能再被引用,那么这个对象就是垃圾,应该被回收。
那么怎么找到垃圾并回收呢?
首先我们会想到,用计数的方式来判断。即当一个对象被引用时计数加一,被去除引用时减一。这样,当计数为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 中最基本的垃圾回收器之一
- 算法:复制算法
- 特点:最古老的一种、 JDK 中最基本的垃圾回收器之一
- 老年代串行回收器
- 特点:
- 算法:标记压缩算法
- 特点:
- 新生代串行回收器
- 特点:单线程,在并发能力较弱的计算机上,性能较好,会触发 Stop-The-World 现象,即其他线程都需要暂停,等待垃圾回收完成。
- 并行回收器
- 特点:对比串行回收器有所改进,使用多线程进行垃圾回收,对于并行能力强的机器,可以有效缩短垃圾回收所使用的时间,会触发 Stop-The-World 现象,即其他线程都需要暂停,等待垃圾回收完成,但因为是多线程,所以停顿时间要短于串行回收器
- 开启命令
-XX:+UseParNewGC
:新生代使用 ParNew 回收器,老年代使用串行回收器。-XX:UseConcMarkSweepGC
:新生代使用 ParNew 回收器,老年代使用 CMS。-XX:ParallelGCThreads
:指定 ParNew 回收器的工作线程数量。-XX:+UseParallelGC
:新生代使用 Parallel 回收器,老年代使用串行回收器。-XX:+UseParallelOldGC
:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
- 分类:
- 新生代 ParNew 回收器
- 特点:只是简单地将串行回收器多线程化,其余一样
- 算法:复制算法
- 新生代 Parallel GC 回收器
- 特点:与新生代 ParNew 回收器类似,不同点是:其注重系统的吞吐量
- 新生代 ParNew 回收器
- CMS 回收器
- 特点:关注系统停顿时间、多线程并行。
- 算法:标记清除算法
- G1 回收器
- 特点:是 JDK 1.7 中使用的全新垃圾回收器,依然使用了分代垃圾回收,但增加了分区算法,从而使得Eden 区、From 区、Survivor 区和老年代等各块内存不必连续。
- 目的:为了取代CMS回收器
- 开启命令:
- 打开 G1 收集器,我们可以使用参数:
-XX:+UseG1GC
- 设置目标最大停顿时间,可以使用参数:
-XX:MaxGCPauseMillis
- 设置 GC 工作线程数量,可以使用参数:
-XX:ParallelGCThreads
- 设置堆使用率触发并发标记周期的执行,可以使用参数:
-XX:InitiatingHeapOccupancyPercent
- 打开 G1 收集器,我们可以使用参数:
- 工作流程:
- 新生代 GC
- 并发标记周期
- 混合收集
- 如果需要,可能进行 FullGC
- 特点:是 JDK 1.7 中使用的全新垃圾回收器,依然使用了分代垃圾回收,但增加了分区算法,从而使得Eden 区、From 区、Survivor 区和老年代等各块内存不必连续。
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日志以文件形式输出。 |
虚拟机统计信息:jstat 命令
查看虚拟机参数:jinfo 命令
导出堆到文件:jmap 命令
堆分析工具:jhat 命令
查看线程堆栈:jstack 命令
远程主机信息收集:jstatd 命令
多功能命令行:jcmd 命令
性能统计工具:hprof 命令
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 事件代理
- Java|Java OpenCV图像处理之SIFT角点检测详解
- java中如何实现重建二叉树
- 数组常用方法一
- 【Hadoop踩雷】Mac下安装Hadoop3以及Java版本问题
- Java|Java基础——数组
- RxJava|RxJava 在Android项目中的使用(一)
- java之static、static|java之static、static final、final的区别与应用
- Java基础-高级特性-枚举实现状态机