Interview
JVM

JVM运行时数据区域

Heap(堆)

对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域;开启逃逸分析后,某些未逃逸的对象可以通过标量替换的方式在栈中分配堆细分:新生代、老年代,对于新生代又分为:Eden 区和 Surviver1 和 Surviver2 区;

方法区

对于 JVM 的方法区也可以称之为永久区,它储存的是已经被Java 虚拟机加载的类信息、常量、静态变量;Jdk1.8 以后取消了方法区这个概念,称之为元空间(MetaSpace);

当应用中的 Java 类过多时,比如 Spring 等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出

虚拟机栈

虚拟机栈是线程私有的,它的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(局部变量表、操作数栈、动态链接、返回地址);在 Java 虚拟机规范中,对此区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出 StackOverflowError 异常;
  • 如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

局部变量表:局部变量表是一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量。底层是变量槽 (variable slot)

操作数栈:是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。

动态链接:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。

返回地址(returnAddress):类型(指向了一条字节码指令的地址);JIT 即时编译器(Just In Time Compiler),简称 JIT 编译器:为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种 层次的优化,比如锁粗化等

本地方法栈

本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是 Java 方法,而本地方法栈服务的是 Native 方法。在 HotSpot 虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出 StackOverflowError 和 OOM 异常。

PC 程序计数器

PC,指的是存放下一条指令的位置的一个指针。它是一块较小的内存空间,且是线程私有的。由于线程的切换,CPU 在执行的过程中,需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的 PC。

JVM 垃圾回收

存活算法和两次标记过程

引用计数法

给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

优点:实现简单,判定效率也很高

缺点:它很难解决对象之间相互循环引用的问题,基本上被抛弃

可达性分析法

通过一系列的成为 ”GC Roots“(活动线程相关的各种引用,虚拟机栈帧引用,静态变量引用,JNI 引用)的对象作为起始点,从这些节点 ReferenceChains 开始向下搜索,搜索所走过的路径成为引用链,当一个对象到 GC ROOTS 没有任何引用链相连时,则证明此对象时不可用的;

两次标记过程

对象被回收之前,该对象的 finalize() 方法会被调用;

两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现 finalize() 方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。

垃圾回收算法

垃圾回收算法:复制算法、标记清除、标记整理和分代收集

复制算法(young)

将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收;

  • 优点:实现简单,内存效率高,不易产生碎片
  • 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低

标记清除(cms)

标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

  • 缺点:效率低,标记清除后会产生大量不连续的碎片,需要预留空间给分配阶段的浮动垃圾

标记整理(old)

标记过程任然与“标记-清除”算法一样,再让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题

分代收集

根据各个年代的特点选择合适的垃圾收集算法。

新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, ToSpace),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

Safepoint 当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为 JVM 是安全的(safe),整个堆的状态是稳定的。如果在 GC 前,有线程迟迟进入不了 safepoint,那么整个 JVM 都在等待这个阻塞的线程,造成了整体 GC 的时间变长

创建一个对象的步骤

步骤:类加载检查、分配内存、初始化零值、设置对象头、执行 init 方法

类加载检查:虚拟机遇到 new 指令时,首先去检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存,分配方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据 信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态 的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

执行 init 方法:从虚拟机的视⻆来看,一个新的对象已经产生了,但从 Java 程序的视⻆来看, 方法还没有执行,所有的字段都还为 零。所以一般来说(除循环依赖),执行 new 指令之后会接着执行 方法,这样一个真正可用的对象才算产生出来。