没有特殊说明,本文使用的是jdk8
参考
黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题)_哔哩哔哩_bilibili
目录
JVM作用
JVM 全称是 Java Virtual Machine,中文译名 Java虚拟机。
JVM 本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件。
解释和运行字节码
对字节码文件中的指令,实时的解释成机器码,让计算机执行。
内存管理
自动为对象、方法等分配内存
自动的垃圾回收机制,回收不再使用的对象
即时编译
JVM提供了即时编译(Just-In-Time 简称JIT) 进行性能的优化,最终能达到接近C、C++语言的运行性能,甚至在特定场景下实现超越。也就是对热点代码进行优化,提升执行效率。
字节码文件
文件组成
基本信息
- 魔数:固定前4个字节为0x ca fe ba be。表示是字节码文件类型。
- 主副版本号:用来表示编译字节码文件的jdk版本号
主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;
1.2之后大版本号计算方法就是: 主版本号 – 44 比如主版本号52就是JDK8副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容。 - 访问标识:标识是类还是接口、注解、枚举、模块
标识public final abstract - 类、父类、接口索引:通过这些索引可以找到类、父类、接口的信息
常量池
把相同的定义只保存一份,节省空间。常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。字节码指令中通过编号引用到常量池的过程称之为符号引用。
字段
当前类或接口声明的字段信息。
方法
当前类或接口声明的方法信息。
属性
类的属性,比如源码的文件名内部类的列表等
查看字节码
javap
javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。
输入javap -v 字节码文件名称 查看具体的字节码信息。(如果jar包需要先使用 jar –xvf 命令解压)
jclasslib
在IDEA中搜索并安装jclasslib插件。
使用方法:如果代码更新了,需要重新运行或编译一下。
arthas
使用方法
在官网下载arthas-boot.jar
项目运行后在cmd窗口启动这个java项目
package test;
public class TestArthas {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(2000);
}
}
}
输出字节码
查看源代码
可以通过这个查看代码部署的是否为最新版。
类的生命周期
类的生命周期描述了一个类加载,使用,卸载的过程。
加载
类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以使用Java代码拓展的不同的渠道。
然后把这些字节码信息生成一个InstanceKlass对象,这个对象保存类的所有信息,而且还会多保存特定功能的信息,比如多态。然后把这个对象保存到内存中的方法区。
多态的原理:
虚方法表中存了虚方法的地址。虚方法:非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。
虚拟方法调用:是在运行时根据实际对象的类型来确定要调用的方法的机制,而不是根据对象的声明的类型。
动态绑定:
编译时,Java 编译器只能知道变量的声明类型,而无法确定其实际的对象类型。而在运行时,Java 虚拟机(JVM)会通过动态绑定来解析实际对象的类型。
同时在堆中也会生成一份与InstanceKlass对象类似的java.lang.Class对象。这样可以在Java代码中获取类信息和存储静态字段的数据。
连接
验证
这个阶段不需要我们管,主要是为了检测字节码文件是否遵守Java虚拟机规范中的约束。
准备
静态变量赋值默认初始值,比如给int类型的赋值成0。
final修饰的直接赋值成设置的值。
解析
把常量池中的符号引用(使用编号来访问常量池中的内容)替换为直接引用,这样效率会更高。
初始化
- 执行静态代码块中的内容,并为静态变量赋值。执行顺序和代码顺序一致。
public class JvmTest1 {
public static int value = 2;
static {
value = 1;
}
// 上面二者顺序不一样,结果也不一样
public static void main(String[] args) {
System.out.println(value);
}
}
- 执行clinit指令(初始化指令)。
下面是会导致类的初始化的方式:
访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
调用Class.forName(String className)。
new一个该类的对象时。
执行Main方法的当前类。
下面是不会不会进行初始化指令
1.无静态代码块且无静态变量赋值语句。
2.有静态变量的声明,但是没有赋值语句。
3.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
小结一下:
- 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化 (除非要执行方法)。
- 直接访问父类的静态变量,不会触发子类的初始化。子类的初始化cinit调用之前, 会先调用父类的cinit初始化方法。
使用
使用阶段是指虚拟机在程序运行过程中,通过类加载器加载的类,被程序实际使用到的阶段。在使用阶段,虚拟机会执行类中的实例构造方法、普通方法等。
卸载
卸载阶段是指虚拟机在运行时动态卸载不再使用的类。当类加载器不再引用某个类时,虚拟机会对这个类进行卸载。虚拟机在满足一定条件时(例如该类所有实例已经被回收且没有剩余引用),会触发类的卸载。
类加载器
类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。类加载器只参与加载过程中的字节码获取并加载到内存这一部分。
分类
启动类加载器
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。加载Java中最核心的类。
默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。
通过启动类加载器去加载用户jar包:
- 放入jre/lib下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载
- 使用参数进行扩展:推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展
扩展类加载器
扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器。
默认加载Java安装目录/jre/lib/ext下的类文件。
通过扩展类加载器去加载用户jar包:
- 放入/jre/lib/ext下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容
- 使用参数进行扩展推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
应用程序类加载器
应用程序类加载器(Application Class Loader)是JDK中提供的、使用Java编写的类加载器。
加载的是我们自己写到类和接口,还有第三方jar包中的类和接口。
Arthas类加载器相关功能
查看加载器列表
查看加载的内容
双亲委派模型
每个Java实现的类加载器中保存了一个成员变量叫“父”(Parent)类加载器,可以理解为它的上级。
应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空。启动类加载器使用C++编写,没有上级类加载器。并不是继承关系
过程
先向上查找是否加载过
在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器。
再由顶向下进行加载
如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。
作用
- 保证类加载的安全性:通过双亲委派机制,让顶层的类加载器去加载核心类,避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。
- 避免重复加载:双亲委派机制可以避免同一个类被多次加载,上层的类加载器如果加载过类,就会直接返回该类,避免重复加载。
打破双亲委派
自定义类加载器
自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除。
Tomcat(一个服务器上可能会跑多个应用)通过这种方式实现不同应用之间相同的类隔离
线程上下文加载器
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”。提供了一种组件化的方式来实现面向接口的编程模式。SPI的核心思想是定义服务接口和服务实现分离,允许在运行时动态替换具体的实现。数据库加载驱动,日志接口都使用了SPI机制。
比如数据库的驱动DriverManager,启动类加载器先加载它。后续初始化DriverManager的时候,通过SPI机制加载jar包中的mysql驱动。其中SPI利用了线程上下文类加载器(应用程序类加载器)去加载并创建对象
Osgi框架的类加载器
历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载
内存数据区
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区。整理上可以分为以下两部分。
线程不共享
程序计数器
程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。
Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。
其中每个栈帧都有局部变量表,操作数栈和帧数据。
局部变量表:存放运行中的所有的局部变量,使用完的局部变量的位置还会被继续利用。
操作数栈:存放运行中的一些中间数据的数据结构,在编译器就确定大小了,执行时分配。
帧数据:存储动态连接(其他类的属性或方法的内存地址),方法出口(方法结束后要让程序计数器指向上一个栈帧中的下一条指令,所以要保存方法出口),异常表的引用(抛出异常时要跳转的指令)。
当栈帧过多,就会出现StackOverflowError的错误,一般是函数递归没有设置正确的结束条件导致的。
要修改Java虚拟机栈的大小,可以使用虚拟机参数 -Xss 。
语法:-Xss栈大小
单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
本地方法栈
本地方法栈和Java虚拟机栈类似,存储的是本地方法的栈帧。
在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
线程共享
堆
一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory错误。
堆空间有三个需要关注的值,used total max。
used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆
如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等。如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64。在实际应用中一般都需要设置total和max的值。
要修改堆的大小,可以使用虚拟机参数 –Xmx(max最大值)和-Xms (初始的total)。
语法:-Xmx值 -Xms值
单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
限制:Xmx必须大于 2 MB,Xms必须大于1MBJava服务端程序开发时,建议将-Xmx和-Xms设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况
方法区
方法区主要保存了:类的元信息,运行时常量池,字符串常量池。
类的元信息:在类加载阶段产生了,存储了每个类的基本信息。
运行时常量池:字节码文件通过编号找到的常量,这种常量池称为静态常量池。当常量池加载到内存后,可以通过内存地址快速定位到常量池中的内容,这种称为运行时常量池。
字符串常量池:保存代码中定义的常量字符串内容。
直接内存
直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。
在 JDK 1.4 中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题:
1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
2、IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。
现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
垃圾回收
方法区回收
方法区的对象回收有下面三个要求:
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
- 加载该类的类加载器已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用
堆回收
当堆上面的对象不在使用了,或者说访问不到了,就应该被回收。
“垃圾”定义
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
当然,即使有引用,但是无法访问到,就可以回收了。
找“垃圾”
计数器(不是Java使用的):引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。
可达性分析算法:Java使用的是可达性分析算法。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。
GC Root对象:
线程Thread对象。
系统类加载器加载的java.lang.Class对象。
监视器对象,用来保存同步锁synchronized关键字持有的对象。
本地方法调用时使用的全局对象。对象引用:
可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。
如何回收
标记清除算法:
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
2.清除阶段,从内存中删除没有被标记也就是非存活对象。
复制算法:
1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。
2.在垃圾回收GC阶段,将From中存活对象复制到To空间。
3.将两块空间的From和To名字互换。
标记整理算法:
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间
分代回收:
现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。
1分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区
接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。
此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
垃圾回收器
垃圾回收器种类很多,主要看三个评判标准
Serial垃圾回收器
SerialOld垃圾回收器
ParNew垃圾回收器
CMS垃圾回收器
1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象。
2.并发标记, 标记所有的对象,用户线程不需要暂停。
3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
4.并发清理,清理死亡的对象,用户线程不需要暂停。
Parallel Scavenge垃圾回收器
Parallel Old垃圾回收器
G1垃圾回收器