2026/2/28 1:05:34
网站建设
项目流程
北京哪家做网站好,做网站销售怎么做,网站推广活动,公司注册资金100万要实缴吗#x1f9d1; 博主简介#xff1a;CSDN博客专家#xff0c;历代文学网#xff08;PC端可以访问#xff1a;https://literature.sinhy.com/#/literature?__c1000#xff0c;移动端可微信小程序搜索“历代文学”#xff09;总架构师#xff0c;15年工作经验#xff0c;… 博主简介CSDN博客专家历代文学网PC端可以访问https://literature.sinhy.com/#/literature?__c1000移动端可微信小程序搜索“历代文学”总架构师15年工作经验精通Java编程高并发设计Springboot和微服务熟悉LinuxESXI虚拟化以及云原生Docker和K8s热衷于探索科技的边界并将理论知识转化为实际应用。保持对新技术的好奇心乐于分享所学希望通过我的实践经历和见解启发他人的创新思维。在这里我希望能与志同道合的朋友交流探讨共同进步一起在技术的世界里不断学习成长。技术合作请加本人wx注明来自csdnforeast_seaJVM 面试题相关总结JVM 面试题总结JVM 的主要作用是什么请你描述一下 Java 的内存区域请你描述一下 Java 中的类加载机制加载验证准备解析初始化使用卸载在 JVM 中对象是如何创建的内存分配方式有哪些呢请你说一下对象的内存布局对象头 Header实例数据 Instance Data对齐 Padding对象访问定位的方式有哪些如何判断对象已经死亡如何判断一个不再使用的类JVM 分代收集理论有哪些聊一聊 JVM 中的垃圾回收算法标记-清除算法标记-复制算法标记-整理算法什么是记忆集什么是卡表记忆集和卡表有什么关系什么是卡页什么是写屏障写屏障带来的问题什么是三色标记法三色标记法会造成哪些问题请你介绍一波垃圾收集器Serial 收集器ParNew 收集器Parallel Scavenge 收集器Serial Old 收集器Parallel Old 收集器CMS 收集器Garbage First 收集器JVM 常用命令介绍什么是双亲委派模型双亲委派模型的缺陷双亲委派机制的三次破坏常见的 JVM 调优参数有哪些肝了一篇非常硬核的 JVM 基础总结写作不易小伙伴们赶紧点赞、转发安排起来JVM 的主要作用是什么JVM 就是 Java Virtual MachineJava虚拟机的缩写JVM 屏蔽了与具体操作系统平台相关的信息使 Java 程序只需生成在 Java 虚拟机上运行的目标代码 字节码就可以在不同的平台上运行。请你描述一下 Java 的内存区域JVM 在执行 Java 程序的过程中会把它管理的内存分为若干个不同的区域这些组成部分有些是线程私有的有些则是线程共享的Java 内存区域也叫做运行时数据区它的具体划分如下虚拟机栈: 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 的类加载机制。一个类从被加载到虚拟机内存开始到卸载出内存为止一共会经历下面这些过程。类加载机制一共有五个步骤分别是加载、链接、初始化、使用和卸载阶段这五个阶段的顺序是确定的。其中链接阶段会细分成三个阶段分别是验证、准备、解析阶段这三个阶段的顺序是不确定的这三个阶段通常交互进行。解析阶段通常会在初始化之后再开始这是为了支持 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 属性所指定的初始值比如publicstaticfinalintvalue666;编译时就会把 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 用来存放线程ID2bit 用来存放 epoch4bit 存放分代年龄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 RootsGC Roots 这种方式是以 Root 根节点为核心逐步向下搜索每个对象的引用搜索走过的路径被称为引用链如果搜索过后这个对象不存在引用链那么这个对象就是无用对象可以被回收。GC Roots 可以解决循环引用问题所以一般 JVM 都采用的是这种方式。解决循环引用代码描述publicclasstest{publicstaticvoidmain(String[]args){AanewA();BbnewB();anull;bnull;}}classA{publicBb;}classB{publicAa;}基于 GC Roots 的这种思想发展出了很多垃圾回收算法下面我们就来聊一聊这些算法。标记-清除算法**标记-清除(Mark-Sweep)**这个算法可以说是最早最基础的算法了标记-清除顾名思义分为两个阶段即标记和清除阶段首先标记处所有需要回收的对象在标记完成后统一回收掉所有被标记的对象。当然也可以标记存活的对象回收未被标记的对象。这个标记的过程就是垃圾判定的过程。后续大部分垃圾回收算法都是基于标记-算法思想衍生的只不过后续的算法弥补了标记-清除算法的缺点那么它由什么缺点呢主要有两个执行效率不稳定因为假如说堆中存在大量无用对象而且大部分需要回收的情况下这时必须进行大量的标记和清除导致标记和清除这两个过程的执行效率随对象的数量增长而降低。内存碎片化标记-清除算法会在堆区产生大量不连续的内存碎片。碎片太多会导致在分配大对象时没有足够的空间不得不进行一次垃圾回收操作。标记算法的示意图如下标记-复制算法由于标记-清除算法极易产生内存碎片研究人员提出了标记-复制算法标记-复制算法也可以简称为复制算法复制算法是一种半区复制它会将内存大小划分为相等的两块每次只使用其中的一块用完一块再用另外一块然后再把用过的一块进行清除。虽然解决了部分内存碎片的问题但是复制算法也带来了新的问题即复制开销不过这种开销是可以降低的如果内存中大多数对象是无用对象那么就可以把少数的存活对象进行复制再回收无用的对象。不过复制算法的缺陷也是显而易见的那就是内存空间缩小为原来的一半空间浪费太明显。标记-复制算法示意图如下现在 Java 虚拟机大多数都是用了这种算法来回收新生代因为经过研究表明新生代对象由 98% 都熬不过第一轮收集因此不需要按照 1 1 的比例来划分新生代的内存空间。基于此研究人员提出了一种 Appel 式回收Appel 式回收的具体做法是把新生代分为一块较大的Eden空间和两块Survivor空间每次分配内存都只使用 Eden 和其中的一块 Survivor 空间发生垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上然后直接清理掉 Eden 和已使用过的 Survivor 空间。在主流的 HotSpot 虚拟机中默认的 Eden 和 Survivor 大小比例是 81也就是每次新生代中可用内存空间为整个新生代容量的 90%只有一个 Survivor 空间所以会浪费掉 10% 的空间。这个 81 只是一个理论值也就是说不能保证每次都有不超过 10% 的对象存活所以当进行垃圾回收后如果 Survivor 容纳不了可存活的对象后就需要其他内存空间来进行帮助这种方式就叫做内存担保(Handle Promotion)通常情况下作为担保的是老年代。标记-整理算法标记-复制算法虽然解决了内存碎片问题但是没有解决复制对象存在大量开销的问题。为了解决复制算法的缺陷充分利用内存空间提出了标记-整理算法。该算法标记阶段和标记-清除一样但是在完成标记之后它不是直接清理可回收对象而是将存活对象都向一端移动然后清理掉端边界以外的内存。具体过程如下图所示什么是记忆集什么是卡表记忆集和卡表有什么关系为了解决跨代引用问题提出了记忆集这个概念记忆集是一个在新生代中使用的数据结构它相当于是记录了一些指针的集合指向了老年代中哪些对象存在跨代引用。记忆集的实现有不同的粒度字长精度每个记录精确到一个字长机器字长就是处理器的寻址位数比如常见的 32 位或者 64 位处理器这个精度决定了机器访问物理内存地址的指针长度字中包含跨代指针。对象精度每个记录精确到一个对象该对象里含有跨代指针。卡精度每个记录精确到一块内存区域区域内含有跨代指针。其中卡精度是使用了卡表作为记忆集的实现关于记忆集和卡表的关系大家可以想象成是 HashMap 和 Map 的关系。什么是卡页卡表其实就是一个字节数组CARD_TABLE[thisaddress9]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 个卡页一共 64*512 32K B。如果不同线程对对象引用的更新操作恰好位于同一个 32 KB 区域内这将导致同时更新卡表的同一个缓存行从而造成缓存行的写回、无效化或者同步操作间接影响程序性能。一个简单的解决方案就是不采用无条件的写屏障而是先检查卡表标记只有当该卡表项未被标记过才将其标记为脏的。这就是 JDK 7 中引入的解决方法引入了一个新的 JVM 参数-XX:UseCondCardMark在执行写屏障之前先简单的做一下判断。如果卡页已被标识过则不再进行标识。简单理解如下if(CARD_TABLE[this address9]!0)CARD_TABLE[this address9]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整堆收集器G1Serial 收集器Serial 收集器是一种新生代的垃圾收集器它是一个单线程工作的收集器使用复制算法来进行回收单线程工作不是说这个垃圾收集器只有一个而是说这个收集器在工作时必须暂停其他所有工作线程这种暴力的暂停方式就是Stop The WorldSerial 就好像是寡头垄断一样只要它一发话其他所有的小弟线程都得给它让路。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 World的Full GC产生。什么是浮动垃圾呢由于并发标记和并发清理阶段用户线程仍在继续运行所以程序自然而然就会伴随着新的垃圾不断出现而且这一部分垃圾出现在标记结束之后CMS 无法处理这些垃圾所以只能等到下一次垃圾回收时在进行清理。这一部分垃圾就被称为浮动垃圾。CMS 最后一个缺点是并发-清除的通病也就是会有大量的空间碎片出现这将会给分配大对象带来困难。Garbage First 收集器Garbage First 又被称为G1 收集器它的出现意味着垃圾收集器走过了一个里程碑为什么说它是里程碑呢因为 G1 这个收集器是一种面向局部的垃圾收集器HotSpot 团队开发这个垃圾收集器为了让它替换掉 CMS 收集器所以到后来JDK 9 发布后G1 取代了 Parallel Scavenge Parallel Old 组合成为服务端默认的垃圾收集器而 CMS 则不再推荐使用。之前的垃圾收集器存在回收区域的局限性因为之前这些垃圾收集器的目标范围要么是整个新生代、要么是整个老年代要么是整个 Java 堆Full GC而 G1 跳出了这个框架它可以面向堆内存的任何部分来组成回收集(Collection SetCSet)衡量垃圾收集的不再是哪个分代这就是 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 使用的是原始快照SATBG1 为 Region 分配了两块 TAMS 指针把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配并发回收时心分配的对象地址都必须在这两个指针位置以上。如果内存回收速度赶不上内存分配速度G1 收集器也要冻结用户线程执行导致 Full GC 而产生长时间的 STW。第三个问题是无法建立可预测的停顿模型。JVM 常用命令介绍下面介绍一下 JVM 中常用的调优、故障处理等工具。jps虚拟机进程工具全称是JVM Process Status Tool它的功能和 Linux 中的ps类似可以列出正在运行的虚拟机进程并显示虚拟机执行主类Main Class所在的本地虚拟机唯一 ID虽然功能比较单一但是这个命令绝对是使用最高频的一个命令。jstat虚拟机统计信息工具用于监视虚拟机各种运行状态的信息的命令行工具它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。jinfoJava 配置信息工具全称是Configuration Info for Java它的作用是可以事实调整虚拟机各项参数。jmapJava 内存映像工具全称是Memory Map For Java它用于生成转储快照用来排查内存占用情况jhat虚拟机堆转储快照分析工具全称是JVM Heap Analysis Tool这个指令通常和 jmap 一起搭配使用jhat 内置了一个 HTTP/Web 服务器生成转储快照后可以在浏览器中查看。不过一般还是 jmap 命令使用的频率比较高。jstackJava 堆栈跟踪工具全称是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引入了 OSGIOSGI 实现热部署的关键在于它自定义类加载器机制的实现OSGI 中的每一个Bundle也就是模块都有一个自己的类加载器。当需要更换 Bundle 时就直接把 Bundle 连同类加载器一起替换掉就能够实现热加载。在 OSGI 环境下类加载器不再遵从双亲委派机制而是使用了一种更复杂的加载机制。常见的 JVM 调优参数有哪些-Xms256m初始化堆大小为 256m-Xmx2g堆最大内存为 2g-Xmn50m新生代的大小50m-XX:PrintGCDetails 打印 gc 详细信息-XX:HeapDumpOnOutOfMemoryError 在发生OutOfMemoryError错误时来 dump 出堆快照-XX:NewRatio4 设置年轻的和老年代的内存比例为 1:4-XX:SurvivorRatio8 设置新生代 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 元空间最大值