Java虚拟机

Java虚拟机

学习自:陈树义的博客 (shuyi.tech)

基础

什么是虚拟机

Java跨平台:Java 语言并不直接将代码编译成与系统有关的机器码,而是编译成一种特定的语言规范,这种语言规范我们称之为字节码。Java 虚拟机会解析字节码文件的内容,并将其翻译为各操作系统能理解的机器码。实际上 Java 虚拟机运行的是字节码文件

一次编译 到处运行

​ Java 虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行

编译过程

​ 编译器分类:前端编译器、JIT 编译器(Just In Time Compiler,即时编译器)和AOT编译器(Ahead of Time Compiler,静态提前编译器)

前端编译器

​ JDK 的安装目录里有一个 javac 工具,就是它将 Java 代码翻译成字节码,这个工具我们叫做编译器。相对于后面要讲的其他编译器,其因为处于编译的前期,因此又被成为前端编译器

作用

​ 源代码 –> 字节码

过程

  1. 词法分析、语法分析:javac编译器”读懂”源代码,生成语法树
  2. 填充符号表:编译阶段无法知道代码中引用到的其他类的地址,先用一个符号填充,到类加载阶段再替换为真实地址
  3. 分析注解
  4. 生成字节码

JIT 编译器

​ 生成字节码后,可以选择使用java解释器运行,或者使用JIT编译器先将字节码编译成机器码再运行。前者启动速度快但运行速度慢,而后者启动速度慢但运行速度快。为了运行速度以及效率,我们通常采用两者相结合的方式进行 Java 代码的编译执行

作用

​ 字节码 –> 机器码

类型

​ HotSpot 虚拟机内置有两种JIT编译器:Client Compiler 和Server Compiler,简称为C1编译模式、C2编译模式。

​ 不同:C1编译模式编译速度快、优化保守;C2编译模式编译较慢、优化相对较好。

参数

​ 默认是混合起来用的(Mixed Mode),编译时可使用参数-client-server指定只使用其中一种

-Xint:只解释运行,不编译。

-Xcomp:优先编译

AOT编译器

​ AOT 编译器则能将源代码直接编译为本地机器码,速度快于前端编译器,慢于即时编译器。存在的目的在于Java 虚拟机加载已经预编译成二进制库,可以直接执行。不必等待及时编译器的预热,减少 Java 应用给人带来“第一次运行慢” 的不良体验。

作用

​ 源代码 –> 机器码

字节码文件结构

​ 《Java 虚拟机规范》规定了 Java 虚拟机结构、Class 类文件结构、字节码指令等内容。

字节码文件结构是一组以 8 位为最小基础的十六进制数据流。

字节码由两种基本数据类型组成:

  • 无符号数:u1、u2、u4、u8 六七分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数
  • 表:由多个无符号数或其他表组成的数据结构,以_info结尾。字节码文件本质就是一张表

魔数

​ Class 文件的第 1 - 4 个字节代表了该文件的魔数(Magic Number),其值固定是:0xCAFEBABE。作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件

文件版本

​ Class 文件的第 5 - 6 个字节代表了 Class 文件的次版本号(Minor Version),即编译该 Class 文件的 JDK 次版本号。

​ Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号(Major Version),即编译该 Class 文件的 JDK 主版本号。

​ JDK1.8的十六进制版本号为0000 0034

常量池

​ 占两个字节的constant_pool_count表示常量池中的常量个数

cp_info表示整个常量池,里面的结构为常量类型(u2) + 常量的数据结构(不定长) 的组合,常量的数据结构共有14 种 类型

访问标志

​ access_flags用于识别一些类或者接口层次的访问信息

​ 访问标志可能是由多个标志名称组成的,如0021是由0x0001 | 0x0020得来的

类索引、父类索引、接口索引

类索引:用于确定这个类的全限定名,大小为u2,指向常量池中的常量

父类索引:引用于确定这个类的父类的全限定名,大小为u2,指向常量池中的常量

接口索引:用来描述哪个类实现了哪些接口,u2 记录接口数量,后面interfaces记录所有的接口信息

字段表

​ 用于描述接口或者类中声明的变量,字段表的每个字段用一个名为 field_info 的表来表示

方法表

​ 用于描述类中的方法信息

属性表

​ 描述的属性信息

整体结构

​ 可以通过javap -verbose 文件名.class反编译将字节码文件全部分析出来

虚拟机内存结构

​ 字节码文件会加载进虚拟机内存中,进行一系列初始化后运行得出结果。

​ Java 虚拟机的内存结构可以分为公有和私有两部分。公有指的是所有线程都共享的部分,指的是 Java 堆方法区常量池。私有指的是每个线程的私有数据,包括:PC寄存器Java 虚拟机栈本地方法栈

公有部分

java堆:专门用于 Java 实例对象的内存分配,几乎所有实例对象都在会这里进行内存的分配,有些小对象可能在栈上分配内存

根据对象存活时间,堆可以分为新生代、老年代、永久代(方法区)

  1. 新生代

    • Eden (伊甸)区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
    • ServivorTo、ServivorFrom:每次MinorGC都会清理serviorFrom和Eden区的对象,然后将幸存对象复制到ServiorTo中(达到老年代指标的对象存到老年代),随后清空Eden和ServicorFrom中的对象,最后交换serviorTo和ServiorFrom。(复制算法

    三个区域的比例Eden:from :to = 8:1:1 ,通过-XX:MaxTenuringThreshold = 设置GC多少次晋升到老年代,默认15。

  2. 老年代

    ​ 当老年代的空间不足时会触发MajorGC,首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象(标记清除法

  3. 方法区

    ​ 指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

私有部分

  1. PC 寄存器:保存线程当前正在执行的方法,任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,其地址被存在 PC 寄存器中。
  2. Java 虚拟机栈:用来存储栈帧,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
  3. 本地方法栈:用于管理本地方法(非java代码的接口)的调用

类加载

指虚拟机将字节码读取进内存,从而进行解析、运行等整个过程

类加载过程分为:加载、验证、准备、解析、初始化、使用、卸载

加载:

​ 把代码数据加载到内存中

验证:

​ 校验Class字节码文件格式是否符合JVM规范,加载完字节码后在内存中创建对应的Class对象,之后检查是否有语法错误

准备

​ 给类分配内存,只会先给static修饰的变量分配内存并赋予对应类型的0值(0, “”, false,null),而修饰了final的话则直接赋予用户希望的值

解析:

​ 将引用在常量池中的符号引用替换成直接其在内存中的直接引用

初始化

​ JVM 会根据语句执行顺序对类对象进行初始化

​ 类初始化:给静态变量赋值,执行静态代码块,有父类会先类初始化父类,main函数所在类会被类初始化

​ 对象初始化:会先类初始化。给成员变量赋值,执行普通代码块,执行构造函数

​ 整体上就是惰性初始化,只有用到时才进行初始化

使用:

​ JVM 开始从入口方法开始执行用户的程序代码

卸载:

​ 执行完程序,销毁Class对象,最后JVM也退出内存

垃圾回收

如果一个对象不再被引用,则会被当做垃圾而回收。

如何知道对象是否被引用

  1. 计数方法(已淘汰):对象被引用时就+1,不再引用就-1。存在的问题:**循环引用(**A引用B,B引用A,但没有被其它对象引用,两者引用都不为0,无法被回收)

  2. 可达性算法GC Root Tracing:从 GC Root 出发,所有可达的对象都是存活的对象,不可达的为垃圾。

    GC Root 就是一组活跃引用的集合,通常GC Root包括:

    • 所有当前被加载的 Java 类
    • Java 类的引用类型静态变量
    • Java类的运行时常量池里的引用类型常量
    • VM的一些静态数据结构里指向GC堆里的对象的引用
    • 等等

如何进行垃圾回收

  1. 标记清除法:标记所有由 GC Root 触发的可达对象,清除所有未被标记的对象。缺点:会有产生内存碎片的问题。
  2. 复制算法:将原有的内存空间分为两块,每次只使用一块,垃圾回收时将存活的对象复制到另一块,清除当前块内所有对象。缺点:内存没有被完全使用。
  3. 标记压缩算法:上面两种方法的结合。标记存活对象,复制到内存的一边,清空剩余区域。

实际是每种方法结合着用。新生代存活对象较少,适合复制算法;老年代存活对象多,适合标记清除法或标记压缩算法,减少对象的移动。

术语

Minor GC/Young GC:从年轻代空间回收内存

Major GC/Old GC:从老年代空间回收内存

Full GC:清理年轻代、老年代和永久代,当发现年轻代空间太小或永久代空间不足时会触发

Stop-The-World:在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔

垃圾回收器

串行回收器(Serial)

指使用单线程进行垃圾回收的回收器

分类

  1. 新生代串行回收器:采用复制算法,垃圾回收时其他线程需要暂停(Stop-The-World
  2. 老年代串行回收器:采用标记压缩算法,也会Stop-The-World,可以与多种新生代回收器配合使用

参数

  • -XX:UseSerialGC:新生代、老年代都使用串行回收器。

  • -XX:UseParNewGC:新生代使用 ParNew 回收器(多线程),老年代使用串行回收器。

  • -XX:UseParallelGC:新生代使用 ParallelGC 回收器(多线程),老年代使用串行回收器。

并行回收器(Parallel)

指使用多线程进行垃圾回收,可以有效缩短垃圾回收所使用的时间

分类

  1. 新生代ParNew 回收器:只是简单地将串行回收器多线程化

    启动参数

    • -XX:+UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器。
    • -XX:UseConcMarkSweepGC:新生代使用 ParNew 回收器,老年代使用 CMS。
    • -XX:ParallelGCThreads:指定 ParNew 回收器的工作线程数量。
  2. 新生代 Parallel GC 回收器:相比于ParNew更加注重系统的吞吐量

    设置参数

    • -XX:+UseAdaptiveSizePolicy:自动调节新生代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数
    • -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间
    • -XX:GCTimeRatio:设置吞吐量大小

    启动参数

    • -XX:+UseParallelGC:新生代使用 Parallel 回收器,老年代使用串行回收器。
    • -XX:+UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
  3. 老年代 ParallelOldGC 回收器:注重吞吐量的老年代的回收器

    启动参数

    • -XX:UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

CMS 回收器

Concurrent Mark Sweep(并发标记扫描),CMS 回收器主要关注系统停顿时间

工作流程

初始标记:标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活的对象所引用的老年代对象,会Stop-The-World,时间短

并发标记:标记所有可达的对象,时间长,和用户线程并行。此阶段由于与用户线程并发执行,对象的状态可能会发生变化,JVM会将改变的区域标记为“脏”区(卡片标记)

并发预清理:标记老年代存活的对象,目的是为了让重新标记阶段的STW尽可能短,和用户线程并行。

-XX:-CMSPrecleaningEnabled:关闭并发预处理

最终标记/重新标记:重新扫描堆中的对象,之前的扫描都是并发的,可能跟不上修改速度。目标是完成老年代中所有存活对象的标记,会触发Stop The World

并发清除和并发重置:用户线程被重新激活,不需要STW,目标是删除不可达的对象,并为下次GC做准备

G1回收器

​ G1(Garbage First,垃圾优先)

​ JDK 1.7 中使用的全新垃圾回收器,JDK9之后的默认垃圾收集器,是一款面向服务端应用的垃圾收集器

​ 在 G1 回收器中,把堆内存分割为很多不相关的区域(region物理上不连续),把堆分为2048个区域。用不同的region可以来表示Eden、幸存者0区、幸存者1区、老年代等,G1回收器优先回收价值最大的Region

分区Region

  • 每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂。可通过-XX :G1HeapRegionSize设定大小
  • Humongous内存区域主要用于存储大对象,如果超过1.5个region, 就放到H

工作流程

  • 新生代 GC(Young GC):启动多线程执行年轻代回收,会STW
  • 老年代并发标记周期(Concurrent Marking):所有将要被回收的区域会被 G1 记录在一个称之为 Collection Set 的集合中
  • 混合收集(Mixed GC):首先针对 Collection Set 中的内存进行回收,在混合回收的时候,也会执行多次新生代 GC 和 混合 GC
  • 如果需要,可能进行 FullGC

JVM参数

堆栈空间配置

参数 含义
-Xms 初始堆大小
-Xmx 最大堆空间
-Xmn 设置新生代大小
-XX:SurvivorRatio 设置新生代eden空间和from/to空间的比例关系
-XX:PermSize 方法区(永久代)初始大小(JDK1.7)
-XX:MaxPermSize 方法(永久代)区最大大小(JDK1.7)
-XX:MetaspaceSize 元空间GC阈值(JDK1.8)
-XX:MaxMetaspaceSize 最大元空间大小(JDK1.8)
-Xss 栈大小
-XX:MaxDirectMemorySize 直接内存大小,默认为最大堆空间
-XX:+PrintGCDetails 查看内存区域的分配信息

实例

堆配置

java -Xms20m -Xmx30m:设置 JVM 的初始堆大小为 20M,最大堆空间为 30M

java -Xms20m -Xmn10M:设置 JVM 堆初始大小为20M,其中年轻代的大小为 10M

java -Xms20m -Xmn10M -XX:SurvivorRatio=2 -XX:+PrintGCDetails:设置 JVM 堆初始大小为20M;年轻代的大小为 10M,其中比例为eden / from = eden / to = 2,即5M:2.5M:2.5M;打印内存区域的分配信息

java -XX:PermSize10m -XX:MaxPermSize50m -XX:+PrintGCDetails:(jdk1.7)设置永久代初始大小为10M,最大50M

java -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=50m -XX:+PrintGCDetails:设置元空间发生 GC 的初始阈值为10M,元空间最大大小为50M,默认为机器内存大小

栈配置

java -Xss2m:设置栈空间最大为2M

java -XX:MaxDirectMemorySize=50m:设置直接内存大小为50M

查看参数

参数 含义
-XX:+PrintVMOptions 程序运行时,打印虚拟机接受到的命令行显式参数。
XX:+PrintCommandLineFlags 打印传递给虚拟机的显式和隐式参数。
-XX:+PrintFlagsFinal 打印所有的系统参数的值

实例

java -XX:+UseSerialGC -XX:+PrintVMOptions:打印显示参数(自己设定的参数)

java -XX:+UseSerialGC -XX:+PrintCommandLineFlags: 打印显式和隐式参数(默认参数)

java -XX:+UseSerialGC -XX:+PrintFlagsFinal 文件名 > jvm_flag_final.txt:打印所有系统参数并重定向到

jvm_flag_final.txt文件中

追踪类信息参数

参数 含义
-verbose:class 跟踪类的加载和卸载
-XX:+TraceClassLoading 跟踪类的加载
-XX:+TraceClassUnloading 跟踪类的卸载
-XX:+PrintClassHistogram 显示类信息柱状图

GC日志参数

参数 含义
-XX:PrintGC 打印GC日志
-XX:+PrintGCDetails 打印详细的GC日志。还会在退出前打印堆的详细信息。
-XX:+PrintHeapAtGC 每次GC前后打印堆信息。
-XX:+PrintGCTimeStamps 打印GC发生的时间。
-XX:+PrintGCApplicationConcurrentTime 打印应用程序的执行时间
-XX:+PrintGCApplicationStoppedTime 打印应用由于GC而产生的停顿时间
-XX:+PrintReferenceGC 跟踪软引用、弱引用、虚引用和Finallize队列。
-XLoggc 将GC日志以文件形式输出。

实例

java -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGC Demo:打印简单日志

  • -XX:+UseSerialGC 表示强制使用Serial+SerialOld收集器组合
  • -Xms20m 表示堆空间初始大小为 20 M。
  • -Xmx20m 表示堆空间最大大小为 20 M。
  • -Xmn10m 表示新生代大小为 10M。
  • -XX:SurvivorRatio=8 表示Eden:Survivor=8:1
  • -XX:PrintGC 打印GC日志

回收前->回收后(可用)

性能监控

jps指令

jps(Java Virtual Machine Process Status Tool):列出所有的 Java 进程

参数 含义
-q 指定jps只输出进程ID
-m 输出传递给Java进程的参数
-l 输出主函数的完整路径
-v 显示传递给Java虚拟机的参数

jstat指令

jstate(Java Virtual Machine Statistics Monitoring Tool):观察 Java 堆信息的详细情况

参数 含义
-class 监视类装载、卸载数量、总空间以及类装载所耗费的时间
-gc 监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息
-gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew 监视新生代GC状况
-gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
-gcold 监视老年代GC状况
-gcoldcapacity 监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间
-gcpermcapacity 输出永久代使用到的最大、最小空间
-compiler 输出JIT编译器编译过的方法、耗时信息
-printcompilation 输出已经被JIT编译的方法

实例

jstat -gcutil 2366 :查看java进程2366的堆使用情况

jinfo指令

查看正在运行的 Java 应用程序的扩展参数

jmap 命令

可以生成 Java 程序的 Dump 文件(当程序产生异常时,用来记录当时的程序状态信息),也可以查看堆内对象实例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列

jhat 命令

用于分析 Java 应用的对快照内存,可以用来分析jmap生成dump文件

面试题

1.概念

jvm分配内存给对象

  • 指针碰撞

​ 给对象分配内存都在一边,分配内存就把指针移动对象大小的距离

  • 空间列表

​ 类似空间分区表

内存

  • 内存溢出(OOM)

    申请的内存超过了可用内存

  • 内存泄漏

    内存没有被正确释放

2.对象创建过程

3.对象内存布局

  1. 对象头:

    1. Mark Word:哈希码、分代年龄、锁状态标志、线程持有的锁等
    2. 类型指针:指向当前对象是哪个类
  2. 实例数据:存放字段的信息

  3. 填充对齐

4.判断对象是否存活

  1. 引用计数

    对象中添加引用计数器,记录当前引用的次数。缺陷:循环引用

  2. 可达性分析

    从GCRoots出发,搜索可以到达的对象并标记,没有标记的对象就是可回收的

    主要的GCRoots:

    1.虚拟机栈中引用的对象

    2.类静态属性引用的对象

    3.常量引用的对象

    4.本地方法栈中JNI引用的对象

5.引用种类

  1. 强引用:一定不会被回收

    1
    Object obj = new Object();
  2. 软引用:内存不足时回收

    1
    2
    3
    Object obj = new Object();
    ReferenceQueue queue = new ReferenceQueue();
    SoftReference reference = new SoftReference(obj, queue);
  3. 弱引用:垃圾回收时就会被回收

    1
    2
    3
    Object obj = new Object();
    ReferenceQueue queue = new ReferenceQueue();
    WeakReference reference = new WeakReference(obj, queue);
  4. 虚引用:为了能在这个对象 被收集器回收时收到一个系统通知

    1
    2
    3
    Object obj = new Object();
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference reference = new PhantomReference(obj, queue);

6.垃圾回收算法

  1. 标记清除
  2. 标记复制:新生代
  3. 标记整理:老年代

7.垃圾回收触发时机

  1. Minor GC(Young GC):Eden区空间不足时
  2. Full GC:
    1. Minor GC 前判断从年轻代升入老年代的对象可能放不下时
    2. Minor GC 后发现放不下时
    3. 老年代空间不足时
    4. 晋升/提前晋升的对象在老年代中放不下时
    5. System.gc()

8.晋升老年代时机

  1. 新生代中对象到达一定年龄
  2. 大对象直接晋升
  3. 动态判断对象是否需要晋升
  4. 把MinorGC后仍然放不下的对象放入老年代

9.主要垃圾收集器种类

  1. Serial收集器(串行收集器):一个线程垃圾回收,所有用户线程需要停下,新生代标记复制,老年代标记整理
  2. ParNew:多个Serial收集器,新生代并发,老年代还是单线程
  3. Parallel Scavenge(并行收集器):注重吞吐量(运行用户代码的时长),老年代也并发
  4. CMS(Concurrent Mark Sweep)收集器(并发收集器):老年代收集器,以最短停顿时间为目标,并发收集、低停顿
  5. G1(Garbage First)收集器(并发收集器):弱化分代,而是分区收集(伊甸区、辛存区、老年区、大对象区),解决内存碎片问题

Java虚拟机
http://xwww12.github.io/2023/02/18/java/Java虚拟机/
作者
xw
发布于
2023年2月18日
许可协议