参考:
https://blog.csdn.net/csdnliuxin123524/article/details/81303711 写的很详细
简介
JVM是虚拟机的英文简称。它是java运行环境的一部分,是一个虚构出来的计算机,它是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
jvm 的组成体系
- 指令集 jvm指令集
- 类加载器ClassLoader(下面有介绍)在jvm启动时或者类运行时将所需要的类加载到jvm当中
- 执行引擎 负责执行Class文件字节码指令
- 运行时数据区 将内存划分几个区,分别完成不同的任务
- 本地方法区 由c或c++实现的本地代码返回的结果
在C语言体系中,每次申请一个变量都要为其分配对应大小的内存,用完之后还要自行销毁,而在java当中最大的优点就是我们不必用代码去管理内存,这里java自己创建了一套自己的内存管理机制,用垃圾回收器来定期回收那些没有被引用或者使用的内存,这样就保证了系统的高可用性,垃圾回收器只负责定期清理方法区和堆里面的内存
jvm各个内存模型详细介绍
程序计数器(Program Counter Register):
也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
- 当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
- 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈(Java Virtual Machine Stack):
-
线程私有的,它的生命周期与线程相同,每个线程都有一个。
-
每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double;和reference (32 位以内的数据类型,具体根据JVM位数(64为还是32位)有关,因为一个solt(槽)占用32位的内存空间 )、部分的返回结果,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址;
-
每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
-
栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进…F3栈帧,再弹出F2栈帧,再弹出F1栈帧。(
-
JAVA虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令,如下图:
-
对上图中的动态链接解释下,比如当出现main方法需要调用method1()方法的时候,操作指令就会触动这个动态链接就会找打方法区中对于的method1(),然后把method1()方法压入虚拟机栈中,执行method1栈帧的指令;此外如果指令表示的代码是个常量,这也是个动态链接,也会到方法区中的运行时常量池找到类加载时就专门存放变量的运行时常量池的数据。
本地方法栈(Native Method Stack):
-
先解释什么是本地方法:jvm中的本地方法是指方法的修饰符是带有native的但是方法体不是用java代码写的一类方法,这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
-
作用同java虚拟机栈类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
-
是线程私有的,它的生命周期与线程相同,每个线程都有一个。
Java 堆(Java Heap):
- 是Java虚拟机所管理的内存中最大的一块。
- 不同于上面3个,堆是jvm所有线程共享的。
- 在虚拟机启动的时候创建。
- 唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
- Java堆是垃圾收集器管理的主要区域。
- 因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
- java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
- 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area):
- 在虚拟机启动的时候创建。
- 所有jvm线程共享。
- 除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。
- 用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据。
(4)被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。 - 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
方法区补充:指令集是个非常重要概念,因为程序员写的代码其实在jvm虚拟机中是被转成了一条条指令集执行的,看下图
首先看看上面各部位位于13图中的那些位置:左侧的foo代码是指令集,可见就是在方法区,程序计数器就不用说了,局部变量区位于虚拟机栈中,右侧最下方的求值栈(也就是操作数栈)我们从动图中明显可以看出存在栈顶这个关键词因此也是位于java虚拟机栈的。
另外,图中,指令是Java代码经过javac编译后得到的JVM指令,PC寄存器指向下一条该执行的指令地址,局部变量区存储函数运行中产生的局部变量,栈存储计算的中间结果和最后结果。
上图的执行的源代码是:
public class Demo {
public static void foo() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
}
}
下面简单解释下执行过程,注意:偏移量的数字只是简单代表第几个指令哦,首先常数1入栈,栈顶元素就是1,然后栈顶元素移入局部变量区存储,常数2入栈,栈顶元素变为2,然后栈顶元素移入局部变量区存储;接着1,2依次再次入栈,弹出栈顶两个元素相加后结果入栈,将5入栈,栈顶两个元素弹出并相乘后结果入栈,然后栈顶变为15,最后移入局部变量。执行return命令如果当前线程对应的栈中没有了栈帧,这个Java栈也将会被JVM撤销。
类的生命周期
编译
java 通过javac编译器 编译成Class文件.class结尾
加载
class文件被jvm以字节码形式加载到内存当中,而加载则是由jvm提供的加载器来完成的,加载完成后会创建一个java.lang.Class对象,当这个对象被创建以后不会被二次创建,正如每个对象都有自身唯一id一样,而判断他们是否相同则由类的包路径和类的类名称来组成,但是在加载器中,加载器则不仅仅会根据对象自身的唯一标示来判断,还会外加上加载该类的加载器,也就是说加载器和唯一标示进行组合来判断class是否相同(关于加载器下面有介绍)
连接
包含了验证,准备,解析三个阶段
验证
验证是主要验证被加载的类是否由正确的内部结构,并和其他类协调一致。
验证的目的是确保Class类的字节流信息符合jvm自身运行条件,这样避免危害jvm自身运行环境。
验证主要包含四个验证阶段:
- 文件验证
验证Class字节流是否符合Class文件规范,并且能被jvm所处理。例如,是否以魔数0xCAFEBABE开头 - 元数据验证
对字节码的描述信息进行语义分析,是否符合java语言语法规范,比如该类继承的父类,是否允许被继承 - 字节码验证
最重要也是最复杂的验证环节,主要进行字节码的数据流和控制流分析,保证在运行过程中不会对jvm作出危害行为 - 引用符号验证
主要是将引用符号转换为直接应用时,在引用到第三阶段解析时,主要是确保在直接引用时能保证能访问得到,避免引用时无法访问的情况
准备
对类里面的静态变量分配内存并进行初始化,注意这里并不会给静态变量赋值,比如
private static final int TEST_NUM = 10;
这里只是给TEST_NUM变量分配一个内存,并且将其初始化对应类型的初始值为0
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对的是类或接口,字段,类方法,接口方法四类符号引用。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与内存布局无关。引用的目标并不一定已经加载到内存中。
- 直接引用:直接引用可以使直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局有关的,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同的。如果有了直接引用,那引用的目标必定已经内存在存在
初始化
这个阶段也就到了类加载过程当中的最后一个阶段了,前面的几个阶段中只有类的加载器可以开发者自行定义加载器去加载类可以参与其中的加载环节,其他的均为jvm自身主导和控制的。但是到了这个初始化阶段则完全由开发者自己主导,比如静态变量的赋值,静态方法块的执行等。
使用
这里就不用过多赘述了,前面的一切铺垫就是此时此刻,这个时候开发者可以使用前面加载的类来完成自己的项目需求。
销毁
被垃圾回收器回收,比如一个变量被显性的置为null,当方法执行完毕之后,该变量就会被垃圾回收器标示并进行回收
关于Classloader类加载器
类加载器又分为三种分别为BootStrap CLassLorder 根类加载器,ExtClassLorder 扩展类加载器,AppClassLorder系统类加载器,其中BootStrap CLassLorder为jvm自身运行需要加载的类,也就是为自己量身订造的类加载器,它是由C++来实现的,开发者无法对其获取和引用。ExtClassLorder 这个类本身是jvm自身的一部分,是用来加载java.ext.dirs路径下的类,它是用java来实现的。AppClassLorder(也可以成为应用类加载器)是加载classpath路径下的类,就是我们源代码编译后的类是由该加载器来负责加载的,我们自己定制的类加载器可以通过继承UrlClassLorder或者ClassLorder来实现,而UrlCLassLorder和CLassLorder都是继承自AppClassLorder,程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。这里需要注意的是BootStrap CLassLorder 和其他两个不存在任何关系,而AppClassLorder继承自ExtClassLorder。
类的加载入口loadClass:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//返回该加载类的锁对象,并对其进行加同步锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//通过包路径和类名称由当前加载器来加载,如果没有加载过,则将当前的parent加载器替换为父类加载器,如果没有父类加载器则为空
Class<?> c = findLoadedClass(name);
//如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//该父类加载器不为空
if (parent != null) {
//则根据父类加载器进行去判断有没有加载过
c = parent.loadClass(name, false);
} else {
//父加载器为空,则调用bootStrapClassLoader来加载此类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//如果所有的父类加载器都没有加载该类的话,就由子类加载器自己去加载,findClass方法由开发者自己定义,如果没有加载成功,则跑出ClassNotFoundException异常
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//如果resolve为true则对该类进行解析
if (resolve) {
resolveClass(c);
}
//如果该类被加载器加载直接返回
return c;
}
}
总结:
这里使用的是双亲委派机制,由父类一层一层往上递归去判断父类能不能加载此类并有没有加载过此类(这里保证了该类不会二次加载),如果没有则由子类加载器去加载。
由于双亲委派机制导致父类加载器加载的类不被二次加载避免了类的重复加载且保护了父类加载器(这里可以视为优先级高的类)加载的类,这样做的目的可以保护jvm自身环境不受恶意破坏而,这样提升了安全性,比如一些常用的包装类,工具类,如果被其他恶意加载器给重新加载,致使jvm无法正常工作。
程序在jvm的运行过程
- 程序在启动之前,class类会被类装载器装入方法区
- 执行引擎读取方法区的字节码边解析边运行
- pc寄存器指向main函数所在的所在位置
- 虚拟机开始为main函数在java栈中预留一个堆栈(每个方法都预留一个堆栈)
- 执行引擎开始运行main方法映射本地操作系统的相关实现,该实现保留在本地方法中
- 调用本地方法接口,操作系统会为本地方法分配本地方法栈,用来存储一些临时变量,这时开始运行本地方法,开始调用操作系统相关api等。