Java虚拟机
Java虚拟机
基础
什么是虚拟机
Java跨平台:Java 语言并不直接将代码编译成与系统有关的机器码,而是编译成一种特定的语言规范,这种语言规范我们称之为字节码。Java 虚拟机会解析字节码文件的内容,并将其翻译为各操作系统能理解的机器码。实际上 Java 虚拟机运行的是字节码文件
Java 虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行
编译过程
编译器分类:前端编译器、JIT 编译器(Just In Time Compiler,即时编译器)和AOT编译器(Ahead of Time Compiler,静态提前编译器)
前端编译器
JDK 的安装目录里有一个 javac 工具,就是它将 Java 代码翻译成字节码,这个工具我们叫做编译器。相对于后面要讲的其他编译器,其因为处于编译的前期,因此又被成为前端编译器
作用
源代码 –> 字节码
过程
- 词法分析、语法分析:javac编译器”读懂”源代码,生成语法树
- 填充符号表:编译阶段无法知道代码中引用到的其他类的地址,先用一个符号填充,到类加载阶段再替换为真实地址
- 分析注解
- 生成字节码
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 实例对象的内存分配,几乎所有实例对象都在会这里进行内存的分配,有些小对象可能在栈上分配内存
根据对象存活时间,堆可以分为新生代、老年代、永久代(方法区)
新生代:
- Eden (伊甸)区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
- ServivorTo、ServivorFrom:每次MinorGC都会清理serviorFrom和Eden区的对象,然后将幸存对象复制到ServiorTo中(达到老年代指标的对象存到老年代),随后清空Eden和ServicorFrom中的对象,最后交换serviorTo和ServiorFrom。(复制算法)
三个区域的比例Eden:from :to = 8:1:1 ,通过
-XX:MaxTenuringThreshold =
设置GC多少次晋升到老年代,默认15。老年代:
当老年代的空间不足时会触发MajorGC,首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象(标记清除法)
方法区:
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
私有部分
- PC 寄存器:保存线程当前正在执行的方法,任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,其地址被存在 PC 寄存器中。
- Java 虚拟机栈:用来存储栈帧,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
- 本地方法栈:用于管理本地方法(非java代码的接口)的调用
类加载
指虚拟机将字节码读取进内存,从而进行解析、运行等整个过程
类加载过程分为:加载、验证、准备、解析、初始化、使用、卸载
加载:
把代码数据加载到内存中
验证:
校验Class字节码文件格式是否符合JVM规范,加载完字节码后在内存中创建对应的Class对象,之后检查是否有语法错误
准备:
给类分配内存,只会先给static修饰的变量分配内存并赋予对应类型的0值(0, “”, false,null),而修饰了final的话则直接赋予用户希望的值
解析:
将引用在常量池中的符号引用替换成直接其在内存中的直接引用
初始化:
JVM 会根据语句执行顺序对类对象进行初始化
类初始化:给静态变量赋值,执行静态代码块,有父类会先类初始化父类,main函数所在类会被类初始化
对象初始化:会先类初始化。给成员变量赋值,执行普通代码块,执行构造函数
整体上就是惰性初始化,只有用到时才进行初始化
使用:
JVM 开始从入口方法开始执行用户的程序代码
卸载:
执行完程序,销毁Class对象,最后JVM也退出内存
垃圾回收
如果一个对象不再被引用,则会被当做垃圾而回收。
如何知道对象是否被引用
计数方法(已淘汰):对象被引用时就+1,不再引用就-1。存在的问题:**循环引用(**A引用B,B引用A,但没有被其它对象引用,两者引用都不为0,无法被回收)
可达性算法GC Root Tracing:从 GC Root 出发,所有可达的对象都是存活的对象,不可达的为垃圾。
GC Root 就是一组活跃引用的集合,通常GC Root包括:
- 所有当前被加载的 Java 类
- Java 类的引用类型静态变量
- Java类的运行时常量池里的引用类型常量
- VM的一些静态数据结构里指向GC堆里的对象的引用
- 等等
如何进行垃圾回收
- 标记清除法:标记所有由 GC Root 触发的可达对象,清除所有未被标记的对象。缺点:会有产生内存碎片的问题。
- 复制算法:将原有的内存空间分为两块,每次只使用一块,垃圾回收时将存活的对象复制到另一块,清除当前块内所有对象。缺点:内存没有被完全使用。
- 标记压缩算法:上面两种方法的结合。标记存活对象,复制到内存的一边,清空剩余区域。
实际是每种方法结合着用。新生代存活对象较少,适合复制算法;老年代存活对象多,适合标记清除法或标记压缩算法,减少对象的移动。
术语
Minor GC/Young GC:从年轻代空间回收内存
Major GC/Old GC:从老年代空间回收内存
Full GC:清理年轻代、老年代和永久代,当发现年轻代空间太小或永久代空间不足时会触发
Stop-The-World:在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔
垃圾回收器
串行回收器(Serial)
指使用单线程进行垃圾回收的回收器
分类
- 新生代串行回收器:采用复制算法,垃圾回收时其他线程需要暂停(Stop-The-World)
- 老年代串行回收器:采用标记压缩算法,也会Stop-The-World,可以与多种新生代回收器配合使用
参数
-XX:UseSerialGC
:新生代、老年代都使用串行回收器。-XX:UseParNewGC
:新生代使用 ParNew 回收器(多线程),老年代使用串行回收器。-XX:UseParallelGC
:新生代使用 ParallelGC 回收器(多线程),老年代使用串行回收器。
并行回收器(Parallel)
指使用多线程进行垃圾回收,可以有效缩短垃圾回收所使用的时间
分类
新生代ParNew 回收器:只是简单地将串行回收器多线程化
启动参数
-XX:+UseParNewGC
:新生代使用 ParNew 回收器,老年代使用串行回收器。-XX:UseConcMarkSweepGC
:新生代使用 ParNew 回收器,老年代使用 CMS。-XX:ParallelGCThreads
:指定 ParNew 回收器的工作线程数量。
新生代 Parallel GC 回收器:相比于ParNew更加注重系统的吞吐量
设置参数
-XX:+UseAdaptiveSizePolicy
:自动调节新生代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数-XX:MaxGCPauseMillis
:设置最大垃圾收集停顿时间-XX:GCTimeRatio
:设置吞吐量大小
启动参数
-XX:+UseParallelGC
:新生代使用 Parallel 回收器,老年代使用串行回收器。-XX:+UseParallelOldGC
:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
老年代 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.对象内存布局
对象头:
- Mark Word:哈希码、分代年龄、锁状态标志、线程持有的锁等
- 类型指针:指向当前对象是哪个类
实例数据:存放字段的信息
填充对齐
4.判断对象是否存活
引用计数
对象中添加引用计数器,记录当前引用的次数。缺陷:循环引用
可达性分析
从GCRoots出发,搜索可以到达的对象并标记,没有标记的对象就是可回收的
主要的GCRoots:
1.虚拟机栈中引用的对象
2.类静态属性引用的对象
3.常量引用的对象
4.本地方法栈中JNI引用的对象
5.引用种类
强引用:一定不会被回收
1
Object obj = new Object();
软引用:内存不足时回收
1
2
3Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);弱引用:垃圾回收时就会被回收
1
2
3Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
WeakReference reference = new WeakReference(obj, queue);虚引用:为了能在这个对象 被收集器回收时收到一个系统通知
1
2
3Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(obj, queue);
6.垃圾回收算法
- 标记清除
- 标记复制:新生代
- 标记整理:老年代
7.垃圾回收触发时机
- Minor GC(Young GC):Eden区空间不足时
- Full GC:
- Minor GC 前判断从年轻代升入老年代的对象可能放不下时
- Minor GC 后发现放不下时
- 老年代空间不足时
- 晋升/提前晋升的对象在老年代中放不下时
- System.gc()
8.晋升老年代时机
- 新生代中对象到达一定年龄
- 大对象直接晋升
- 动态判断对象是否需要晋升
- 把MinorGC后仍然放不下的对象放入老年代
9.主要垃圾收集器种类
- Serial收集器(串行收集器):一个线程垃圾回收,所有用户线程需要停下,新生代标记复制,老年代标记整理
- ParNew:多个Serial收集器,新生代并发,老年代还是单线程
- Parallel Scavenge(并行收集器):注重吞吐量(运行用户代码的时长),老年代也并发
- CMS(Concurrent Mark Sweep)收集器(并发收集器):老年代收集器,以最短停顿时间为目标,并发收集、低停顿
- G1(Garbage First)收集器(并发收集器):弱化分代,而是分区收集(伊甸区、辛存区、老年区、大对象区),解决内存碎片问题