关于jvm:一文图解JVM面试的全部考点

1次阅读

共计 9399 个字符,预计需要花费 24 分钟才能阅读完成。

前言

本文将通过简介 Class 字节码文件,Class 类加载,类加载完后的 JVM 运行时数据区,运行中的 GC 垃圾回收等相干常识带大家理解在面试时 JVM 常问的考点。

注:本文为 转载文章,原文作者:ElasticForce , 原文地址:JVM 面试常考点全在这:多图看懂 Java 虚拟机。

本文主线:

①、Class 字节码文件

②、Class 类加载

③、JVM 运行时数据区

④、GC 垃圾回收

⑤、常见的垃圾回收器

Class 字节码文件

class 文件也叫字节码文件,由 Java 源代码(.java 文件)编译而成的字节码(.class 文件)。

Class 文件格式:

无符号数:专用的数据类型,以 u1、u2、u4、u8 来代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。

表:通常以“_info” 结尾,由多个无符号数和其它表形成的复合数据类型。

魔数:标识这个文件是否是一个能被虛拟机所承受的 class 文件,值是固定的:0xCAFEBABE

副版本号和主版本号:判断是否是以后虚拟机所反对的版本

常量池:能够看作是 class 文件的资源仓库,蕴含 class 文件构造及其子结构中援用的所有字符串常量、类或接口名、字段名和其余常量。

Class 类加载

类的生命周期

  1. 加载:通过类的 全限定类名 查找并加载类的二进制字节流
  2. 连贯:目标是将加载的二进制字节流的动态存储构造转化为办法区的运行时数据结构

    • 验证:确保被加载的 class 文件中的字节流,符合规范,保障不会对虚拟机造成危害,如类文件构造查看、元数据验证、字节码验证、符号援用验证
    • 筹备:为类的动态变量分配内存,并初始化(默认值)
    • 解析:把常量池中的符号援用转换成间接援用,让类可能间接援用到想要依赖的指标
  3. 初始化:执行类结构器 < clinit > 办法(类结构器不是实例结构器),为类的动态变量赋初始值
  4. 应用:分为被动应用和被动应用,JVM 只会在每个类或接口首次被动应用时才初始化该类

    • 被动应用:

      ①、创立类实例

      ②、拜访类或接口的动态变量、调用静态方法

      ③、反射调用某个类

      ④、初始化某个类的子类,也会初始化该类

      ⑤、实现的接口蕴含默认办法,该接口会被初始化

    • 被动应用:

      ①、通过子类拜访父类的动态变量,不会初始化子类

      ②、拜访类中的常量,不会初始化该类

类加载器:

  • 启动类加载器:负责将 <JAVA_HOME>/lib,或者 -Xbootclasspath 参数指定的门路中的,必须是 JVM 辨认的类库加载进内存(依照名字辨认例如 rt.jar,不装载不能辨认的文件)
  • 扩大类加载器:负责加载 <JRE_HOME> /lib/ext,或者 java.ext.dirs 零碎变量所指定门路中的所有类库
  • 应用程序类加载器:负责加载 classpath 门路中的所有类库

双亲委派模型:

JVM 中的 ClassLoader 除了启动类加载器外,其余的类加载器都应该有本人的父级加载器(如上图所示)。

双亲委派模型的 工作流程

  1. 一个类加载器接管到类加载申请后,本人不去尝试加载这个类,而是先委派给父类加载器,层层委派,始终到启动类加载器
  2. 如果父级加载器不能实现加载申请,比方在它的搜寻门路下找不到这个类(ClassNotFoundException),就反馈给子类加载器,让子类加载器进行加载

双亲委派模型的 作用:保障了 Java 程序的稳固运作,使得一些公共类只会被加载一次,且避免了外围类库被用户自定义的同名类所篡改

JVM 运行时数据区

留神:下图为 JDK1.7 版本:

在 JDK1.8 中,曾经将办法区移除了,应用 元空间 进行代替,并且元空间也不是位于 JVM 的运行时的数据区中了,而是位于 本地内存 中,本地内存指的是什么呢?先来看一张大图,就明确本地内存是什么了!

留神:下图为 JDK1.7 版本的:

先对上图进行形容下

当要启动一个 Java 程序的时候,会创立一个相应的过程;

每个过程都有本人的虚拟地址空间,JVM 用到的内存(包含堆、栈和办法区)就是从过程的虚拟地址空间上调配的。请留神的是,JVM 内存只是过程空间的一部分,除此之外过程空间内还有代码段、数据段、内存映射区、内核空间等。

在 JVM 的角度看,JVM 内存之外的局部叫作本地内存。

而后在来看 JDK1.8 版本的一张大图,应该霎时就会明确了:

程序计数器:

程序计数器也叫 PC 寄存器(Program Counter),是线程公有的,即每个线程都有一个程序计数器,在线程创立时会创立对应的程序计数器,用来存储指向下一条指令的地址。

程序计数器占用很小的内存空间,JVM 标准中没有规定这块内存会 OOM。

虚拟机栈:

虚拟机中每个线程都有本人独有的虚拟机栈,该栈和线程同时创立,用于存储栈帧(Frame),每个办法被调用时会生成一个栈帧压栈,栈帧中存储了局部变量表、操作数栈、动静链接、办法返回地址。

栈帧是用来存储数据和局部过程后果的数据结构,同时也用来解决动静链接、办法返回值和异样分派。

栈帧随着办法调用而创立,随着办法完结而销毁。

每个栈帧都有本人的:局部变量表、操作数栈和指向以后办法所属的类的运行时常量池的援用。

正在执行的栈帧称为 以后栈帧 ,对应的办法则是 以后办法 ,定义了该办法的类就是 以后类

局部变量表:

局部变量表寄存了编译期可知的各种根本数据类型和援用类型,slot 是虚拟机为局部变量分配内存所应用的最小单位,每个 slot 寄存 32 位及以内的数据,对于 64 位的 long、double 数据类型须要应用两个间断的槽位,slot 槽位是复用的,能够节俭栈帧的空间应用。

局部变量表中的局部变量索引从 0 开始,当调用办法为实例办法时,第 0 个局部变量肯定是存储的该实例办法所在对象的援用(this 关键字),之后依据变量定义的程序和作用域调配 slot。

public class HelloJvm{public static int test1(int a,int b){return a+b;}
    public int test2(int a,int b){
        int c = a+b;
        return a+c;
    }
}
/**
*                slot name Signature
* 对于 test1 来说:    0     a    I     
*                1     b    I     
*
* 对于 test2 来说:    0   this  ../HelloJvm
*                1     a    I 
*                2     b    I     
*                3      c       I
*/

操作数栈:

每个栈帧外部都蕴含一个称为操作数栈的后进先出(LIFO)栈。操作数栈是用来寄存办法在运行时,各个指令操作的数据。

栈帧在刚刚创立时,操作数栈是空的。Java 虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作后果从新入栈。

比方对于下面的 test2 办法中指令和对应的操作数栈:

public class HelloJvm{public static void main(String[] args){HelloJvm h = new HelloJvm()
        int res = h.test2(1,2);
        System.out.println(res);
    }
}
/**
* 对于 test2 的栈帧中:
* 局部变量表:0    this ../HelloJvm
*            1      a       I
*            2      b       I
*            3      c       I
*
* 指令:    0 iload_1(加载局部变量表中 slot1 的 int 数据)
*          1 iload_2(加载局部变量表中 slot2 的 int 数据)
*          2 iadd(int 相加)
*        3 istore_3(将值存储到局部变量表中 slot3 的 int 数据)
*        4 iload_1
*        5 iload_3
*        6 iadd
*        7 ireturn(返回 int 类型)    
*                
*
* 对应的操作数栈:      return 4(栈顶)
*                    4
*                    c=3
*                    a=1
*                    c=3
*                    3
*                    b=2
*                    a=1(栈底)        
*/

动静链接:

每个栈帧外部都蕴含一个指向以后办法所在类型的运行时常量池的援用,以便对以后办法的代码实现动静链接。

在 class 文件外面,一个办法若要调用其余办法,或者拜访成员变量,则须要通过符号援用来示意,动静链接的作用就是将这些以符号援用所示意的办法转换为对理论办法的间接援用。

办法返回:

办法执行后,有两种形式退出该办法:失常调用实现,执行返回指令。异样调用实现,遇到未捕捉异样,不会有办法返回值给调用者。

本地办法栈:

和虚拟机栈构造相似,不过是用来反对 native 本地办法的执行。

堆区:

VM 治理的内存中占比最大的一块内存,在 JVM 启动时创立,用来寄存利用创立的对象和数组,所有的线程共享堆内存。

在 JVM 中,堆(heap)是可供各个线程共享的运行时内存区域,也是供所有类对象和数组对象分配内存的区域。

堆在运行期动态分配内存大小,堆的容量能够固定,也能够依据程序的需要进行伸缩。

堆中主动进行垃圾回收,无需也不能显式地销毁一个对象。

堆空间组成:

堆中依据对象在内存中存活工夫的长短,空间进行了分代;

  1. Java 堆 = 新生代 + 老年代
  2. 新生代 = Eden + S0 + S1

堆中默认占比 1 / 3 是 新生代 (Young generation),2/ 3 是 老年代 (Old generation),能够通过-XX:NewRatio 参数调整。

新生代中有 伊甸区 (Eden 区)和 幸存者区 (Survivor 区),Survivor 区中又分为两块:From 区和 To 区。
默认比为 8:1:1,能够通过 -XX:SurvivorRatio 参数调整。

新生代用来放新调配的对象,每次应用 Eden 区和一块 Survivor 区。新生代中通过一次垃圾回收后,没有回收掉的对象,将被复制到另外一块 Survivor 区,同时对象年龄 +1。(新生代 GC 算法是 复制算法

通过屡次 GC 后,当仍存活对象年龄达到指定值,将进入老年代。

因而老年代存储对象比新生代对象年龄大得多。此外,老年代也存储着大对象,也就是当大对象产生时,可能会间接从新生代进入老年代。

堆中的对象内存散布:

对象的组成:

  • 对象头
  • 实例数据:该对象理论存储数据的中央
  • 对齐填充:JVM 以字节为最小单位调配,所以要求对象的大小必须是 8 的倍数,这部分是为了填充位数对齐到 8 的倍数,没非凡意义,也不肯定存在。

留神:数组对象相比于一般对象,在对象头中要多一个数组长度的字宽。

对象头:

堆、栈、办法区的交互:

对象的拜访定位:

JVM 中只规定了 reference 类型是一个指向对象的援用,但没有规定这个援用具体如何去定位、拜访堆中对象的具体位置。

目前两种常见的拜访形式为:句柄、间接指针

句柄:

句柄形式: Java 堆中为句柄池划分一块内存,reference 中存储了对应句柄的地址,在句柄中存储对象的实例数据和类元数据的地址,也就是通过两次寻址就能拜访到对应的数据。

间接指针:

间接指针形式: 而 reference 存储的就间接是对象的地址,而拜访对象类型数据的指针寄存在堆中。

留神:间接指针的形式比句柄更快,少了一次指针定位的操作。

办法区:

JDK1.7 及之前版本是存在办法区的,而在 JDK1.8 时就将其移除了;

用来保留虚拟机装载的类的构造信息,运行时常量池、字段、办法、办法字节码,是各个线程共享的内存区域。

JVM 标准把办法区形容为堆的一个逻辑局部,但它有一个别名:非堆(Non-heap), 是为了与堆内存离开。

办法区中寄存的是什么?

在 Java 虛拟机中,办法区(method area)是可供各个线程共享的运行时内存区域。它存储了每一个类的构造信息,例如:运行时常量池、字段和办法数据、构造函数和一般办法的字节码内容,还包含一些在类、实例、接口初始化时用到的非凡办法。

JDK8 之前,办法区的实现是 永恒代,也叫长久代,是属于 JVM 虚拟机的内存空间

因为这些起因:

  1. 永恒代的大小不好指定。
  2. 永恒代会给 GC 带来不必要的复杂度,回收效率升高
  3. 字符串寄存在永恒代中容易导致性能问题和内存溢出(字符串常量池从 JDK7 开始,从办法区挪动到堆中)。
  4. JRockit 和 Hotspot 的整合(JRockit 没有永恒代)

因而从 JDK8 开始,去掉了永恒代,取而代之的是 元空间 (meta space),元空间并不属于 JVM 虚拟机外部内存,而是间接应用的 本地内存;本地内存下面有介绍的。

GC 垃圾回收

GC 是什么?

在零碎运行过程中,会产生一些无用的对象,这些对象占据着肯定的内存,如果不对这些对象清理回收无用对象的内存,可能会导致内存的耗尽,所以垃圾回收机制回收的是内存。

对象回收判断:

所谓垃圾,就是指内存中曾经不再被应用到的内存空间就是垃圾。

垃圾断定的算法有两种:援用计数法、根搜索算法

援用计数法:

给对象增加一个援用计数器,有其余对象援用该对象就 +1,援用生效时就 -1。当援用计数器为 0 时,示意不再被任何对象援用,就成为了垃圾。

这种形式实现简略高效,然而有一个致命毛病:不能解决循环援用问题。

比方对象 A 和对象 B 相互援用,单方的援用计数器都为 1,然而 A 和 B 都不再被应用了,却没被垃圾回收。

根搜索算法(也叫 可达性剖析):

也叫 可达性分析法,从根节点(GCRoots)开始向下搜寻对象节点,搜寻过的路经称为援用链,当一个对象到根之间没有连通,就表明该对象不可用。

常见的可作为 GC Roots 的对象包含:

  • 虚拟机栈(栈帧局部变量表)中援用的对象
  • 办法区类动态属性援用的对象
  • 办法区中常量援用的对象
  • 同步锁(synchronized 关键字)中持有的对象
  • 本地办法栈中调用的对象

不过对于根搜索算法来说,每次 GC 前都遍历所有的 GCRoots 并不事实。所以 Hotspot 引入了一个 OopMap 的映射表来实现精确式 GC,这个 OopMap 能够用来记录对象间的援用关系。

JVM 在类加载实现后,计算出对象内的什么偏移量上有什么援用,而后记录在 OopMap 中。垃圾判断时不必再从根节点遍历,而是间接从 OopMap 中扫描。

JVM 能够通过 OopMap 疾速实现 GCRoots 枚举,然而同样 OopMap 也存在一个问题,就是在程序运行中,援用关系可能随时变动,如果每条指令都去保护 OopMap,这样做的老本很高。

所以 JVM 引入了 平安点(Safe Point),在平安点时才去记录变动的 OopMap。同时 JVM 要求,只有以后线程到了平安点后,能力暂停下来进行 GC。

而对于在一段代码内,援用关系不会变动,任何工夫进行 GC 都是平安的,这块区域称之为 平安区域(Safe Region)。

判断一个对象是否是垃圾:

①、根搜索算法判断

②、否有必要执行 finalize 办法

③、通过前两个步骤后对象依然不被应用,就属于垃圾

如果该对象没有笼罩重写 finalize(),又或者是重写了 finalize(),然而被调用过一次了(finalize 办法只能被运行一次),就没有必要执行 finalize()。

如果是第一次执行 finalize(),会将其放入 FinalizerThread 的 ReferenceQueue 中,此时这个对象成为 Finalizer,临时不会被回收(依然被援用),当 finalize()执行实现后,就不再有任何援用,能够被垃圾回收。

因而能够在 finalize 办法中进行对象自救(办法内再次让该对象被援用),来实现豁免 gc,但只能自救一次。

不过并不举荐重写 finalize(),因为在对象进入队列后,还是占用着堆空间的内存。

援用分类:

Java 中援用分为四类:

  • 强援用(StrongReference)
  • 软援用(SoftReference)
  • 弱援用(WeakReference)
  • 虚援用(PhantomReference)

不同的援用体现了不同的可达性状态和对垃圾回收的影响。

强援用:

像 Object a = new A()这样的一般对象援用,只有还有强援用指向一个对象,就表明对象还存活,不可被回收。对于一个对象,如果没有其余的援用关系,只有超过了作用域,或者将援用赋值为 null,就示意能够回收,然而具体回收机会还是看垃圾回收策略。

软援用:

用 java.lang.ref.SoftReference 来实现,比强援用弱一点的援用,如果内存空间足够,就不会被回收,只有在垃圾回收后内存还不够,才会对软援用对象进行回收。能够转化为强援用。

弱援用:

用 WeakReference 来实现,用来示意一种不是必须的对象,比软援用还要弱,并不能豁免垃圾回收,每次垃圾回收时会被回收掉。能够转化为强援用。

虚援用:

用 PhantomReference 来实现,也被称为幽灵援用或幻影援用,是最弱的援用,不能通过这个援用拜访对象,能够用来确保对象执行 finalize()后,来实现某些机制,比方垃圾回收的跟踪。垃圾回收时会被回收掉。

垃圾回收算法:

标记 - 革除算法:

标记革除算法(Mark Sweep)分为标记和革除两个阶段,先标记可回收的对象,而后对立回收这些对象。

  • 长处:实现简略
  • 毛病:效率不高,会产生不间断的内存碎片,可能导致调配大对象时间断的空间有余会引发 GC

复制算法:

复制算法(Copying)会把内存分成两块完全相同的区域,每次只应用其中一块,另一块临时不必。当触发 GC 时,就把应用的这块上还存活的对象拷贝到另外一块,而后革除掉这块区域,去应用另外一块。

  • 长处:实现简略,效率高,不放心内存碎片
  • 毛病:空间利用率低,每次只能应用一半的内存空间

标记 - 整顿:

[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-KPENmS7L-1606056723598)(https://cdn.jsdelivr.net/gh/l…]

标记整顿算法(Mark Compact),和标记革除相似,也有雷同的标记过程,不过回收时并不是间接革除,而是让存活的对象都向一端挪动,而后革除掉边界以外的内存。尽管整顿的效率要低于间接革除,然而通常不会产生内存碎片。

分代收集:

之前提到了堆中内存是使用的分代思维,分为新生代和老年代,因而 GC 也是分代收集的。

  • MinorGC(YoungGC):产生在新生代的 GC
  • MajorGC(OldGC):产生在老年代的 GC(除了 CMS 收集器,其余收集器会同时收集新生代)
  • MixedGC:G1 收集器特有,收集整个新生代以及局部老年代
  • FulIGC:收集整个 Java 堆和办法区的 GC

新生代应用的是复制算法,将内存分为一块较大的 Eden 区和两块较小的 Survivor 区,每次应用 Eden 区和其中一块 Survivor 区,也就是每次应用 90% 的新生代内存。

产生 GC 时,把存活的对象复制到另一块 Survivor 区。如果对象超过另一块 Survivor 区下限,则间接进入老年代。

而老年代多选用标记整顿算法,因为复制算法空间节约验证,并且在存活对象比拟多的时候,效率较低。因而老年代个别不会选用复制算法,而是选用标记整顿或标记革除。

垃圾收集器:

下图展现堆中新生代和老年代中能够应用的各种垃圾回收器。

Serial + SerialOld:

Serial(串行)收集器,是一个单线程收集器,特点是是简略高效,对于单 CPU 来说,没有多线程的切换交互的开销,可能会更高效,是默认的 Client 模式下的新生代收集器。

SerialOld 是它的老年代版本,应用标记整顿算法。

在垃圾收集时,须要进行其余工作线程,也就是 STW(Stop-The-World):

STW 是 Java 中一种全局暂停的景象,也就是用户线程暂停运行,多是因为 GC 造成。过多的 STW,导致服务长时间进行,没有响应,用户体验极差。

而垃圾革除的算法的抉择,GC 收集器的一直进化,就是为了缩小 STW 的工夫

Parallel Scavenge + ParallelOld:

Parallel Scavenge 收集器,应用多线程进行垃圾回收,在垃圾收集时会 STW,利用于新生代的,应用复制算法的并行收集器。特点是着重关注吞吐量,动静调整以提供最合适的进展工夫或者最大吞吐量,可能最高效率的利用 CPU,适宜 CPU 资源敏感的利用。

ParallelOld 是 Parallel Scavenge 的老年代版本,应用标记整顿算法。

ParNew + CMS:

ParNew 收集器,是 Serial 的多线程版本,和 Parallel Scavenge 收集器相似,应用多线程进行垃圾回收,在垃圾收集时会 STW。

在并发能力好的 CPU 环境里,STW 的工夫要比 Serial 短。但对于单 cpu 或并发能力差的 CPU,因为多线程切换的开销,效率可能低于 Serial 收集器。

ParNew 是 Server 模式下的首选新生代收集器,通常和 CMS 收集器配合应用:

CMS(Concurrent Mark and Sweep)并发标记革除收集器,垃圾回收时有四个步骤:

  1. 初始标记:只标记 GCRoots 能间接关联到的对象,会进行 STW,但速度快,STW 工夫短
  2. 并发标记:从间接关联对象开始追踪标记
  3. 从新标记:修改在并发标记期间,因程序运行导致变动的标记,此时也会 STW
  4. 并发革除:并发回收垃圾对象

CMS 能够实现低进展,用户线程和 GC 线程并发执行。不过 CMS 革除不了浮动垃圾,可能导致 Full GC,且 CMS 采纳的是标记革除算法,产生的内存碎片,在为大对象调配空间时也会导致 Full GC。

CMS 的备用收集器为 SerialOld,当 CMS 呈现问题时,就应用 SerialOld 收集器作为老年代的收集器。

G1 收集器:

G1(Garbage First)收集器是实用于 服务端利用 的收集器,G1 不是只对某个代进行回收,它是对 整个堆 中的对象进行回收。G1 的设计指标是适应以后的多 CPU、多核环境的硬件劣势,进一步缩短 STW 的工夫。

G1 采纳的是 标记整顿和复制算法

不过 G1 和后面的收集器有些不同,它把内存划分成多个独立的固定大小的区域(Region),在 HotSpot 中,堆被分为了 2048 个 Region,每个 Region 大小在 1~32M 之间,Region 是垃圾回收的根本单位。

尽管 G1 保留了分代思维,也有新生代和老年代,不过并不隔离,每个 Region 能够属于任意代。

G1 还设置了 Humongous 区域,专门用来存储大对象。在 HotSpot 中,当对象大小超过 Region 的 1 / 2 时,须要用一个或多个间断的 Region 寄存该对象,这种区域就是 Humongous。

G1 最重要的个性:可预测进展,也就是用户能指明一个时间段,要求 G1 尽量在这个时间段实现垃圾回收。通过 -XX:MaxGCPauseMillis=n参数配置。

G1 保护了一个优先级列表,它会跟踪各个 Region 外面垃圾的价值大小,依据用户设置的进展工夫内,回收垃圾带来的收益进行优先级判断,抉择出容许工夫内价值最大的 Region,从而保障在无限工夫内的高效收集。

跟 CMS 收集器相似的是,G1 的 MixedGC 也分为四个阶段:

  1. 初始标记:只标记 GCRoots 能间接关联到的对象,会触发 STW,同时随同着一次一般的 YoungGC,新生代的回收也是初始标记。
  2. 并发标记:从 GCRoots 开始向堆中对象进行可达性分析判断。
  3. 最终标记:修改并发标记期间, 因程序运行导致标记发生变化的那一部分对象,会触发 STW。
  4. 筛选回收:依据用户冀望进展工夫来进行价值最大化的回收,会触发 STW。

对于 G1 来说,YoungGC 只回收新生代,而 MixedGC 会回收新生代和老年代。因而对于 MixedGC 的初始标记,共用了 YoungGC 的 STW。

筛选回收时,G1 会对筛选出的区域,抉择须要留下的对象拷贝到新的区域,其余被选中的对象全副革除掉。

❤ 关注 + 点赞 + 珍藏 + 评论 哟

如果本文对您有帮忙的话,请挥动下您爱发财的小手点下赞呀,您的反对就是我一直创作的能源,谢谢!

您能够 VX 搜寻【木子雷】公众号,保持高质量原创 java 技术文章,值得您关注!

往期文章回顾

一文让你彻底明确 JVM 参数该怎么设置

正文完
 0