关于面试:这些不可不知的JVM知识我都用思维导图整理好了

4次阅读

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

JVM 是面试中必问的局部,本文通过思维导图以面向面试的角度整顿 JVM 中不可不知的常识。

先上图:

1、JVM 基本概念

1.1、JVM 是什么

JVM 的全称是 「Java Virtual Machine」,也就是咱们耳熟能详的 Java 虚拟机。

JVM 具备着计算机的根本运算形式,它次要负责把 Java 程序生成的字节码文件,解释成具体零碎平台上的机器指令,让其在各个平台运行。

JVM 是运行在操作系统上的,它与硬件没有间接的交互。

当然,严格来说 JVM 也是虚拟机标准,有很多不同的实现,Sun/OracleJDK 和 OpenJDK 中的默认 Java 虚拟机是 HotSpot 虚拟机,是目前应用范畴最广的 Java 虚拟机,个别讲到的 JVM 默认指的就是 HotSpot 虚拟机。

1.2、Java 程序运行过程

咱们都晓得 Java 源文件,通过编译器,可能生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。

也就是如下:

每一种平台的解释器是不同的,然而实现的虚拟机是雷同的,这也就是 Java 为什么可能跨平台的起因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者敞开,则虚拟机实例沦亡,多个虚拟机实例之间数据不能共享。

1.3、JDK、JRE、JVM

  • JDK(Java Development Kit Java 开发工具包),JDK 是提供给 Java 开发人员应用的,其中蕴含了 Java 的开发工具,也包含了 JRE。其中的开发工具包含编译工具 (javac.exe) 打包工具(jar.exe) 等。
  • JRE(Java Runtime Environment Java 运行环境) 是 JDK 的子集,也就是包含 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其余组件,用于运行 Java 编程语言、小程序、应用程序。
  • JVM(Java Virtual Machine Java 虚拟机),JVM 能够了解为是一个虚构进去的计算机,具备着计算机的根本运算形式,它次要负责把 Java 程序生成的字节码文件,

    解释成具体零碎平台上的机器指令,让其在各个平台运行。

JDK 中蕴含 JRE,也包含 JDK,而 JRE 也包含 JDK。

范畴关系:JDK>JRE>JVM。

2、JVM 内存区域

Java 虚拟机在执行 Java 程序的过程中会把它所治理的内存划分为若干个不同的数据区域。依据《Java 虚拟机标准》的规定,Java 虚拟机所治理的内存将会包含以下几个运行时数据区域:

当然,实际上,为了更好的适应 CPU 性能晋升,最大限度晋升 JVM 运行效率,JDK 中各个版本对 JVM 进行了一些迭代,示意图如下:

JDK1.6、JDK1.7、JDK1.8 JVM 内存模型次要有以下差别:

  • JDK 1.6:有永恒代,动态变量寄存在永恒代上。
  • JDK 1.7:有永恒代,但曾经把字符串常量池、动态变量,寄存在堆上。逐步的缩小永恒代的应用。
  • JDK 1.8:无永恒代,运行时常量池、类常量池,都保留在元数据区,也就是常说的元空间。但字符串常量池依然寄存在堆上。

2.1、程序计数器

一块较小的内存空间, 是以后线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程公有”的内存。

正在执行 java 办法的话,计数器记录的是虚拟机字节码指令的地址(以后指令的地址)。如果还是 Native 办法,则为空。

这个内存区域是惟一一个在虚拟机中没有规定任何 OutOfMemoryError 状况的区域。

2.2、Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stack)也是线程公有的,它的生命周期与线程雷同。

虚拟机栈形容的是 Java 办法执行的线程内存模型:每个办法被执行的时候,Java 虚拟机都 会同步创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静连贯、办法进口等信息。每一个办法被调用直至执行结束的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表寄存了编译期可知的各种 Java 虚拟机根本数据类型、对象援用(reference 类型,它并不等同于对象自身,可能是一个指向对象起始址的援用指针,也可能是指向一个代表对象的句柄或者其余与此对象相干的地位)和 returnAddress 类型(指向了一条字节码指令的地址)。

2.3、本地办法栈

本地办法栈(Native Method Stacks)与虚拟机栈所施展的作用是十分类似的,其区别只是虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则是为虚拟机应用到的本地(Native)办法服务。

Hot-Spot 虚拟机间接把本地办法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地办法栈也会在栈深度溢出或者栈扩大失败时别离抛出 StackOverflowError 和 OutOfMemoryError 异样。

2.4、Java 堆

对于 Java 应用程序来说,Java 堆(Java Heap)是虚拟机所治理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,Java 世界里“简直”所有的对象实例都在这里分配内存。

Java 堆是垃圾收集器治理的内存区域,因而一些材料中它也被称作“GC 堆”。

从回收内存的角度看,

  • Java 堆,由年老代和年轻代组成,别离占据 1/3 和 2/3。
  • 而年老代又分为三局部,EdenFrom SurvivorTo Survivor,占据比例为 8:1:1,可调。

须要留神的是这些区域划分仅仅是一部分垃圾收集器的独特个性或者说设计格调而已,而非某个 Java 虚拟机具体实现的固有内存布局,HotSpot 外面曾经呈现了不采纳分代设计的新垃圾收集器。

Java 堆既能够被实现成固定大小的,也能够是可扩大的,不过以后支流的 Java 虚拟机都是依照可扩大来实现的(通过参数 -Xmx 和 -Xms 设定)。如果在 Java 堆中没有内存实现实例调配,并且堆也无奈再扩大时,Java 虚拟机将会抛出 OutOfMemoryError 异样。

2.5、办法区(JDK1.8 移除)

办法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、动态变量、即时编译器编译后的代码缓存等数据。

在 JDK1.8 以前,HotSpot 应用永恒代来实现办法区,所以某些场合也认为办法区和永恒代是一个概念。

在 JDK 6 的 时候 HotSpot 开发团队就有放弃永恒代,逐渐改为采纳本地内存(Native Memory)来实现办法区的打算了,到了 JDK 7 的 HotSpot,曾经把本来放在永恒代的字符串常量池、动态变量等移出,而到了 JDK 8,终于齐全废除了永恒代的概念,改用在本地内存中实现的元空间(Meta- space)来代替,把 JDK 7 中永恒代还残余的内容(次要是类型信息)全副移到元空间中。

如果办法区无奈满足新的内存调配需要时,将抛出 OutOfMemoryError 异样。

2.6、运行时常量池

运行时常量池(Runtime Constant Pool)是办法区的一部分——在 JDK1.8 曾经被移到了元空间。

运行时常量池绝对于 Class 文件常量池的一个重要特色是具备动态性,Java 语言并不要求常量肯定只有编译期能力产生,也就是说,并非预置入 Class 文件中常量池的内容能力进入运行时常量池,运行期间也能够将新的常量放入池中,这种个性被开发人员利用得比拟多的便是 String 类的 intern()办法。

2.7、间接内存

间接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。

显然,本机间接内存的调配不会受到 Java 堆大小的限度,然而,既然是内存,则必定还是会受到本机总内存(包含物理内存、SWAP 分区或者分页文件)大小以及处理器寻址空间的限度。

元空间 从虚拟机 Java 堆中转移到本地内存,默认状况下,元空间的大小仅受本地内存的限度。jdk1.8 以前版本的 class 和 JAR 包数据存储在 PermGen 上面,PermGen 大小是固定的,而且我的项目之间无奈共用,私有的 class,所以比拟容易呈现 OOM 异样。

降级 JDK 1.8 后,元空间配置参数,-XX:MetaspaceSize=512M XX:MaxMetaspaceSize=1024M。

3、JVM 中的对象

下面曾经理解 Java 虚拟机的运行时数据区域,咱们接下来更进一步理解这些虚拟机内存中数据的其余细节,譬如它们是如何创立、如何布局以及如何拜访的。以最罕用的虚拟机 HotSpot 和最罕用的内存区域 Java 堆为例,理解一下 HotSpot 虚拟机在 Java 堆中对象调配、布局和拜访的全过程。

3.1、对象的创立

Java 对象创立的大略过程如下:

  • 类加载查看:虚拟机遇到⼀条 new 指令时,⾸先将去查看这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且查看这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
  • 分配内存:在类加载查看通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载实现后便可确定,为对象调配空间的工作等同于把⼀块确定⼤⼩的内存从 Java 堆中划分进去。调配⽅式有 指针碰撞 闲暇列表 两种,抉择那种调配⽅式由 Java 堆是否规整决定,⽽Java 堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整顿性能决定。

内存调配的两种⽅式:抉择以上两种⽅式中的哪⼀种,取决于 Java 堆内存是否规整。⽽ Java 堆内存是否规整,取决于 GC 收集器的算法是 ” 标记 - 革除 ”,还是 ” 标记 - 整顿 ”(也称作 ” 标记 - 压缩 ”),值得注意的是,复制算法内存也是规整的。

  • 初始化零值:内存调配实现后,虚拟机须要将调配到的内存空间都初始化为零值(不包含对象头),这⼀步操作保障了对象的实例字段在 Java 代码中能够不赋初始值就间接使⽤,程序能拜访到这些字段的数据类型所对应的零值。
  • 设置对象头:初始化零值实现之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何能力找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。这些信息寄存在对象头中。另外,依据虚拟机以后运⾏状态的不同,如是否启⽤偏差锁等,对象头会有不同的设置⽅式。
  • 执⾏ init ⽅法:在上⾯⼯作都实现之后,从虚拟机的视⻆来看,⼀个新的对象曾经产⽣了,但从 Java 程序的视⻆来看,对象创立才刚开始,<init> ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ <init> ⽅法,把对象依照程序员的志愿进⾏初始化,这样⼀个真正可⽤的对象才算齐全产⽣进去。

3.2、对象的内存布局

在 HotSpot 虚拟机里,对象在堆内存中的存储布局能够划分为三个局部:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot 虚拟机对象的对象头局部包含两类信息。第一类是用于存储对象本身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标记、线程持有的锁、偏差线程 ID、偏差工夫戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中别离为 32 个比特和 64 个比特,官网称它为“Mark Word”。

3.3、对象的拜访定位

建⽴对象就是为了使⽤对象,咱们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的拜访⽅式有虚拟机实现⽽定,⽬前支流的拜访⽅式有 使⽤句柄和 间接指针两种:

  • 句柄:如果使⽤句柄的话,那么 Java 堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中蕴含了对象实例数据与类型数据各⾃的具体地址信息。

  • 间接指针:如果使⽤间接指针拜访,那么 Java 堆对象的布局中就必须思考如何搁置拜访类型数据的相干信息,⽽ reference 中存储的间接就是对象的地址。

4、GC 垃圾回收

对于垃圾回收,次要思考的就是实现三件事:

  • 哪些内存须要回收?
  • 什么时候回收?
  • 如何回收?

4.1、如何判断对象须要回收?

4.1.1、援用计数法

援用计数法的算法:

  • 在对象中增加一个援用计数器,每当有一个中央援用它时,计数器值就加一;
  • 当援用生效时,计数器值就减一;
  • 任何时刻计数器为零的对象就是不可 能再被应用的。

主观地说,援用计数算法(Reference Counting)尽管占用了一些额定的内存空间来进行计数,但它的原理简略,断定效率也很高,在大多数状况下它都是一个不错的算法。也有一些比拟驰名的利用案例,例如微软 COM(Component Object Model)技术、应用 ActionScript 3 的 FlashPlayer、Python 语言以及在游戏脚本畛域失去许多利用的 Squirrel 中都应用了援用计数算法进行内存治理。

然而,在 Java 畛域,至多支流的 Java 虚拟机外面都没有选用援用计数算法来治理内存,次要起因是,这个看似简略的算法有很多例外情况要思考,例如在解决解决一些相互依赖、循环援用时非常复杂。

4.1.2、可达性剖析算法

以后支流的商用程序语言(Java、C#,上溯至后面提到的古老的 Lisp)的内存管理子系统,都是通过可达性剖析(Reachability Analysis)算法来断定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,依据援用关系向下搜寻,搜寻过程所走过的门路称为“援用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何援用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证实此对象是不可能再被应用的。

GC Roots 包含;

  • 全局性援用,对办法区的动态对象、常量对象的援用
  • 执行上下文,对 Java 办法栈帧中的部分对象援用、对 JNI handles 对象援用
  • 已启动且未进行的 Java 线程

4.1.3、援用

无论是通过援用计数算法判断对象的援用数量,还是通过可达性剖析算法判断对象是否援用链可达,断定对象是否存活都和“援用”离不开关系。

Java 的援用分为四种:强援用(Strongly Re-ference)软援用(Soft Reference)弱援用(Weak Reference) 虚援用(Phantom Reference)

  • 强援用是最传统的“援用”的定义,是指在程序代码之中普遍存在的援用赋值,即相似“Object obj=new Object()”这种援用关系。无论任何状况下,只有强援用关系还存在,垃圾收集器就永远不会回收掉被援用的对象。
  • 软援用是用来形容一些还有用,但非必须的对象。只被软援用关联着的对象,在零碎将要产生内存溢出异样前,会把这些对象列进回收范畴之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异样。Java 提供提供了 SoftReference 类来实现软援用。
  • 弱援用也是用来形容那些非必须对象,然而它的强度比软援用更弱一些,被弱援用关联的对象只能生存到下一次垃圾收集产生为止。当垃圾收集器开始工作,无论以后内存是否足够,都会回收掉只被弱援用关联的对象。Java 提供了 WeakReference 类来实现弱援用。
  • 虚援用也称为“幽灵援用”或者“幻影援用”,它是最弱的一种援用关系。一个对象是否有虚援用的 存在,齐全不会对其生存工夫形成影响,也无奈通过虚援用来获得一个对象实例。为一个对象设置虚援用关联的惟一目标只是为了能在这个对象被收集器回收时收到一个零碎告诉。Java 提供了 PhantomReference 类来实现虚援用。

4.2、垃圾收集算法

4.2.1、标记 - 革除算法

最早呈现也是最根底的垃圾收集算法是“标记 - 革除”(Mark-Sweep)算法,

算法分为“标记 ”和“ 革除”两个阶段:首先标记出所有须要回收的对象,在标记实现后,对立回收掉所有被标记的对象,也能够反过来,标记存活的对象,对立回

收所有未被标记的对象。标记过程就是对象是否属于垃圾的断定过程。

后续的收集算法大多都是以标记 - 革除算法为根底,对其毛病进行改良而失去的。

它的次要毛病有两个:

  • 第一个是执行效率不稳固,如果 Java 堆中蕴含大量对象,而且其中大部分是须要被回收的,这时必须进行大量标记和革除的动作,导致标记和革除两个过

程的执行效率都随对象数量增长而升高;

  • 第二个是内存空间的碎片化问题,标记、革除之后会产生大量不间断的内存碎片,空间碎片太多可能会导致当当前在程序运行过程中须要调配较大对象时无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

标记 - 革除算法的执行过程如图:

4.2.2、标记 - 复制算法

标记 - 复制算法常被简称为复制算法。为了解决标记 - 革除算法面对大量可回收对象时执行效率低的问题。

它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块下面,而后再把已应用过的内存空间一次清理掉。

这样实现简略,运行高效,不过其缺点也不言而喻,这种复制回收算法的代价是将可用内存放大为了原来的一半,空间节约较多。

标记 - 复制算法的执行过程如图所示。

4.2.3、标记 - 整顿算法

标记 - 整顿算法的标记过程依然与“标记 - 革除”算法一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端挪动,而后间接清理掉边界以外的内存。

“标记 - 整顿”算法的示意图如图:

4.3、分代收集实践

以后商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的实践进行设计,分代收集名为实践,本质是一套合乎大多数程序运行理论状况的教训法令,它建设在两个分代假说之上:

  • 1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 2)强分代假说(Strong Generational Hypothesis):熬过越屡次垃圾收集过程的对象就越难以沦亡。

基于这两个假说,收集器应该将 Java 堆划分出不同的区域,而后将回收对象根据其年龄(年龄即对象熬过垃圾收集过程的次数)调配到不同的区域之中存储。

设计者个别至多会把 Java 堆划分为 新生代 (Young Generation)和 老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集

时都发现有少量对象死去,而每次回收后存活的大量对象,将会逐渐降职到老年代中寄存。

基于这种分代,老年代和新生代具备不同的特点,能够采纳不同的垃圾收集算法。

  • ⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以能够抉择 标记 - 复制 算法,只须要付出大量对象的复制老本就能够实现每次垃圾收集。
  • ⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额定的空间对它进⾏调配担保,所以必须抉择 标记 - 革除 标记 - 整顿 算法进⾏垃圾收集。

因为有了分代收集实践,所以就有了了“Minor GC(新⽣代 GC)”、“Major GC(⽼年代 GC)”、“Full GC(全局 GC)”这样的回收类型的划分

4.4、垃圾收集器

4.4.1、Serial 收集器

Serial 收集器是最根底、历史最悠久的收集器,已经(在 JDK 1.3.1 之前)是 HotSpot 虚拟机新生代收集器的惟一抉择。这个收集器是一个单线程工作的收集器,但它的“复线 程”的意义并不仅仅是阐明它只会应用一个处理器或一条收集线程去实现垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其余所有工作线程,直到它收集完结。

Serial/Serial Old 收 集器的运行过程如下:

4.4.2、ParNew 收集器

ParNew 收集器本质上是 Serial 收集器的多线程并行版本,除了同时应用多条线程进行垃圾收集之外,其余的行为包含 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象调配规定、回收策略等都与 Serial 收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

ParNew 收集器的工作过程如图所示:

4.4.3、Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记 - 复制算法实现的收集器,也是可能并行收集的多线程收集器

Parallel Scavenge 收集器的指标则是达到一个可管制的吞吐量(Throughput)。因为与吞吐量关系密切,Parallel Scavenge 收集器也常常被称作“吞吐量优先收集器”。

Parallel Scavenge 收集器提供了两个参数用于准确管制吞吐量,别离是管制最大垃圾收集进展工夫的 -XX:MaxGCPauseMillis 参数以及间接设置吞吐量大小的 -XX:GCTimeRatio 参数。

4.4.4、Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,应用标记 - 整顿算法。这个收集器的次要意义也是供客户端模式下的 HotSpot 虚拟机应用。如果在服务端模式下,它也可能有两种用处:一种是在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配应用,另外一种就是作为 CMS 收集器产生失败时的后备预案,在并发收集产生 Concurrent Mode Failure 时应用。这两点都将在前面的内容中持续解说。

Serial Old 收集器的工作过程如图所示。

4.4.5、Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,反对多线程并发收集,基于标记 - 整顿算法实现。这个收集器是直到 JDK 6 时才开始提供的。Parallel Old 收集器的工作过程如图所示。

4.4.6、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收进展工夫为指标的收集器。目前很大一部分的 Java 利用集中在互联网网站或者基于浏览器的 B / S 零碎的服务端上,这类利用通常都会较为关注服务的响应速度,心愿零碎进展工夫尽可能短,以给用户带来良好的交互体验。CMS 收集器就十分合乎这类利用的需要。

Concurrent Mark Sweep 收集器运行过程如图:

4.4.7、Garbage First 收集器

G1 是一款次要面向服务端利用的垃圾收集器,是目前垃圾回收器的前沿成绩。HotSpot 开发团队最后赋予它的冀望是(在比拟长期的)将来能够替换掉 JDK 5 中公布的 CMS 收集器。当初这个冀望指标曾经实现过半了,JDK 9 公布之日,G1 宣告取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的默认垃圾收集器。

G1 收集器运行过程如图:

5、JVM 类加载

Java 虚拟机把形容类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机间接应用的 Java 类型,这个过程被称作虚拟机的类加载机制。

5.1、类加载过程

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经验加载(Loading)、验证(Verification)、筹备(Preparation)、解析(Resolution)、初始化(Initialization)、应用(Using)和卸载(Unloading)七个阶段,其中验证、筹备、解析三个局部统称为连贯(Linking)。

过程如下图:

加载 :

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java 虚拟机须要实现以下三件事件:

  • 1)通过一个类的全限定名来获取定义此类的二进制字节流。
  • 2)将这个字节流所代表的动态存储构造转化为堆的运行时数据结构。
  • 3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访入口。

验证:

验证是连贯阶段的第一步,这一阶段的目标是确保 Class 文件的字节流中蕴含的信息合乎《Java 虚拟机标准》的全副束缚要求,保障这些信息被当作代码运行后不会危害虚拟机本身的平安。

验证阶段大抵上会实现四个阶段的测验动作:文件格式验证 元数据验证 字节码验证 符号援用验证

筹备:

筹备阶段是正式为类中定义的变量(即动态变量,被 static 润饰的变量)分配内存并设置类变量初始值的阶段。

解析

解析阶段是 Java 虚拟机将常量池内的符号援用替换为间接援用的过程。

5.2、类加载器

Java 虚拟机设计团队无意把类加载阶段中的“通过一个类的全限定名来获取形容该类的二进制字节流”这个动作放到 Java 虚拟机内部去实现,以便让应用程序本人决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

5.2.1、类与类加载器

类加载器尽管只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类自身一起独特确立其在 Java 虚拟机中的唯一性,每一个类加载器,都领有一个独立的类名称空间。这句话能够表白得更艰深一些:比拟两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即便这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只有加载它们的类加载器不同,那这两个类就必然不相等。

5.2.2、双亲委派模型

JVM 中内置了三个重要的 ClassLoader,启动类加载器(Bootstrap ClassLoader),这个类加载器应用 C ++ 语言实现,是虚拟机本身的一部分,其余所有

的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机内部,并且全都继承自抽象类 java.lang.ClassLoader。

  • 启动类加载器(Bootstrap Class Loader): 这个类加载器负责加载寄存在 <JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数所指定的门路中寄存的,而且是 Java 虚拟机可能辨认的(依照文件名辨认,如 rt.jar、tools.jar,名字不合乎的类库即便放在 lib 目录中也不会被加载)类库加载到虚拟机的内存中。
  • 扩大类加载器(Extension Class Loader):这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的模式实现的。它负责加载 <JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 零碎变量所指定的门路中所有的类库。
  • 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现。因为应用程序类加载器是 ClassLoader 类中的 getSystem-ClassLoader()办法的返回值,所以有些场合中也称它为“零碎类加载器”。它负责加载用户类门路(ClassPath)上所有的类库,开发者同样能够间接在代码中应用这个类加载器。

双亲委派模型: 如果一个类加载器收到了类加载的申请,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现,每一个档次的类加载器都是如此,因而所有的加载申请最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈本人无奈实现这个加载申请(它的搜寻范畴中没有找到所需的类)时,子加载器才会尝试本人去实现加载。

为什么要应用双亲委派模型来组织类加载器之间的关系呢?一个不言而喻的益处就是 Java 中的类随着它的类加载器一起具备了一种带有优先级的档次关系。例如类 java.lang.Object,它寄存在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因而 Object 类在程序的各种类加载器环境中都可能保障是同一个类。反之,如果没有应用双亲委派模型,都由各个类加载器自行去加载的话,如果用户本人也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那零碎中就会呈现多个不同的 Object 类,Java 类型体系中最根底的行为也就无从保障,应用程序将会变得一片凌乱。

5.2.3、毁坏双亲委派模型

过双亲委派模型并不是一个具备强制性束缚的模型,而是 Java 设计者举荐给开发者们的类加载器实现形式。在 Java 的世界中大部分的类加载器都遵循这个模型,但也有例外的状况,直到 Java 模块化呈现为止,双亲委派模型次要呈现过 3 次较大规模“被毁坏”的状况。

  • 第一次毁坏:向前兼容

    JDK1.2 公布之前,兼容之前的代码。

  • 第二次毁坏:加载 SPI 接口实现类

第二次被毁坏是这个模型本身的缺点导致的。双亲委派模型很好的解决了各个类加载器的根底类的对立问题(越根底的类由越下层的加载器进行加载),根底类之所以称为“根底”,是因为它们总是作为被用户代码调用的 API,但没有相对,如果根底类调用会用户的代码怎么办呢?

这不是没有可能的。一个典型的例子就是 JNDI 服务,JNDI 当初曾经是 Java 的规范服务,它的代码由启动类加载器去加载(在 JDK1.3 时就放进去的 rt.jar), 但它须要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“意识“这些代码啊。因为这些类不在 rt.jar 中,然而启动类加载器又须要加载。怎么办呢?

为了解决这个问题,Java 设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器能够通过 java.lang.Thread 类的 setContextClassLoader 办法进行设置。如果创立线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范畴内都没有设置过多的话,那这个类加载器默认即便应用程序类加载器。

有了线程上下文加载器,JNDI 服务应用这个线程上下文加载器去加载所须要的 SPI 代码,也就是父类加载器申请子类加载器去实现类加载的动作,这种行为实际上就是买通了双亲委派模型的层次结构来逆向应用类加载器,实际上曾经违反了双亲委派模型的一般性准则。但这无可奈何,Java 中所有波及 SPI 的加载动作根本胜都采纳这种形式。例如 JNDI,JDBC,JCE,JAXB,JBI 等。

  • 第三次毁坏:热部署

双亲委派模型的第三次“被毁坏”是因为用户对程序的动态性的谋求导致的。为了实现热插拔,热部署,模块化,意思是增加一个性能或减去一个性能不必重启,只须要把这模块连同类加载器一起换掉就实现了代码的热替换。例如 OSGi 的呈现。在 OSGi 环境下,类加载器不再是双亲委派模型中的树状构造,而是进一步倒退为网状结构。

如果咱们本人想定义一个类加载器,毁坏双亲委派模型,只须要重写重写其中的 loadClass 办法,使其不进行双亲委派即可。

5.2.4、Tomcat 类加载器架构

Tomcat 是支流的 Java Web 服务器之一,为了实现一些非凡的性能需要,自定义了一些类加载器。

Tomcat 类加载器如下:

Tomcat 实际上也是毁坏了双亲委派模型的。

Tomact 是 web 容器,可能须要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,然而不同版本的类库中某一个类的全路径名可能是一样的。如多个利用都要依赖 hollis.jar,然而 A 利用须要依赖 1.0.0 版本,然而 B 利用须要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class。如果采纳默认的双亲委派类加载机制,那么无奈加载多个雷同的类。

所以,Tomcat 毁坏双亲委派准则,提供隔离的机制,为每个 web 容器独自提供一个 WebAppClassLoader 加载器。

Tomcat 的类加载机制:为了实现隔离性,优先加载 Web 利用本人定义的类,所以没有遵循双亲委派的约定,每一个利用本人的类加载器——WebAppClassLoader 负责加载自身的目录下的 class 文件,加载不到时再交给 CommonClassLoader 加载,这和双亲委派刚好相同。

6、JVM 故障解决

6.1、根底故障解决工具

6.1.1、jps:虚拟机过程情况工具

jps(JVM Process Status Tool),它的性能与 ps 命令相似,能够列出正在运行的虚拟机过程,并显示虚拟机执行主类(Main Class,main()函数所在的类)

名称以及这些过程的本地虚拟机惟一 ID(Local Virtual Machine Identifier,LVMID),相似于 ps -ef | grep java 的性能。

命令格局

jps [options] [hostid]

 options:选项、参数,不同的参数能够输入须要的信息

 hostid:近程查看

选项列表 形容
-q 只输入过程 ID,疏忽主类信息
-l 输入主类全名,或者执行 JAR 包则输入门路
-m 输入虚拟机过程启动时传递给主类 main()函数的参数
-v 输入虚拟机过程启动时的 JVM 参数

6.1.2、jstat:虚拟机统计信息监督工具

jstat(JVM Statistics Monitoring Tool),用于监督虚拟机各种运行状态信息。它能够查看本地或者近程虚拟机过程中,类加载、内存、垃圾收集、即时编译等运行时数据。

命令格局

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

  • vmid:如果是查看近程机器,须要依照此格局:

    [protocol:][//]lvmid[@hostname[:port]/servername]

  • interval 和 count,示意查问距离和次数,比方每隔 1000 毫秒查问一次过程 ID 的 gc 收集状况,每次查问 5 次。jstat -gc 111552 1000 5

选项列表:

选项列表 形容
-class 监督类加载、卸载数量、总空间以及类装载所消耗时长
-gc 监督 Java 堆状况,包含 Eden 区、2 个 Survivor 区、老年代、永恒代或者 jdk1.8 元空间等,容量、已用空间、垃圾收集工夫共计等信息
-gccapacity 监督内容与 -gc 基本一致,但输入次要关注 Java 堆各个区域应用到的最大、最小空间
-gcutil 监督内容与 -gc 基本相同,但输入次要关注已应用空间占总空间的百分比
-gccause 与 -gcutil 性能一样,然而会额定输入导致上一次垃圾收集产生的起因
-gcnew 监督新生代垃圾收集状况
-gcnewcapacity 监督内容与 -gcnew 基本相同,输入次要关注应用到的最大、最小空间
-gcold 监督老年代垃圾收集状况
-gcoldcapacity 监督内容与 -gcold 基本相同,输入次要关注应用到的最大、最小空间
-compiler 输入即时编译器编译过的办法、耗时等信息
-printcompilation 输入曾经被即时编译的办法

6.1.3、jinfo:Java 配置信息工具

jinfo(Configuration Info for Java),实时查看和调整 JVM 的各项参数。在下面讲到 jps -v 指令时,能够看到它把虚拟机启动时显式的参数列表都打印

进去了,但如果想更加清晰的看具体的一个参数或者想晓得未被显式指定的参数时,就能够通过 jinfo -flag 来查问了。

命令格局

jinfo [option] pid

6.1.4、jmap:Java 内存映像工具

jmap(Memory Map for Java),用于生成堆转储快照(heapdump 文件)。

jmap 的作用除了获取堆转储快照,还能够查问 finalize 执行队列、Java 堆和

办法区的详细信息。

命令格局

jmap [option] pid

 option:选项参数

 pid:须要打印配置信息的过程 ID

 executable:产生外围 dump 的 Java 可执行文件

 core:须要打印配置信息的外围文件

 server-id:可选的惟一 id,如果雷同的近程主机上运行了多台调试服务器,用此选

项参数标识服务器

 remote server IP or hostname:近程调试服务器的 IP 地址或主机名

选项 形容
-dump 生成 Java 堆转储快照。
-finalizerinfo 显示在 F-Queue 中期待 Finalizer 线程执行 finalize 办法的对象。Linux 平台
-heap 显示 Java 堆详细信息,比方:用了哪种回收器、参数配置、分代状况。Linux 平台
-histo 显示堆中对象统计信息,包含类、实例数量、共计容量
-permstat 显示永恒代内存状态,jdk1.7,永恒代
-F 当虚拟机过程对 -dump 选项没有响应式,能够强制生成快照。Linux 平台

6.1.5、jhat:虚拟机堆转储快照剖析工具

jhat(JVM Heap Analysis Tool),与 jmap 配合应用,用于剖析 jmap 生成的堆转储快照。

jhat 内置了一个小型的 http/web 服务器,能够把堆转储快照剖析的后果,展现在浏览器中查看。不过用处不大,根本大家都会应用其余第三方工具。

命令格局

jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-

debug <int>] [-version] [-h|-help] <file>

6.1.6、jstack:Java 堆栈跟踪工具

jstack(Stack Trace for Java),用于生成虚拟机以后时刻的线程快照(threaddump、javacore)。

线程快照就是以后虚拟机内每一条线程正在执行的办法堆栈的汇合,生成线程快照的目标通常是定位线程呈现长时间进展的起因,如:线程死锁、死循环、申请

内部资源耗时较长导致挂起等。

线程呈现听登时通过 jstack 来查看各个线程的调用堆栈,就能够取得没有响应的线程在搞什么鬼。

命令格局

jstack [option] vmid

选项参数:

选项 形容
-F 当失常输入的申请不被响应时,强制输入线程堆栈
-l 除了堆栈外,显示对于锁的附加信息
-m 如果调用的是本地办法的话,能够显示 c/c++ 的堆栈

6.2、可视化故障解决工具

JDK 中除了附带大量的命令行工具外,还提供了几个性能集成度更高的可视化工具,用户能够应用这些可视化工具以更加便捷的形式进行过程故障诊断和调试工作。这类工具次要包含 JConsole、JHSDB、VisualVM 和 JMC 四个。

6.2.1、JHSDB:基于服务性代理的调试工具

JDK 中提供了 JCMD 和 JHSDB 两个集成式的多功能工具箱,它们不仅整合了所有 根底工具所能提供的专项性能,而且因为有着“后发优势”,可能做得往往比之前的老工具们更好、更弱小。

JHSDB 是一款基于服务性代理(Serviceability Agent,SA)实现的过程外调试工具。

应用以下命令进入 JHSDB 的图形化模式,并使其附加过程 11180:

jhsdb hsdb --pid 11180

命令关上的 JHSDB 的界面:

6.2.2、JConsole:Java 监督与治理控制台

JConsole(Java Monitoring and Management Console)是一款基于 JMX(Java Manage-ment Extensions)的可视化监督、管理工具。它的次要性能是通过 JMX 的 MBean(Managed Bean)对系统进 行信息收集和参数动静调整。

JConsole 连贯页面 :

通过 JDK/bin 目录下的 jconsole.exe 启动 JCon-sole 后,会主动搜寻出本机运行的所有虚拟机过程

6.2.3、VisualVM:多合 - 故障解决工具

VisualVM(All-in-One Java Troubleshooting Tool)是性能最弱小的运行监督和故障处理程序之一,已经在很长一段时间内是 Oracle 官网主力倒退的虚拟机故障解决工具。

它除了惯例的运行监督、故障解决外,还能够做性能剖析等工作。因为它的通用性很强,对应用程序影响较小,所以能够间接接入到生产环境中。

VisualVM 的插件能够手工进行装置,在网站上下载 nbm 包后,点击“工具 -> 插件 -> 已下载”菜单,而后在弹出对话框中指定 nbm 包门路便可实现装置。

VisualVM 插件页签:

6.2.4、Java Mission Control:可继续在线的监控工具

JMC 最后是 BEA 公司的产品,因而并没有像 VisualVM 那样一开始就基于自家的 Net-Beans 平台来开发,而是抉择了由 IBM 捐献的 Eclipse RCP 作为根底框架,当初的 JMC 不仅能够下载到独立程序,更常见的是作为 Eclipse 的插件来应用。JMC 与虚拟机之间同样采取 JMX 协定进行通信,JMC 一方面作为 JMX 控制台,显示来自虚拟机 MBean 提供的数据;另一方面作为 JFR 的剖析工具,展现来自 JFR 的数据。

JMC 的主界面如图:

本文是作者联合一些常见面试题学习周志朋老师《深刻了解 Java 虚拟机:JVM 高级个性与最佳实际》的整顿。这本书是十分经典的 JVM 书籍,也是一部七百多页的大部头,强烈建议有空认真研读这本书籍,来学习更多 JVM 的个性和细节。

<big>参考:</big>

【1】:周志朋编著《深刻了解 Java 虚拟机:JVM 高级个性与最佳实际(第 3 版》

【2】:JavaGuide 搞定大厂 jvm 面试

【3】:小傅哥编著《Java 面经手册》

【4】:Java 内存治理 -JVM 内存模型以及 JDK7 和 JDK8 内存模型比照总结(三)

正文完
 0