offer-JVM

JVM

JVM内存与垃圾回收

1 JVM与Java体系结构

  • Java虚拟机是一台执行Java字节码的虚拟计算机;Java虚拟机就是二进制字节码的运行环境
  • 特点:一次编译,到处运行;自动内存管理;自动垃圾回收功能
  • 用户User ——> 字节码文件 ——> JVM ——> 操作系统 ——> 硬件
  • JVM的整体结构(HotSpot VM)
    结构简图
  • JVM的架构模型:Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。前者指令集小,后者指令少。后者执行效率高,但依赖硬件。
  • JVM的生命周期:
    1. 虚拟机的启动:通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。
    2. 虚拟机的执行:执行一个所谓的Java程序的时候,真真正正执行的是一个叫Java虚拟机的进程
    3. 虚拟机的退出:程序正常执行结束;异常或错误而导致终止;某线程调用Runtime类或System类的exit方法,Runtime类的halt方法。

2 类加载子系统

类加载器和类的加载过程

  1. 类加载器子系统作用:负责从文件系统或网络中加载Class文件;ClassLoader只负责加载,至于是否可以运行,则有执行引擎决定。

  2. 类的加载过程:(加载 ——> 链接(验证->准备->解析) ——> 初始化)

    • 加载:通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    • 链接:
      1. 验证:目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证、元数据验证、字节码验证、符号引用验证。 CAFE BABY
      2. 准备:为类变量分配内存并且设置该类变量的默认初始值,即零值。这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
      3. 解析:将常量池内的符号引用转换为直接引用的过程。事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
    • 初始化:初始化阶段就是执行类构造器方法()的过程。此方法不需定义,式javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在源文件出现的顺序执行。()不同于类的构造器()。若该类具有父类,JVM会保证字类的()执行前,父类的()已经执行完毕。虚拟机必须保证一个类的()方法在多线程下被同步加锁.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ClassInitTest {
    private static int num = 1;

    static{
    num = 2;
    number = 20;
    System.out.println(num);
    //System.out.println(number);//报错:非法的前向引用。
    }

    private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10

    public static void main(String[] args) {
    System.out.println(ClassInitTest.num);//2
    System.out.println(ClassInitTest.number);//10
    }
    }
  3. 类加载器的分类
    JVM支持引导类加载器用户自定义加载器两类。将派生于抽象类ClassLoader的类加载器都称为用户自定义加载器,如扩展类加载器、应用程序类加载器。

    • 引导类加载器底层为非java语言(C/C++)编写,Java的核心类库都是使用类加载器进行加载的,出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
    • 扩展类加载器派生于ClassLoader,jre/lib/ext。
    • 应用程序类加载器派生于ClassLoader,负责加载环境变量classpath或系统属性java.class.path指定路径下的类库,是程序中默认的类加载器。
    • ClassLoader,一个抽象类,loadClass(String name)或着findClass和defineClass组合使用,生成class实例,都不是抽象方法。
  4. 双亲委派机制(面试重点)

  • 工作原理:1)如果一个类加载器收到了类加载器的请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;2)如果父类加载器还存在其父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器;3)如果父类加载器可以完成类加载器任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
  • 优势:避免类的重复加载,保护程序安全,防止核心API被随意篡改。java.lang.wzh
  • 沙箱安全机制,保证对java核心源代码的保护
  1. 其他内容
  • 在JVM中表示两个class对象是否为同一类存在两个必要条件:1. 类的完整类名必须一致,包括包名。 2. 加载这个类的classLoader(指classLoader实例对象)必须相同。
  • 如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
  • 类的主动使用和被动使用。主动使用分为七种情况:1)创建类的实例;2)访问某个类或接口的静态变量,或者对该静态变量赋值;3)调用类的静态方法;4)反射;5)初始化一个类的子类;6)java虚拟机启动时被标明为启动类的类;7)JDK 7 开始提供的动态语言支持。类的被动使用不会导致类的初始化。

3 运行时数据区概述及线程

不同的JVM对于内存的划分方式和管理机制存在着部分差异。
JVM允许一个应用有多个线程并行的执行;在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。

4 程序计数器(PC寄存器)

介绍(Program Counter Register)

  1. JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟,程序钩子。
  2. 用来存储指向下一条指令的地址,由执行引擎读取下一条指令。内存空间小,运行速度最快的存储区域。
  3. 唯一一个没有规定任何OutofMemoryError的区域。

两个常见的问题

  1. 使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
    答: 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
  2. PC寄存器为甚会被设定为线程私有?
    答: 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

5 虚拟机栈

虚拟机栈概述

栈是运行时的单位,而堆是存储的单位。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。是线程私有的。
生命周期和线程一致。作用是主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
访问速度仅次于程序计数器,只有进栈出栈两个操作,对于栈来说不存在垃圾回收(GC)问题,但存在OOM问题。
栈中可能出现的异常:1. StackOverflowError异常(线程请求分配的栈容量超过java虚拟机栈允许的最大容量);2. OutOfMemoryError异常(没有足够的内存去扩展或创建)。 -Xss设置栈内存的大小

栈的存储单位

栈帧(Stack Frame);在这个线程上正在执行的每个方法都各自对应一个栈帧。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种信息。
当前栈帧——当前方法——当前类;
栈帧结构:局部变量表(Local Variables);操作数栈(Operand Stack)(或表达式栈);动态链接(Dynamic Linking)(或指向允许时常量池的方法引用);方法返回地址(Return Address)(或方法正常退出或者异常推出的定义);一些附加信息。

局部变量表

局部变量数组或本地变量表,定义一个数字数组,用于存储方法参数和定义在方法体内的局部变量,基本数据类型,对象引用,returnAddress类型。不存在数据安全问题,线程的私有数据;局部变量表所需的容量大小是在编译期确定下来的。只在当前方法调用中生效,随着栈帧的销毁而销毁。

  1. 局部变量表,最基本的存储单元是Slot(变量槽);32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot;按照顺序被复制到每一个slot上;
  2. 如果当前帧是由构造方法或实例方法创建的,那么该对象引用this将会放在index为0的slot处,其余按顺序。
  3. 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

例:按照在类中声明的位置分–> 1)成员变量,在使用前都经历过默认初始化赋值;类变量,linking的prepare阶段,默认赋值,initial阶段,显式赋值;实例变量,随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。 2)局部变量,在使用前,必须进行显式赋值,否则编译不通过。

在栈帧中,与性能调优关系最为密切的部分是局部变量表,局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈

栈可以使用数组或链表来实现。操作数栈是用数组实现,或叫表达式栈。根据字节码指令,入栈出栈。主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。编译期就定义好了栈深度。32bit一个栈单位深度,64bit两个。如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中。Java虚拟机的解释引擎是基于操作数栈的执行引擎。

代码追踪

1
2
3
4
5
6
7
8
9
10
public void testAddOperation() {
//byte、short、char、boolean:都以int型来保存
byte i = 15;
int j = 8;
int k = i + j;

// int m = 800;

}

  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中。
  • 面试题重点:常见的i++和++i有什么区别?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void add(){
//第1类问题:
int i1 = 10;
i1++;

int i2 = 10;
++i2;

//第2类问题:
int i3 = 10;
int i4 = i3++;

int i5 = 10;
int i6 = ++i5;

//第3类问题:
int i7 = 10;
i7 = i7++;

int i8 = 10;
i8 = ++i8;

//第4类问题:
int i9 = 10;
int i10 = i9++ + ++i9;
}

栈顶缓存技术

Top-of-Stack Cashing:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧的所属方法的引用。目的是将符号引用转换为调用方法的直接引用。

方法的调用:解析与分派

静态链接:如果被调用的目标方法在编译器可知,且运行期保持不变(早期绑定);
动态链接:如果被调用的方法在编译器无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法(晚期绑定)。

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

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

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

  • 普通调用指令:
    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本;
    2. invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本;
    3. invokevirtual:调用所有虚方法;
    4. invokeinterface:调用接口方法。
  • 动态调用指令:
    1. invokedynamic:动态解析出需要调用的方法,然后执行。如lambda表达式。

1,2调用的方法为非虚方法,其余(final修饰的除外)称为虚方法。

静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息。

1
2
3
Java: String info = "xxx";
JS: var name = "xxx";
Python: info = 130.5
  • 虚方法表:用索引表代替查找

方法返回地址

  • 帧数据区(动态链接、方法返回地址、一些附加信息)
  • 方法返回地址:存放调用该方法的pc寄存器的值。

一些附加信息及栈的相关面试题

  • 一些附加信息:栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。