1、JVM生命周期

1、虚拟机的启动

Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成,这个类是虚拟机的具体实现指定的。

2、虚拟机的执行

  • 一个运行中的Java虚拟机有着一个清晰地任务:执行java程序。
  • 程序开始执行时他才运行程序,程序结束时他就停止。
  • 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。

3、虚拟机的退出

有如下的的几个情况:

  • 程序正常结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止
  • 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6WeQiZW-1617711741742)(JVM.assets/image-20201230142338108.png)]

2、类加载器

JVM中提供了三层的ClassLoader:

  • Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类

3、双亲委派机制

概念:Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

3.1、工作原理

  1. 如果一个类加载器收到了类加载器请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  2. 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vG7T29XJ-1617711741756)(JVM.assets/image-20201231100805493.png)]

3.2、双亲委派模型工作过程

1.当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。

2.当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。

3.如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。

4.如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。

5.如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。

6.如果均加载失败,就会抛出ClassNotFoundException异常。

3.3、沙箱安全机制

自定义String类,但是在加载自定义String类的时候会率先使用加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中 java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱机制

3.4、类的主动使用和被动使用

Java程序对类的使用方式分为:主动使用和被动使用

  • 主动使用,又分为其中情况:
  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • Java虚拟机启动时被表明为启动类的类
  • JDK7还是提供的动态语言支持
  • 除了以上其中情况,其他Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

4、运行时数据区概述及线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qg1urdFI-1617711741759)(JVM.assets/image-20210112154931544.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eEq21Edk-1617711741764)(JVM.assets/image-20210112155214279.png)]

  • 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
  • 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
  • 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
  • 操作系统负责所有的线程安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。

后台同线程在Hotspot JVM里主要是以下几个:

  • 虚拟机线程:这种线程的操作是需要JVM到达安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM到达安全点,这样堆才不会变化。这种线程的执行类型包括“stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
  • 周期任务线程:这种线程是时间周期时间的提现。他们一般用于周期性操作的调度执行。
  • GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
  • 编译线程

4.1、PC寄存器

作用:PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

  • JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在Java虚拟机规范中没有规定任何OutOtMemeryError情况的区域。

5、虚拟机栈的主要特点

栈是运行时的单位,而堆是存储的单位

解决程序的运行问题,即程序如何执行,或者说如何处理数据。

解决的是数据存储的问题,即数据怎么放、放哪儿。

5.1、虚拟机栈的概述

Java虚拟机栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack frame),对应着一次次的Java方法调用。

  • 是线程私有的。
  • 生命周期:
  • 生命周期和线程一致
  • 作用
  • 主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的特点:
  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接对Java栈的操作只有两个:
  • 每个方法执行,入栈
  • 执行结束后出栈
  • 对于栈来说,不存在垃圾回收问题
设置栈内存大小

我们可以使用参数 -Xss 选项来设置线程最大的栈空间,栈的大小直接决定了函数调用的最大可达深度。

5.2、栈的存储单位

栈中存储什么?
  • 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧是有效的),这个栈帧被称为当前栈帧。与当前栈帧相对应的方法就是当前方法,定义这个方法的就是当前类。
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
栈帧的内部结构
  • 局部变量表
  • 操作数栈
  • 动态链接(或指向运行时常量池的方法引用)
  • 方法返回地址
  • 一些附加信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nscXU3MR-1617711741767)(JVM.assets/image-20210113191112428.png)]

  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得当前一个栈帧重新成为当前栈帧。
  • Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出

5.3、局部变量表

  • 局部变量表也被称之为局部变量数组或者本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型。
  • 由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题。
  • 局部变量表所需的容量大小是在编译期间确定下来的,并保存在方法的code属性的maximun local variables数据项中。在方法运行期间是不会改变局部变量表的大小。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会销毁

关于Slot的理解

  • 参数值的存放总是在局部变量数据的Index0开始,到数组长度减1的索引结束。
  • 局部变量表,最基本的存储单元是Slot(变量槽)
  • JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按顺序被复制到局部变量表中的每一个slot上。
  • 如果需要访问局部变量表中一个64bit的局部变量时,只需要使用前一个索引即可。
  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余参数按照参数表顺序继续排列。
    在栈帧中,与性能调优关系最为密切的部门就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

5.4、操作数栈

  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
  • 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  • 每个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的code属性中,为max_stack的值。
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

5.5、代码追踪

5.6、栈顶缓存技术

5.7、动态链接(指向运行时常量池的方法引用)

  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGnTY8qU-1617711741771)(JVM.assets/image-20210201180755419.png)]

5.8、方法的调用:解析与分派

绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

早期绑定:

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪个,因此可以使用静态链接的方式将符号引用转换为直接引用。

晚期绑定:

如果被调用的方法在编译期间无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

非虚方法:

  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  • 其他的方法则称为虚方法。

虚拟机中提供以下几种方法调用指令:

  • 普通调用指令:
  1. invokestatic:调用静态方法,解析阶段确定方法版本
  2. invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  3. invokevirtual:调用所有虚方法
  4. invokeinterface:调用接口方法
  • 动态调用指令
  • invokedynamic:动态解析出需要调用的方法,然后执行。
invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
关于invokedynamic指令
  • 动态类型语言和静态类型语言
  • 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期编译期间的就是静态语言,反之就是动态语言。
  • 静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量没有类型信息,变量值才有类型信息。

5.9、方法返回地址

5.10、一些附加信息

常见的问题

1、使用PC寄存器存储字节码指令地址有什么用呢?

因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪继续开始。

2、为什么使用PC寄存器记录当前线程的执行地址呢?

JVM的字节码解释器就需要改变PC寄存器的值来明确下一跳应该执行什么样的字节码指令。

3、PC寄存器为什么会被设定为线程私有?

4、为什么使用常量池

常量池的作用,就是为了提供一些符号和常量,便于指令的识别。

栈相关面试题