虚拟机栈 : Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。
方法在实行过程中,会在虚拟机栈中创建一个 栈帧(stack frame)。
每个方法实行的过程就对应了一个入栈和出栈的过程。

本地方法栈: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域紧张是 Java 中利用 native 关键字润色的方法所存储的区域。
程序计数器:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、非常、线程切换和规复等功能,这些都通过程序计数器来完成。
方法区:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆:堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的工具实例都会分配在堆上。
JDK 1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
堆空间的内存分配(默认情形下):老年代 : 三分之二的堆空间年轻代 : 三分之一的堆空间eden 区: 8/10 的年轻代空间survivor 0 : 1/10 的年轻代空间survivor 1 : 1/10 的年轻代空间命令行上实行如下命令,会查看默认的 JVM 参数。
java -XX:+PrintFlagsFinal -version 输出的内容非常多,但是只有两行能够反响出上面的内存分配结果运行时常量池:运行时常量池又被称为 Runtime Constant Pool,这块区域是方法区的一部分,它的名字非常故意思,常日被称为 非堆。
它并不哀求常量一定只有在编译期才能产生,也便是并非编译期间将常量放在常量池中,运行期间也可以将新的常量放入常量池中,String 的 intern 方法便是一个范例的例子。
请你描述一下 Java 中的类加载机制?

Java 虚拟机卖力把描述类的数据从 Class 文件加载到系统内存中,并对类的数据进行校验、转换解析和初始化,终极形成可以被虚拟机直策应用的 Java 类型,这个过程被称之为 Java 的类加载机制。

一个类从被加载到虚拟机内存开始,到卸载出内存为止,一共会经历下面这些过程。

osgijsp报错JVM 面试题支配上了 Webpack

类加载机制一共有五个步骤,分别是加载、链接、初始化、利用和卸载阶段,这五个阶段的顺序是确定的。

个中链接阶段会细分成三个阶段,分别是验证、准备、解析阶段,这三个阶段的顺序是不愿定的,这三个阶段常日交互进行。
解析阶段常日会在初始化之后再开始,这是为了支持 Java 措辞的运行时绑定特性(也被称为动态绑定)。

下面我们就来聊一下这几个过程。

加载

关于什么时候开始加载这个过程,《Java 虚拟机规范》并没有逼迫约束,以是这一点我们可以自由实现。
加载是全体类加载过程的第一个阶段,在这个阶段,Java 虚拟机须要完成三件事情:

通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流表示的一种存储构造转换为运行时数据区中方法区的数据构造。
在内存中天生一个 Class 工具,这个工具就代表了这个数据构造的访问入口。

《Java 虚拟机规范》并未规定全限定名是如何获取的,以是现在业界有很多获取全限定名的办法:

从 ZIP 包中读取,终极会改变为 JAR、EAR、WAR 格式。
从网络中获取,最常见的运用便是 Web Applet。
运行时动态天生,利用最多的便是动态代理技能。
由其他文件天生,比如 JSP 运用处景,由 JSP 文件天生对应的 Class 文件。
从数据库中读取,这种场景就比较小了。
可以从加密文件中获取,这是范例的防止 Class 文件被反编译的保护方法。

加载阶段既可以利用虚拟机内置的勾引类加载器来完成,也可以利用用户自定义的类加载器来完成。
程序员可以通过自己定义类加载器来掌握字节流的访问办法。

数组的加载不须要通过类加载器来创建,它是直接在内存等分配,但是数组的元素类型(数组去掉所有维度的类型)终极还是要靠类加载器来完成加载。

验证

加载过后的下一个阶段便是验证,由于我们上一步讲到在内存中天生了一个 Class 工具,这个工具是访问其代表数据构造的入口,以是这一步验证的事情便是确保 Class 文件的字节流中的内容符合《Java 虚拟机规范》中的哀求,担保这些信息被当作代码运行后,它不会威胁到虚拟机的安全。

验证阶段紧张分为四个阶段的考验:

文件格式验证。
元数据验证。
字节码验证。
符号引用验证。

文件格式验证

这一阶段可能会包含下面这些验证点:

魔数是否以 0xCAFEBABE 开头。
主、次版本号是否在当前 Java 虚拟机接管范围之内。
常亮池的常量中是否有不支持的常量类型。
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据。
Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。

实际上验证点远远不止有这些,上面这些只是从 HotSpot 源码中摘抄的一小段内容。

元数据验证

这一阶段紧张是对字节码描述的信息进行语义剖析,以确保描述的信息符合《Java 措辞规范》,验证点包括

验证的类是否有父类(除了 Object 类之外,所有的类都该当有父类)。
要验证类的父类是否继续了不许可继续的类。
如果这个类不是抽象类,那么这个类是否实现了父类或者接口中哀求的所有方法。
是否覆盖了 final 字段,是否涌现了不符合规定的重载等。

须要记住这一阶段只是对《Java 措辞规范》的验证。

字节码验证

字节码验证阶段是最繁芜的一个阶段,这个阶段紧张是确定程序语意是否合法、是否是符合逻辑的。
这个阶段紧张是对类的方法体(Class 文件中的 Code 属性)进行校验剖析。
这部分验证包括

确保操作数栈的数据类型和实际实行时的数据类型是否同等。
担保任何跳转指令不会跳出到方法体外的字节码指令上。
担保方法体中的类型转换是有效的,例如可以把一个子类工具赋值给父类数据类型,但是不能把父类数据类型赋值给子类等诸如此不屈安的类型转换。
其他验证。

如果没有通过字节码验证,就解释验证出问题。
但是不一定通过了字节码验证,就能担保程序是安全的。

符号引用验证

末了一个阶段的校验行为发生在虚拟机将符号引用转换为直接引用的时候,这个转化将在连接的第三个阶段,即解析阶段中发生。
符号引用验证可以看作是对类自身以外的各种信息进行匹配性校验,这个验证紧张包括

符号引用中的字符串全限定名是否能找到对应的类。
指定类中是否存在符合方法的字段描述符以及大略名称所描述的方法和字段。
符号引用的类、字段方法的可访问性是否可被当前类所访问。
其他验证。

这一阶段紧张是确保解析行为能否正常实行,如果无法通过符号引用验证,就会涌现类似 IllegalAccessError、NoSuchFieldError、NoSuchMethodError 等缺点。

验证阶段对付虚拟机来说非常主要,如果能通过验证,就解释你的程序在运行时不会产生任何影响。

准备

准备阶段是为类中的变量分配内存并设置其初始值的阶段,这些变量所利用的内存都应该在方法区中进行分配,在 JDK 7 之前,HotSpot 利用永久代来实现方法区,是符合这种逻辑观点的。
而在 JDK 8 之后,变量则会随着 Class 工具一起存放在 Java 堆中。

下面常日情形下的基本类型和引用类型的初始值

除了"常日情形"下,还有一些"例外情形",如果类字段属性中存在 ConstantValue 属性,那就这个变量值在初始阶段就会初始化为 ConstantValue 属性所指定的初始值,比如

public static final int value = "666";

编译时就会把 value 的值设置为 666。

解析

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

符号引用:符号引用以一组符号来描述所引用的目标。
符号引用可以是任何形式的字面量,只要利用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。
直接引用:直接引用可以直接指向目标的指针、相对便宜量或者一个能间接定位到目标的句柄。
直接引用和虚拟机的布局是干系的,不同的虚拟机对付相同的符号引用所翻译出来的直接引用一样平常是不同的。
如果有了直接引用,那么直接引用的目标一定被加载到了内存中。

这样说你可能还有点不明白,我再换一种说法:

在编译的时候一个每个 Java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址,以是就用符号引用来代替,而在这个解析阶段便是为了把这个符号引用转化成为真正的地址的阶段。

《Java 虚拟机规范》并未规定解析阶段发生的韶光,只哀求了在 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield 和 putstatic 这 17 个用于操作符号引用的字节码指令之前,先对所利用的符号引用进行解析。

解析也分为四个步骤

类或接口的解析字段解析方法解析接口方法解析初始化

初始化是类加载过程的末了一个步骤,在之前的阶段中,都是由 Java 虚拟机占主导浸染,但是到了这一步,却把主动权移交给运用程序。

对付初始化阶段,《Java 虚拟机规范》严格规定了只有下面这六种情形下才会触发类的初始化。

在碰着 new、getstatic、putstatic 或者 invokestatic 这四条字节码指令时,如果没有进行过初始化,那么首先触发初始化。
通过这四个字节码的名称可以判断,这四条字节码实在就两个场景,调用 new 关键字的时候进行初始化、读取或者设置一个静态字段的时候、调用静态方法的时候。
在初始化类的时候,如果父类还没有初始化,那么就须要先对父类进行初始化。
在利用 java.lang.reflect 包的方法进行反射调用的时候。
当虚拟机启动时,用户须要指定实行主类的时候,说白了便是虚拟机会先初始化 main 方法这个类。
在利用 JDK 7 新加入的动态措辞支持时,如果一个 jafva.lang.invoke.MethodHandle 实例末了的解析结果为 REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,须要先对其进行初始化。
当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字润色的接口方法)时,如果有这个借口的实现类发生了初始化,那该接口要在其之前被初始化。

实在上面只有前四个大家须要知道就好了,后面两个比较冷门。

如果说要答类加载的话,实在聊到这里已经可以了,但是为了完全性,我们索性把后面两个过程也来聊一聊。

利用

这个阶段没什么可说的,便是初始化之后的代码由 JVM 来动态调用实行。

卸载

当代表一个类的 Class 工具不再被引用,那么 Class 工具的生命周期就结束了,对应的在方法区中的数据也会被卸载。

⚠️但是须要把稳一点:JVM 自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的。

在 JVM 中,工具是如何创建的?

如果要回答工具是怎么创建的,我们一样平常想到的回答是直接 new 出来就行了,这个回答不仅局限于编程中,也融入在我们生活中的方方面面。

但是碰着口试的时候你只回答一个"new 出来就行了"显然是弗成的,由于口试更趋向于让你阐明当程序实行到 new 这条指令时,它的背后发生了什么。

以是你须要从 JVM 的角度来阐明这件事情。

当虚拟机遇到一个 new 指令时(实在便是字节码),首先会去检讨这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检讨这个符号引用所代表的类是否已经被加载、解析和初始化。

由于此时很可能不知道详细的类是什么,以是这里利用的是符号引用。

如果创造这个类没有经由上面类加载的过程,那么就实行相应的类加载过程。

类检讨完成后,接下来虚拟机将会为新生工具分配内存,工具所需的大小在类加载完成后便可确定(我会不才面的口试题中先容)。

分配内存相称于是把一块固定的内存块从堆中划分出来。
划分出来之后,虚拟机会将分配到的内存空间都初始化为零值,如果利用了 TLAB(本地线程分配缓冲),这一项初始化事情可以提前在 TLAB 分配时进行。
这一步操作担保了工具实例字段在 Java 代码中可以不赋值就能直策应用。

接下来,Java 虚拟机还会对工具进行必要的设置,比如确定工具是哪个类的实例、工具的 hashcode、工具的 gc 分代年事信息。
这些信息存放在工具的工具头(Object Header)中。

如果上面的事情都做完后,从虚拟机的角度来说,一个新的工具就创建完毕了;但是对付程序员来说,工具创建才刚刚开始,由于布局函数,即 Class 文件中的 <init>() 方法还没有实行,所有字段都为默认的零值。
new 指令之后才会实行 <init>() 方法,然后按照程序员的意愿对工具进行初始化,这样一个工具才可能被完全的布局出来。

内存分配办法有哪些呢?

在类加载完成后,虚拟机须要为新生工具分配内存,为工具分配内存相称于是把一块确定的区域从堆中划分出来,这就涉及到一个问题,要划分的堆区是否规整。

假设 Java 堆中内存是规整的,所有利用过的内存放在一边,未利用的内存放在一边,中间放着一个指针,这个指针为分界指示器。
那么为新工具分配内存空间就相称于是把指针向空闲的空间挪动工具大小相等的间隔,这种内存分配办法叫做指针碰撞(Bump The Pointer)。

如果 Java 堆中的内存并不是规整的,已经被利用的内存和未被利用的内存相互交错在一起,这种情形下就没有办法利用指针碰撞,这里就要利用其余一种记录内存利用的办法:空闲列表(Free List),空闲列表掩护了一个列表,这个列表记录了哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给工具实例,并更新列表上的记录。

以是,上述两种分配办法选择哪个,取决于 Java 堆是否规整来决定。
在一些垃圾网络器的实现中,Serial、ParNew 等带压缩整理过程的网络器,利用的是指针碰撞;而利用 CMS 这种基于打消算法的网络器时,利用的是空闲列表,详细的垃圾网络器我们后面会聊到。

请你说一下工具的内存布局?

在 hotspot 虚拟机中,工具在内存中的布局分为三块区域:

工具头(Header)实例数据(Instance Data)对齐添补(Padding)

这三块区域的内存分布如下图所示

我们来详细先容一下上面对象中的内容。

工具头 Header

工具头 Header 紧张包含 MarkWord 和工具指针 Klass Pointer,如果是数组的话,还要包含数组的长度。

在 32 位的虚拟机中 MarkWord ,Klass Pointer 和数组长度分别占用 32 位,也便是 4 字节。

如果是 64 位虚拟机的话,MarkWord ,Klass Pointer 和数组长度分别占用 64 位,也便是 8 字节。

在 32 位虚拟机和 64 位虚拟机的 Mark Word 所占用的字节大小不一样,32 位虚拟机的 Mark Word 和 Klass Pointer 分别占用 32 bits 的字节,而 64 位虚拟机的 Mark Word 和 Klass Pointer 占用了64 bits 的字节,下面我们以 32 位虚拟机为例,来看一下其 Mark Word 的字节详细是如何分配的。

用中文翻译过来便是

无状态也便是无锁的时候,工具头开辟 25 bit 的空间用来存储工具的 hashcode ,4 bit 用于存放分代年事,1 bit 用来存放是否倾向锁的标识位,2 bit 用来存放锁标识位为 01。
倾向锁 中划分更细,还是开辟 25 bit 的空间,个中 23 bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年事,1 bit 存放是否倾向锁标识, 0 表示无锁,1 表示倾向锁,锁的标识位还是 01。
轻量级锁中直接开辟 30 bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为 00。
重量级锁中和轻量级锁一样,30 bit 的空间用来存放指向重量级锁的指针,2 bit 存放锁的标识位,为 11GC标记开辟 30 bit 的内存空间却没有占用,2 bit 空间存放锁标志位为 11。

个中无锁和倾向锁的锁标志位都是 01,只是在前面的 1 bit 区分了这是无锁状态还是倾向锁状态。

关于为什么这么分配的内存,我们可以从 OpenJDK 中的markOop.hpp类中的列举窥出端倪

来阐明一下

age_bits 便是我们说的分代回收的标识,占用4字节lock_bits 是锁的标志位,占用2个字节biased_lock_bits 是是否倾向锁的标识,占用1个字节。
max_hash_bits 是针对无锁打算的 hashcode 占用字节数量,如果是 32 位虚拟机,便是 32 - 4 - 2 -1 = 25 byte,如果是 64 位虚拟机,64 - 4 - 2 - 1 = 57 byte,但是会有 25 字节未利用,以是 64 位的 hashcode 占用 31 byte。
hash_bits 是针对 64 位虚拟机来说,如果最大字节数大于 31,则取 31,否则取真实的字节数cms_bits 我以为该当是不是 64 位虚拟机就占用 0 byte,是 64 位就占用 1byteepoch_bits 便是 epoch 所占用的字节大小,2 字节。

在上面的虚拟机工具头分配表中,我们可以看到有几种锁的状态:无锁(无状态),倾向锁,轻量级锁,重量级锁,个中轻量级锁和倾向锁是 JDK1.6 中对 synchronized 锁进行优化后新增加的,其目的便是为了大大优化锁的性能,以是在 JDK 1.6 中,利用 synchronized 的开销也没那么大了。
实在从锁有无锁定来讲,还是只有无锁和重量级锁,倾向锁和轻量级锁的涌现便是增加了锁的获取性能而已,并没有涌现新的锁。

以是我们的重点放在对 synchronized 重量级锁的研究上,当 monitor 被某个线程持有后,它就会处于锁定状态。
在 HotSpot 虚拟机中,monitor 的底层代码是由 ObjectMonitor 实现的,其紧张数据构造如下(位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件,C++ 实现的)

这段 C++ 中须要把稳几个属性:_WaitSet 、 _EntryList 和 _Owner,每个等待获取锁的线程都会被封装称为 ObjectWaiter 工具。

_Owner 是指向了 ObjectMonitor 工具的线程,而 _WaitSet 和 _EntryList 便是用来保存每个线程的列表。

那么这两个列表有什么差异呢?这个问题我和你聊一下锁的获取流程你就清楚了。

锁的两个列表

当多个线程同时访问某段同步代码时,首先会进入 _EntryList 凑集,当线程获取到工具的 monitor 之后,就会进入 _Owner 区域,并把 ObjectMonitor 工具的 _Owner 指向为当前哨程,并使 _count + 1,如果调用了开释锁(比如 wait)的操作,就会开释当前持有的 monitor ,owner = null, _count - 1,同时这个线程会进入到 _WaitSet 列表中等待被唤醒。
如果当前哨程实行完毕后也会开释 monitor 锁,只不过此时不会进入 _WaitSet 列表了,而是直接复位 _count 的值。

Klass Pointer 表示的是类型指针,也便是工具指向它的类元数据的指针,虚拟机通过这个指针来确定这个工具是哪个类的实例。

你可能不是很理解指针是个什么观点,你可以大略理解为指针便是指向某个数据的地址。

实例数据 Instance Data

实例数据部分是工具真正存储的有效信息,也是代码中定义的各个字段的字节大小,比如一个 byte 占 1 个字节,一个 int 占用 4 个字节。

对齐 Padding

对齐不是必须存在的,它只起到了占位符(%d, %c 等)的浸染。
这便是 JVM 的哀求了,由于 HotSpot JVM 哀求工具的起始地址必须是 8 字节的整数倍,也便是说工具的字节大小是 8 的整数倍,不足的须要利用 Padding 补全。

工具访问定位的办法有哪些?

我们创建一个工具的目的当然便是为了利用它,但是,一个工具被创建出来之后,在 JVM 中是如何访问这个工具的呢?一样平常有两种办法:通过句柄访问和 通过直接指针访问。

如果利用句柄访问办法的话,Java 堆中可能会划分出一块内存作为句柄池,引用(reference)中存储的是工具的句柄地址,而句柄中包含了工具的实例数据与类型数据各自详细的地址信息。
如下图所示。
如果利用直接指针访问的话,Java 堆中工具的内存布局就会有所差异,栈区引用指示的是堆中的实例数据的地址,如果只是访问工具本身的话,就不会多一次直接访问的开销,而工具类型数据的指针是存在于方法区中,如果定位的话,须要多一次直接定位开销。
如下图所示

这两种工具访问办法各有各的上风,利用句柄最大的好处便是引用中存储的是句柄地址,工具移动时只需改变句柄的地址就可以,而无需改变工具本身。

利用直接指针来访问速率更快,它节省了一次指针定位的韶光开销,由于工具访问在 Java 中非常频繁,由于这类的开销也是值得优化的地方。

上面聊到了工具的两种数据,一种是工具的实例数据,这没什么好说的,便是工具实例字段的数据,一种是工具的类型数据,这个数听说的是工具的类型、父类、实现的接口和方法等。

如何判断工具已经去世亡?

我们大家知道,基本上所有的工具都在堆等分布,当我们不再利用工具的时候,垃圾网络器会对无用工具进行回收♻️,那么 JVM 是如何判断哪些工具已经是"无用工具"的呢?

这里有两种判断办法,首先我们先来说第一种:引用计数法。

引用计数法的判断标准是这样的:在工具中添加一个引用计数器,每当有一个地方引用它时,计数器的值就会加一;当引用失落效时,计数器的值就会减一;只要任何时候计数器为零的工具便是不会再被利用的工具。
虽然这种判断办法非常大略粗暴,但是每每很有用,不过,在 Java 领域,主流的 Hotspot 虚拟机实现并没有采取这种办法,由于引用计数法不能办理工具之间的循环引用问题。

循环引用问题大略来讲便是两个工具之间相互依赖着对方,除此之外,再无其他引用,这样虚拟机无法判断引用是否为零从而进行垃圾回收操作。

还有一种判断工具无用的方法便是可达性剖析算法。

当前主流的 JVM 都采取了可达性剖析算法来进行判断,这个算法的基本思路便是通过一系列被称为GC Roots的根工具作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径被称为引用链(Reference Chain),如果某个工具到 GC Roots 之间没有任何引用链相连接,或者说从 GC Roots 到这个工具不可达时,则证明此这个工具是无用工具,须要被垃圾回收。

这种引用办法如下

如上图所示,从列举根节点 GC Roots 开始进行遍历,object 1 、2、3、4 是存在引用关系的工具,而 object 5、6、7 之间虽然有关联,但是它们到 GC Roots 之间是不可大的,以是被认为是可以回收的工具。

在 Java 技能体系中,可以作为 GC Roots 进行检索的工具紧张有

在虚拟机栈(栈帧中确当地变量表)中引用的工具。
方法区中类静态属性引用的工具,比如 Java 类的引用类型静态变量。
方法区中常量引用的工具,比如字符串常量池中的引用。
在本地方法栈中 JNI 引用的工具。
JVM 内部的引用,比如基本数据类型对应的 Class 工具,一些非常工具比如 NullPointerException、OutOfMemoryError 等,还有系统类加载器。
所有被 synchronized 持有的工具。
还有一些 JVM 内部的比如 JMXBean、JVMTI 中注册的回调,本地代码缓存等。
根据用户所选的垃圾网络器以及当前回收的内存区域的不同,还可能会有一些工具临时加入,共同构成 GC Roots 凑集。

虽然我们上面提到了两种判断工具回收的方法,但无论是引用计数法还是判断 GC Roots 都离不开引用这一层关系。

这里涉及到到强引用、软引用、弱引用、虚引用的引用关系,你可以阅读作者的这一篇文章

小心点,别被当成垃圾回收了。

如何判断一个不再利用的类?

判断一个类型属于"不再利用的类"须要知足下面这三个条件

这个类所有的实例已经被回收,也便是 Java 堆中不存在该类及其任何这个类字累的实例加载这个类的类加载器已经被回收,但是类加载器一样平常很难会被回收,除非这个类加载器是为了这个目的设计的,比如 OSGI、JSP 的重加载等,否则常日很难达成。
这个类对应的 Class 工具没有任何地方被引用,无法在任何时候通过反射访问这个类的属性和方法。

虚拟机许可对知足上面这三个条件的无用类进行回收操作。

JVM 分代网络理论有哪些?

一样平常商业的虚拟机,大多数都遵照了分代网络的设计思想,分代网络理论紧张有两条假说。

第一个是强分代假说,强分代假说指的是 JVM 认为绝大多数工具的生存周期都是朝生夕灭的;

第二个是弱分代假说,弱分代假说指的是只要熬过越多次垃圾网络过程的工具就越难以回收(看来工具也会长心眼)。

便是基于这两个假说理论,JVM 将堆区划分为不同的区域,再将须要回收的工具根据其熬过垃圾回收的次数分配到不同的区域中存储。

JVM 根据这两条分代网络理论,把堆区划分为新生代(Young Generation)和老年代(Old Generation)这两个区域。
在新生代中,每次垃圾网络时都创造有大批工具去世去,剩下没有去世去的工具会直接晋升到老年代中。

上面这两个假说没有考虑工具的引用关系,而事实情形是,工具之间会存在引用关系,基于此又出身了第三个假说,即跨代引用假说(Intergeneration Reference Hypothesis),跨代引用比较较同代引用来说仅占少数。

正常来说存在相互引用的两个工具该当是同生共去世的,不过也会存在特例,如果一个新生代工具跨代引用了一个老年代的工具,那么垃圾回收的时候就不会回收这个新生代工具,更不会回收老年代工具,然后这个新生代工具熬过一次垃圾回收进入到老年代中,这时候跨代引用才会肃清。

根据跨代引用假说,我们不须要由于老年代中存在少量跨代引用就去直接扫描全体老年代,也不用在老年代中掩护一个列表记录有哪些跨代引用,实际上,可以直接在新生代中掩护一个影象集(Remembered Set),由这个影象集把老年代划分称为多少小块,标识出老年代的哪一块会存在跨代引用。

影象集的图示如下

从图中我们可以看到,影象集中的每个元素分别对应内存中的一块连续区域是否有跨代引用工具,如果有,该区域会被标记为“脏的”(dirty),否则便是“干净的”(clean)。
这样在垃圾回收时,只须要扫描影象集就可以大略地确定跨代引用的位置,是个范例的空间换韶光的思路。

聊一聊 JVM 中的垃圾回收算法?

在聊详细的垃圾回收算法之前,须要明确一点,哪些工具须要被垃圾网络器进行回收?也便是说须要先判断哪些工具是"垃圾"?

判断的标准我在上面如何判断工具已经去世亡的问题中描述了,有两种办法,一种是引用计数法,这种判断标准便是给工具添加一个引用计数器,引用这个工具会使计数器的值 + 1,引用失落效后,计数器的值就会 -1。
但是这种技能无法办理工具之间的循环引用问题。

还有一种办法是 GC Roots,GC Roots 这种办法因此 Root 根节点为核心,逐步向下搜索每个工具的引用,搜索走过的路径被称为引用链,如果搜索过后这个工具不存在引用链,那么这个工具便是无用工具,可以被回收。
GC Roots 可以办理循环引用问题,以是一样平常 JVM 都采取的是这种办法。

办理循环引用代码描述:

public class test{ public static void main(String[]args){ A a = new A(); B b = new B(); a=null; b=null; }}class A { public B b;}class B { public A a;}

基于 GC Roots 的这种思想,发展出了很多垃圾回收算法,下面我们就来聊一聊这些算法。

标记-打消算法

标记-打消(Mark-Sweep)这个算法可以说是最早最根本的算法了,标记-打消顾名思义分为两个阶段,即标记和打消阶段:首先标记处所有须要回收的工具,在标记完成后,统一回收掉所有被标记的工具。
当然也可以标记存活的工具,回收未被标记的工具。
这个标记的过程便是垃圾剖断的过程。

后续大部分垃圾回收算法都是基于标记-算法思想衍生的,只不过后续的算法填补了标记-打消算法的缺陷,那么它由什么缺陷呢?紧张有两个

实行效率不稳定,由于如果说堆中存在大量无用工具,而且大部分须要回收的情形下,这时必须进行大量的标记和打消,导致标记和打消这两个过程的实行效率随工具的数量增长而降落。
内存碎片化,标记-打消算法会在堆区产生大量不连续的内存碎片。
碎片太多会导致在分配大工具时没有足够的空间,不得不进行一次垃圾回收操作。

标记算法的示意图如下

标记-复制算法

由于标记-打消算法极易产生内存碎片,研究职员提出了标记-复制算法,标记-复制算法也可以简称为复制算法,复制算法是一种半区复制,它会将内存大小划分为相等的两块,每次只利用个中的一块,用完一块再用其余一块,然后再把用过的一块进行打消。
虽然办理了部分内存碎片的问题,但是复制算法也带来了新的问题,即复制开销,不过这种开销是可以降落的,如果内存中大多数工具是无用工具,那么就可以把少数的存活工具进行复制,再回收无用的工具。

不过复制算法的毛病也是显而易见的,那便是内存空间缩小为原来的一半,空间摧残浪费蹂躏太明显。
标记-复制算法示意图如下

现在 Java 虚拟机大多数都是用了这种算法来回收新生代,由于经由研究表明,新生代工具由 98% 都熬不过第一轮网络,因此不须要按照 1 : 1 的比例来划分新生代的内存空间。

基于此,研究职员提出了一种 Appel 式回收,Appel 式回收的详细做法是把新生代分为一块较大的 Eden 空间和两块 Survivor 空间,每次分配内存都只利用 Eden 和个中的一块 Survivor 空间,发生垃圾网络时,将 Eden 和 Survivor 中仍旧存活的工具一次性复制到其余一块 Survivor 空间上,然后直接清理掉 Eden 和已利用过的 Survivor 空间。

在主流的 HotSpot 虚拟机中,默认的 Eden 和 Survivor 大小比例是 8:1,也便是每次新生代中可用内存空间为全体新生代容量的 90%,只有一个 Survivor 空间,以是会摧残浪费蹂躏掉 10% 的空间。
这个 8:1 只是一个理论值,也便是说,不能担保每次都有不超过 10% 的工具存活,以是,当进行垃圾回收后如果 Survivor 容纳不了可存活的工具后,就须要其他内存空间来进行帮助,这种办法就叫做内存包管(Handle Promotion) ,常日情形下,作为包管的是老年代。

标记-整理算法

标记-复制算法虽然办理了内存碎片问题,但是没有办理复制工具存在大量开销的问题。
为理解决复制算法的毛病,充分利用内存空间,提出了标记-整理算法。
该算法标记阶段和标记-打消一样,但是在完成标记之后,它不是直接清理可回收工具,而是将存活工具都向一端移动,然后清理掉端边界以外的内存。
详细过程如下图所示:

什么是影象集,什么是卡表?影象集和卡表有什么关系?

为理解决跨代引用问题,提出了影象集这个观点,影象集是一个在新生代中利用的数据构造,它相称于是记录了一些指针的凑集,指向了老年代中哪些工具存在跨代引用。

影象集的实现有不同的粒度

字长精度:每个记录精确到一个字长,机器字长便是处理器的寻址位数,比如常见的 32 位或者 64 位处理器,这个精度决定了机器访问物理内存地址的指针长度,字中包含跨代指针。
工具精度:每个记录精确到一个工具,该工具里含有跨代指针。
卡精度:每个记录精确到一块内存区域,区域内含有跨代指针。

个中卡精度是利用了卡表作为影象集的实现,关于影象集和卡表的关系,大家可以想象成是 HashMap 和 Map 的关系。

什么是卡页?

卡表实在便是一个字节数组

CARD_TABLE[this address >> 9] = 0;

字节数组 CARD_TABLE 的每一个元素都对应着内存区域中一块特定大小的内存块,这个内存块便是卡页,一样平常来说,卡页都是 2 的 N 次幂字节数,通过上面的代码我们可以知道,卡页一样平常是 2 的 9 次幂,这也是 HotSpot 中利用的卡页,即 512 字节。

一个卡页的内存常日包含不止一个工具,只要卡页中有一个工具的字段存在跨代指针,那就将对应卡表的数组元素的值设置为 1,称之为这个元素变脏了,没有标示则为 0 。
在垃圾网络时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,然后把他们加入 GC Roots 进行扫描。

以是,卡页和卡表紧张用来办理跨代引用问题的。

什么是写樊篱?写樊篱带来的问题?

如果有其他分代区域中工具引用了本区域的工具,那么其对应的卡表元素就会变脏,这个引用说的便是工具赋值,也便是说卡表元素会变脏发生在工具赋值的时候,那么如何在工具赋值的时候更新掩护卡表呢?

在 HotSpot 虚拟机中利用的是写樊篱(Write Barrier) 来掩护卡表状态的,这个写樊篱和我们内存樊篱完备不同,希望读者不要搞混了。

这个写樊篱实在便是一个 Aop 切面,在引用工具进行赋值时会产生一个环形关照(Around),环形关照便是切面前后分别产生一个关照,由于这个又是写樊篱,以是在赋值前的部分写樊篱叫做写前樊篱,在赋值后的则叫做写后樊篱。

写樊篱会带来两个问题

无条件写樊篱带来的性能开销

每次对引用的更新,无论是否更新了老年代对新生代工具的引用,都会进行一次写樊篱操作。
显然,这会增加一些额外的开销。
但是,扫描全体老年代比较较,这个开销就低得多了。

不过,在高并发环境下,写樊篱又带来了伪共享(false sharing)问题。

高并发下伪共享带来的性能开销

在高并发情形下,频繁的写樊篱很随意马虎发生伪共享(false sharing),从而带来性能开销。

假设 CPU 缓存行大小为 64 字节,由于一个卡表项占 1 个字节,这意味着,64 个卡表项将共享同一个缓存行。

HotSpot 每个卡页为 512 字节,那么一个缓存行将对应 64 个卡页一共 64512 = 32K B。

如果不同线程对工具引用的更新操作,恰好位于同一个 32 KB 区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

一个大略的办理方案,便是不采取无条件的写樊篱,而是先检讨卡表标记,只有当该卡表项未被标记过才将其标记为脏的。

这便是 JDK 7 中引入的办理方法,引入了一个新的 JVM 参数 -XX:+UseCondCardMark,在实行写樊篱之前,先大略的做一下判断。
如果卡页已被标识过,则不再进行标识。

大略理解如下:

if (CARD_TABLE [this address >> 9] != 0) CARD_TABLE [this address >> 9] = 0;

与原来的实现比较,只是大略的增加了一个判断操作。

虽然开启 -XX:+UseCondCardMark 之后多了一些判断开销,但是却可以避免在高并发情形下可能发生的并发写卡表问题。
通过减少并发写操作,进而避免涌现伪共享问题(false sharing)。

什么是三色标记法?三色标记法会造成哪些问题?

根据可达性算法的剖析可知,如果要找出存活工具,须要从 GC Roots 开始遍历,然后搜索每个工具是否可达,如果工具可达则为存活工具,在 GC Roots 的搜索过程中,按照工具和其引用是否被访问过这个条件会分成下面三种颜色:

白色:白色表示 GC Roots 的遍历过程中没有被访问过的工具,涌现白色显然在可达性剖析刚刚开始的阶段,这个时候所有工具都是白色的,如果在剖析结束的阶段,仍旧是白色的工具,那么代表不可达,可以进行回收。
灰色:灰色表示工具已经被访问过,但是这个工具的引用还没有访问完毕。
玄色:玄色表示此工具已经被访问过了,而且这个工具的引用也已经呗访问了。

注:如果标记结束后工具仍为白色,意味着已经“找不到”该工具在哪了,不可能会再被重新引用。

当代的垃圾回收器险些都借鉴了三色标记的算法思想,只管实现的办法不尽相同:比如白色/玄色凑集一样平常都不会涌现(但是有其他表示颜色的地方)、灰色凑集可以通过栈/行列步队/缓存日志等办法进行实现、遍历办法可以是广度/深度遍历等等。

三色标记法会造成两种问题,这两种问题所涌现的环境都是由于用户环境和网络器并行事情造成的 。
当用户线程正在修正引用关系,此时网络器在回收引用关系,此时就会造成把原来已经消亡的工具标记为存活,如果涌现这种状况的话,问题不大,下次再让网络看重新网络一波就完了,但是还有一种情形是把存活的工具标记为去世亡,这种状况就会造成不可预知的后果。

针对上面这两种工具消逝问题,业界有两种处理办法,一种是增量更新(Incremental Update) ,一种是原是快照(Snapshot At The Beginning, SATB)。

请你先容一波垃圾网络器

垃圾网络器是口试的常考,也是必考点,只要涉及到 JVM 的干系问题,都会环绕着垃圾网络器来做一波展开,以是,有必要理解一下这些垃圾网络器。

垃圾网络器有很多,不同商家、不同版本的J VM 所供应的垃圾网络器可能会有很在差别,我们紧张先容 HotSpot 虚拟机中的垃圾网络器。

垃圾网络器是垃圾回收算法的详细实现,我们上面提到过,垃圾回收算法有标记-打消算法、标记-整理、标记-复制,以是对应的垃圾网络器也有不同的实现办法。

我们知道,HotSpot 虚拟机中的垃圾网络都是分代回收的,以是根据不同的分代,可以把垃圾网络器分为

新生代网络器:Serial、ParNew、Parallel Scavenge;

老年代网络器:Serial Old、Parallel Old、CMS;

整堆网络器:G1;

Serial 网络器

Serial 网络器是一种新生代的垃圾网络器,它是一个单线程事情的网络器,利用复制算法来进行回收,单线程事情不是说这个垃圾网络器只有一个,而是说这个网络器在事情时,必须停息其他所有事情线程,这种暴力的停息办法便是 Stop The World,Serial 就彷佛是寡头垄断一样,只要它一发话,其他所有的小弟(线程)都得给它让路。
Serial 网络器的示意图如下:

SefePoint 全局安全点:它便是代码中的一段分外的位置,在所有用户线程到达 SafePoint 之后,用户线程挂起,GC 线程会进行清理事情。

虽然 Serial 有 STW 这种显而易见的缺陷,不过,从其他角度来看,Serial 还是很讨喜的,它还有着优于其他网络器的地方,那便是大略而高效,对付内存资源首先的环境,它是所有网络器中额外内存花费最小的,对付单核处理器或者处理器核心较少的环境来说,Serial 网络器由于没有线程交互开销,以是 Serial 专心做垃圾回收效率比较高。

ParNew 网络器

ParNew 是 Serial 的多线程版本,除了同时利用多条线程外,其他参数和机制(STW、回收策略、工具分配规则)都和 Serial 完备同等,ParNew 网络器的示意图如下:

虽然 ParNew 利用了多条线程进行垃圾回收,但是在单线程环境下它绝对不会比 Serial 网络效率更高,由于多线程存在线程交互的开销,但是随着可用 CPU 核数的增加,ParNew 的处理效率会比 Serial 更高效。

Parallel Scavenge 网络器

Parallel Scavenge 网络器也是一款新生代网络器,它同样是基于标记-复制算法实现的,而且它也能够并行网络,这么看来,表面上 Parallel Scavenge 于 ParNew 非常相似,那么它们之间有什么差异呢?

Parallel Scavenge 的关注点紧张在达到一个可掌握的吞吐量上面。
吞吐量便是处理器用于运行用户代码的韶光与处理器总花费韶光的比。
也便是

这里给大家举一个吞吐量的例子,如果实行用户代码的韶光 + 运行垃圾网络的韶光统共耗费了 100 分钟,个中垃圾网络耗费掉了 1 分钟,那么吞吐量便是 99%。
停顿韶光越短就越适宜须要与用户交互或须要担保做事相应质量,良好的相应速率可以提升用户体验,而高吞吐量可以最高效率利用处理器资源。

Serial Old 网络器

前面先容了一下 Serial,我们知道它是一个新生代的垃圾网络,利用了标记-复制算法。
而这个 Serial Old 网络器却是 Serial 的老年版本,它同样也是一个单线程网络器,利用的是标记-整理算法,Serial Old 网络器有两种用场:一种是在 JDK 5 和之前的版本与 Parallel Scavenge 网络器搭配利用,其余一种用法便是作为 CMS 网络器的备选,CMS 垃圾网络器我们下面说,Serial Old 的网络流程如下

Parallel Old 网络器

前面我们先容了 Parallel Scavenge 网络器,现在来先容一下 Parallel Old 网络器,它是 Parallel Scavenge 的老年版本,支持多线程并发网络,基于标记 - 整理算法实现,JDK 6 之后涌现,吞吐量优先可以考虑 Parallel Scavenge + Parallel Old 的搭配

CMS 网络器

CMS网络器的紧张目标是获取最短的回收停顿韶光,它的全称是 Concurrent Mark Sweep,从这个名字就可以知道,这个网络器是基于标记 - 打消算法实现的,而且支持并发网络,它的运行过程要比上面我们提到的网络器繁芜一些,它的事情流程如下:

初始标记(CMS initial mark)并发标记(CMS concurrent mark)重新标记(CMS remark)并发打消(CMS concurrent sweep)

对付上面这四个步骤,初始标记和并发标记都须要 Stop The World,初始标记只是标记一下和 GC Roots 直接关联到的工具,速率较快;并发标记阶段便是从 GC Roots 的直接关联工具开始遍历全体工具图的过程。
这个过程韶光比较长但是不须要停顿用户线程,也便是说与垃圾网络线程一起并发运行。
并发标记的过程中,可能会有错标或者漏标的情形,此时就须要在重新标记一下,末了是并发打消阶段,清理掉标记阶段中止定已经去世亡的工具。

CMS 的网络过程如下

CMS 是一款非常精良的垃圾网络器,但是没有任何网络器能够做到完美的程度,CMS 也是一样,CMS 至少有三个缺陷:

CMS 对处理器资源非常敏感,在并发阶段,虽然不会造成用户线程停顿,但是却会由于占用一部分线程而导致运用程序变慢,降落总吞吐量。
CMS 无法处理浮动垃圾,有可能涌现Concurrent Mode Failure失落败进而导致另一次完备 Stop The WorldFull GC 产生。
什么是浮动垃圾呢?由于并发标记和并发清理阶段,用户线程仍在连续运行,以是程序自然而然就会伴随着新的垃圾不断涌现,而且这一部分垃圾涌如今标记结束之后,CMS 无法处理这些垃圾,以是只能等到下一次垃圾回收时在进行清理。
这一部分垃圾就被称为浮动垃圾。
CMS 末了一个缺陷是并发-打消的通病,也便是会有大量的空间碎片涌现,这将会给分配大工具带来困难。
Garbage First 网络器

Garbage First 又被称为 G1 网络器,它的涌现意味着垃圾网络器走过了一个里程碑,为什么说它是里程碑呢?由于 G1 这个网络器是一种面向局部的垃圾网络器,HotSpot 团队开拓这个垃圾网络器为了让它更换掉 CMS 网络器,以是到后来,JDK 9 发布后,G1 取代了 Parallel Scavenge + Parallel Old 组合,成为做事端默认的垃圾网络器,而 CMS 则不再推举利用。

之前的垃圾网络器存在回收区域的局限性,由于之前这些垃圾网络器的目标范围要么是全体新生代、要么是全体老年代,要么是全体 Java 堆(Full GC),而 G1 跳出了这个框架,它可以面向堆内存的任何部分来组成回收集(Collection Set,CSet),衡量垃圾网络的不再是哪个分代,这便是 G1 的 Mixed GC 模式。

G1 是基于 Region 来进行回收的,Region 便是堆内存中任意的布局,每一块 Region 都可以根据须要扮演 Eden 空间、Survivor 空间或者老年代空间,网络器能够对不同的 Region 角色采取不同的策略来进行处理。
Region 中还有一块分外的区域,这块区域便是 Humongous 区域,它是专门用来存储大工具的,G1 认为只要大小超过了 Region 容量一半的工具即可剖断为大工具。
如果超过了 Region 容量的大工具,将会存储在连续的 Humongous Region 中,G1 大多数行为都会吧 Humongous Region 作为老年代来看待。

G1 保留了新生代(Eden Suvivor)和老年代的观点,但是新生代和老年代不再是固定的了。
它们都是一系列区域的动态凑集。

G1 网络器的运作过程可以分为以下四步:

初始标记:这个步骤也仅仅是标记一下 GC Roots 能够直接关联到的工具;并修正 TAMS 指针的值(每一个 Region 都有两个 RAMS 指针),似的下一阶段用户并发运行时,能够在可用的 Region 等分配工具,这个阶段须要停息用户线程,但是韶光很短。
这个停顿是借用 Minor GC 的时候完成的,以是可以忽略不计。
并发标记:从 GC Root 开始对堆中工具进行可达性剖析,递归扫描全体堆中的工具图,找出要回收的工具。
当工具图扫描完成后,重新处理 SATB 记录下的在并发时有引用的工具;终极标记:对用户线程做一个短暂的停息,用于处理并发阶段结束后遗留下来的少量 SATB 记录(一种原始快照,用来记录并发标记中某些工具)筛选回收:卖力更新 Region 的统计数据,对各个 Region 的回收代价和本钱进行排序,根据用户所期望的停顿韶光来制订回收操持,可以自由选择多个 Region 构成回收集,然后把决定要回收的那一部分 Region 存活工具复制到空的 Region 中,再清理掉全体旧 Region 的全部空间。
这里的操作设计工具的移动,以是必须要停息用户线程,由多条网络器线程并行网络

从上面这几个步骤可以看出,除了并发标记外,别的三个阶段都须要停息用户线程,以是,这个 G1 网络器并非追求低延迟,官方给出的设计目标是在延迟可控的情形下尽可能的提高吞吐量,担当全功能网络器的重任。

下面是 G1 回收的示意图

G1 网络器同样也有缺点和问题:

第一个问题便是 Region 中存在跨代引用的问题,我们之前知道可以用影象集来办理跨代引用问题,不过 Region 中的跨代引用要繁芜很多;第二个问题便是如何担保网络线程与用户线程互不滋扰的运行?CMS 利用的是增量更新算法,G1 利用的是原始快照(SATB),G1 为 Region 分配了两块 TAMS 指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新工具分配,并发回收时心分配的工具地址都必须在这两个指针位置以上。
如果内存回收速率赶不上内存分配速率,G1 网络器也要冻结用户线程实行,导致 Full GC 而产生永劫光的 STW。
第三个问题是无法建立可预测的停顿模型。
JVM 常用命令先容

下面先容一下 JVM 中常用的调优、故障处理等工具。

jps :虚拟机进程工具,全称是 JVM Process Status Tool,它的功能和 Linux 中的 ps 类似,可以列出正在运行的虚拟机进程,并显示虚拟机实行主类 Main Class 所在确当地虚拟机唯一 ID,虽然功能比较单一,但是这个命令绝对是利用最高频的一个命令。
jstat:虚拟机统计信息工具,用于监视虚拟机各种运行状态的信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾网络、即时编译等运行时数据。
jinfo:Java 配置信息工具,全称是 Configuration Info for Java,它的浸染是可以事实调度虚拟机各项参数。
jmap:Java 内存映像工具,全称是 Memory Map For Java,它用于天生转储快照,用来排查内存占用情形jhat:虚拟机堆转储快照剖析工具,全称是 JVM Heap Analysis Tool,这个指令常日和 jmap 一起搭配利用,jhat 内置了一个 HTTP/Web 做事器,天生转储快照后可以在浏览器中查看。
不过,一样平常还是 jmap 命令利用的频率比较高。
jstack:Java 堆栈跟踪工具,全称是 Stack Trace for Java ,顾名思义,这个命令用来追踪堆栈的利用情形,用于虚拟机当前时候的线程快照,线程快照便是当前虚拟机内每一条正在实行的方法堆栈的凑集。
什么是双亲委派模型?

JVM 类加载默认利用的是双亲委派模型,那么什么是双亲委派模型呢?

这里我们须要先先容一下三种类加载器:

启动类加载器,Bootstrap Class Loader,这个类加载器是 C++ 实现的,它是 JVM 的一部分,这个类加载器卖力加载存放在 <JAVA_HOME>\lib 目录,启动类加载器无法被 Java 程序直接引用。
这也便是说,JDK 中的常用类的加载都是由启动类加载器来完成的。
扩展类加载器,Extension Class Loader,这个类加载器是 Java 实现的,它卖力加载 <JAVA_HOME>\lib\ext 目录。
运用程序类加载器,Application Class Loader,这个类加载器是由 sum.misc.Launcher$AppClassLoader 来实现,它卖力加载 ClassPath 上所有的类库,如果运用程序中没有定义自己的类加载器,默认利用便是这个类加载器。

以是,我们的 Java 运用程序都是由这三种类加载器来相互合营完成的,当然,用户也可以自己定义类加载器,即 User Class Loader,这几个类加载器的模型如下

上面这几类类加载器构成了不同的层次构造,当我们须要加载一个类时,子类加载器并不会立时去加载,而是依次去要求父类加载器加载,一贯往上要求到最高类加载器:启动类加载器。
当启动类加载器加载不了的时候,依次往下让子类加载器进行加载。
这便是双亲委派模型。

双亲委派模型的毛病?

在双亲委派模型中,子类加载器可以利用父类加载器已经加载的类,而父类加载器无法利用子类加载器已经加载的。
这就导致了双亲委派模型并不能办理所有的类加载器问题。

Java 供应了很多外部接口,这些接口统称为 Service Provider Interface, SPI,许可第三方实现这些接口,而这些接口却是 Java 核心类供应的,由 Bootstrap Class Loader 加载,而一样平常的扩展接口是由 Application Class Loader 加载的,Bootstrap Class Loader 是无法找到 SPI 的实现类的,由于它只加载 Java 的核心库。
它也不能代理给 Application Class Loader,由于它是最顶层的类加载器。

双亲委派机制的三次毁坏

虽然双亲委派机制是 Java 强烈推举给开拓者们的类加载器的实现办法,但是并没有逼迫规定你必须就要这么实现,以是,它一样也存在被毁坏的情形,实际上,历史上一共涌现三次双亲委派机制被毁坏的情形:

双亲委派机制第一次被毁坏发生在双亲委派机制涌现之前,由于双亲委派机制 JDK 1.2 之后才引用的,但类加载的观点在 Java 刚涌现的时候就有了,以是引用双亲委派机制之前,设计者们必须兼顾开拓者们自定义的一些类加载器的代码,以是在 JDK 1.2 之后的 java.lang.ClassLoader 中添加了一个新的 findClass 方法,勾引用户编写类加载器逻辑的时候重写这个 findClass 方法,而不是基于 loadClass编写。
双亲委派机制第二次被毁坏是由于它自己模型导致的,由于它只能向上(根本)加载,越根本的类越由上层加载器加载,以是如果根本类型又想要调用用户的代码,该怎么办?这也便是我们上面那个问题所说的 SPI 机制。
那么 JDK 团队是如何做的呢?它们引用了一个 线程高下文类加载器(Thread Context ClassLoader),这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader 进行设置,如果创建时线程还未设置,它将会从父线程中继续,如果全局没有设置类加载器的话,这个 ClassLoader 便是默认的类加载器。
这种行为虽然是一种犯规行为,但是 Java 代码中的 JNDI、JDBC 等都是利用这种办法来完成的。
直到 JDK 6 ,引用了 java.util.ServiceLoader,利用 META-INF/services + 任务链的设计模式,才办理了 SPI 的这种加载机制。
双亲委派机制第三次被毁坏是由于用户对程序的动态需求使热加载、热支配的引入所致。
由于时期的变革,我们希望 Java 能像鼠标键盘一样实现热支配,即时加载(load class),引入了 OSGI,OSGI 实现热支配的关键在于它自定义类加载器机制的实现,OSGI 中的每一个 Bundle 也便是模块都有一个自己的类加载器。
当须要改换 Bundle 时,就直接把 Bundle 连同类加载器一起更换掉就能够实现热加载。
在 OSGI 环境下,类加载器不再屈服双亲委派机制,而是利用了一种更繁芜的加载机制。
常见的 JVM 调优参数有哪些?-Xms256m:初始化堆大小为 256m;-Xmx2g:堆最大内存为 2g;-Xmn50m:新生代的大小50m;-XX:+PrintGCDetails 打印 gc 详细信息;-XX:+HeapDumpOnOutOfMemoryError 在发生OutOfMemoryError缺点时,来 dump 出堆快照;-XX:NewRatio=4 设置年轻的和老年代的内存比例为 1:4;-XX:SurvivorRatio=8 设置新生代 Eden 和 Survivor 比例为 8:2;-XX:+UseSerialGC 新生代和老年代都用串行网络器 Serial + Serial Old-XX:+UseParNewGC 指定利用 ParNew + Serial Old 垃圾回收器组合;-XX:+UseParallelGC 新生代利用 Parallel Scavenge,老年代利用 Serial Old-XX:+UseParallelOldGC:新生代 ParallelScavenge + 老年代 ParallelOld 组合;-XX:+UseConcMarkSweepGC:新生代利用 ParNew,老年代的用 CMS;-XX:NewSize:新生代最小值;-XX:MaxNewSize:新生代最大值-XX:MetaspaceSize 元空间初始化大小-XX:MaxMetaspaceSize 元空间最大值

如果对你有帮助,可以关注一下 "大众号:程序员cxuan, 有更多的硬核文章等着你。