跳至主要內容

JVM

HeChuangJun约 7443 字大约 25 分钟

.1. JVM、运行时数据区组成?作用?堆和栈区别?JDK678内存区域的变化

示意图
jvmstructure.jpg
jvmstructure.jpg

类加载器ClassLoader:负责将字节码文件加载到JVM中,生成Class对象
运行时数据区Runtime Data Area:负责管理java程序运行时的内存
执行引擎Execution Engine:负责将字节码转换为机器码并执行,包括即时编译器、垃圾回收器
本地库接口Native Interface:负责执行与底层系统或硬件交互的操作或者Java无法直接实现的功能

程序计数器Program Counter Register:线程私有 记录当前线程执行的字节码指令地址
java虚拟机栈Java Virtual Machine Statcks:~ 存储每个线程的栈帧。当线程执行方法时会创建栈帧存储局部变量表、操作数栈、动态链接、方法返回地址等信息。当方法执行完后会从栈中移除
本地方法栈Native Method Stack:~ 执行native方法服务。存储native方法的~
java堆Java Heap:线程共享 存储大部分对象和数组
方法区Method Area:~ 逻辑概念,并不真实存在。存储类的元数据、运行时常量池、字符串常量池、静态变量、类的方法字节码等信息
直接内存Direct Memory:不在JVM规范中,通过NIO库引入,用于提高I/O操作的性能,能直接分配内存

堆属于线程共享的内存区域,栈属于线程私有的内存区域
堆存储大部分对象和数组,栈存储局部变量、方法参数、对象引用等
堆对象生命周期可以在方法调用结束后继续存在,直到不再被引用,然后被垃圾回收。栈对象随着方法调用的结束而自动释放,不需要垃圾回收

JDK6使用永久代实现方法区,大小固定,容易内存溢出
JDK7继续~,运行时常量池、字符串常量池移到堆,
JDK8去掉永久代,使用直接内存作为元空间,全都移动到元空间
修改原因:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen

.2. 普通对象(除了数组和Class对象)创建、销毁过程?内存分配方法?怎么保证线程安全?

类加载检查:检查类是否已被加载,如果没有,则执行类加载过程
在堆中给对象分配内存
将分配到的内存空间都初始化为零值
设置对象头信息
如果字节码中有invokespecial指令则执行init方法把对象初始化

垃圾收集器通过可达性分析算法判断对象是否存活,从GC Root开始,递归查找所有可以到达的对象,如果对象无法通过任何路径从GC Root访问到,可能被回收。通过标记清除、标记复制、标记整理等算法来回收内存,释放对象占用的内存

指针碰撞Bump the Pointer:如果堆内存地址连续,分配器用指针指向可用内存的起始位置,分配内存时将指针移动到下一个可用位置,然后将这段内存分配给对象实例即可。分配快,有内存碎片,如年轻代
空闲列表Free List:如果堆内存地址不连续,分配器用列表记录可用内存块,从列表中找到一块足够大的空间分配给对象实例,并更新记录。用于内存碎片严重或对象大小差异大,如老年代

分配内存时线程安全方案(多线程给对象A分配内存,还没修改指针,对象B又用该指针分配内存)
使用同步机制,采用CAS和失败重试的方式保证更新操作的原子性
使用本地线程分配缓冲Thread Local Allocation Buffer TLAB,允许每个线程有分配缓冲区,只有TLAB用完并分配新TLAB时才同步 -XX:+/-UseTLAB

.3. 对象在内存中的存储布局?对象、对象引用大小?对象怎么访问定位?

对象头Object Header:存储堆对象的布局、类型、GC状态、同步状态和标识哈希码等信息
实例数据Instance Data:存储对象的数据信息,父类的信息,对象字段属性信息
对齐填充Padding:可选,用于对齐实例数据部分,为了计算机高效寻址。jvm要求对象的大小是8字节整数倍。否则有可能出现跨缓存行的字段。效率低

对象头:-XX:+/-UseCompressedClassPointers/UseCompressedOops //开启/关闭压缩类/普通对象指针
mark word 存储对象自身的运行数据
锁标志位lock:区分锁状态
biased_lock:是否偏向锁
分代年龄age:对象被GC的次数
对象的hashcode:对象加锁后31位不够表示,会被转移到Monitor中
偏向锁的线程ID:表示持有偏向锁的线程的ID
epoch:偏向锁在CAS锁操作过程中表示对象更偏向哪个锁。
ptr_to_lock_record:轻量级锁状态下指向栈中锁记录的指针
ptr_to_heavyweight_monitor:重量级锁状态下指向对象监视器Monitor的指针
类型指针Klass Pointer 指向对象所属类的元数据的指针,用于确定对象是哪个类的实例
Length field 当对象是数组时的大小
objectheader.png
64bithotSpotmarkword.png

64位操作系统,JDK8默认开启压缩指针
new Object()占16字节(12 字节的对象头(8字节markword+4字节类型指针) + 4 字节的对齐填充)
对象引用占4字节。关闭则8字节

查看对象的内存布局
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public class JOLSample {
    public static void main(String[] args) {
        // 打印JVM详细信息
        System.out.println(VM.current().details());

        Object obj = new Object();

        // Object实例的内存布局
        String layout = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(layout);
    }
}
class ReferenceSizeExample {
    private static class ReferenceHolder {
        Object reference;
    }

    public static void main(String[] args) {
        System.out.println(VM.current().details());
        System.out.println(ClassLayout.parseClass(ReferenceHolder.class).toPrintable());
    }
}
OFFSET:偏移地址,单位字节
SIZE:大小,单位字节
TYPE DESCRIPTION:类型描述
VALUE:对应内存中当前存储的值,二进制32位;
ReferenceHolder.reference字段位于偏移量12,为4字节,对象引用占用的内存大小为4字节
javaobjectsize.png
javaobjectsize.png
javaobjectreferencesize.png
javaobjectreferencesize.png

通过栈上的reference数据来访问堆上的具体对象。reference类型是一个指向对象的引用,访问方式分为
句柄访问:reference中存储的是句柄地址,而句柄中包含了对象实例数据与类型数据的地址信息,对象被移动(垃圾收集)时修改句柄中的实例数据指针,而reference不变
直接指针访问(HotSpot):reference中存储的是对象地址,速度快,节省了一次指针定位的时间开销,适合对象访问频繁的场景

.4. 内存溢出/泄漏概念、举例、解决方案?

内存溢出:当程序请求分配内存时没有足够的内存空间抛出OutOfMemoryError。因为内存泄漏、程序尝试分配大内存
内存泄漏:当程序在使用完内存时没有释放的内存空间,导致这部分内存无法再被使用。随着时间的推移,可用内存逐渐减少,最终导致内存溢出。通常发生在长期存活的对象没有及时释放对短期存活对象的引用,从而导致短期存活对象无法被回收。

静态集合类:生命周期和JVM一致,尽量不要使用static成员变量,减少生命周期
单例模式:初始化后生命周期和JVM一致(以静态变量的方式),如果单例对象持有外部的引用,那么对象将不能被JVM正常回收,导致内存泄漏
连接:数据库、网络连接t和IO连接不会自动被GC回收;调用close()关闭资源
变量不合理的作用域:变量的定义作用域大于其使用范围或不再使用对象没有及时将对象设置为null,导致内存泄漏。不用的对象,手动设置为null
hash值发生变化:对象修改后的Hash值和存储进容器时的Hash值不同,所以找不到对象删除
ThreadLocal使用不当。使用完ThreadLocal用remove方法清除
监听器:释放对象的时候没有删除监听器
内部类:内部类的引用没释放导致后继类对象没有释放
连单内监静ht变

.5. jvm垃圾判断算法?被标记为垃圾的对象一定会被回收吗?finalize()方法作用?强软弱虚引用?为什么这么设计?

垃圾回收Garbage Collection,GC用来判断哪些对象在内存中不再被引用,可以被垃圾回收器回收的算法
引用计数算法Reference Counting 用计数器记录引用对象的数量。每当引用指向对象时计数加1;引用被删除时计数减1。计数为0表示对象不再被引用时可以被回收。简单,效率高,实时性高,无需等到内存不够的时才回收,有循环引用问题
可达性分析算法Reachability Analysis 以GC Roots对象为起点向下搜索,走过的路径称为引用连Reference Chain,当一个对象到GC Roots没有任何引用链相连时,即从GC Roots到这个对象不可达,证明此对象是不可用的。可以处理循环引用问题,遍历和标记对象的开销较大。

java虚拟机栈(栈中的本地变量表)中引用的对象(方法的参数、局部变量等)
本地方法栈中JNI(native方法)引用的对象
类静态变量
运行时常量池中的常量(String 或 Class 类型)

在可达性分析算法中不可达的对象会被第一次标记,  
如果对象没有在finalize方法中重新与引用链建立关联关系的,将被第二次标记并回收,
建议在finalize方法中释放该对象持有的资源,不建议重写。该方法有且仅会被调用一次

强引用Strong Reference:new Object()强引用对象永远不会被垃圾回收,即使抛出OutOfMemoryError
软引用Soft:弱引用对象在抛出内存溢出异常前被二次回收,如果这次回收还没有足够内存才会抛出内存异常。由SoftReference类实现。可以和引用队列ReferenceQueue联合使用,如果软引用所引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中
弱引用Weak:弱引用对象没有强引用时会被垃圾回收。由Weakreference类实现、可以~
虚引用Phantom:用来跟踪对象被垃圾回收的活动。虚引用不影响对象存活,也不能通过虚引用获得对象实例。由PhantomReference类实现、必须~。程序如果发现虚引用已经被加入到引用队列,就可以在所引用的对象被回收前采取行动

为了控制对象被回收的时机,利用软引用和弱引用解决OOM问题。通过软引用和hashMap实现Java对象的高速缓存。如用HashMap保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收缓存图片对象所占用的空间

.6. 常用的垃圾回收算法?Java堆的内存分区?√为什么新生代不用标记整理?JVM垃圾回收机制?对象进入老年代时机?Minor/Young、Major/Old、Mixed、Full GC概念?触发条件?

标记-复制算法Copying:将可用内存按容量划分为大小相等的两块,每次只用一块分配内存,用完了就将存活的对象复制到另外一块,然后对这一块内存空间进行回收
不产生内存碎片,内存分配时只要移动堆顶指针,按顺序分配内存即可,效率高;但空间利用率低。适合只有少量对象存活的场景,新生代
标记-清除算法Mark-Sweep:标记存活对象,清除那些没有被标记的对象
会产生内存碎片,不利于分配大对象。适合对象不多的情况。标记和清除过程效率不高.老年代
标记-整理算法Mark-Compact:标记存活对象并向一端移动并清理掉端边界以外的内存
不产生内存碎片,需要移动对象,清除效率比标记清除低,老年代

根据对象生命周期的划分为
新生代Young Generation包括Eden、Survivor From和Survivor to空间,大小比例8:1:1(–XX:SurvivorRatio),用标记-复制算法,因为每次复制少量的存活对象 效率高
老年代Tenured Generation对象存活率高。用标记—清理或整理算法。新生代与老年代的比例为1:2 (–XX:NewRatio)
永久代Permanet Generation存储class,method,filed对象
Virtual区:最大内存和初始内存的差值

老年代收集Major/Old GC:CMS收集器特有
混合收集Mixed GC:G1垃圾收集器特有,同时清理年轻代和部分老年代
整堆收集Full GC:堆和方法区的垃圾收集。STW

heapmemoryallocation.png
采用分代收集算法(Generational Collection)
分配内存只在新生代Eden。Eden空间不足时触发Minor GC/Young GC,清除Eden和Survivor From区,仍然存活的对象会被转移到 To 区,Survivor两个区域在每次 GC 时交替使用。

对象在Survivor区中超过年龄阈值(默认15次-XX:MaxTenuringThreshold) Minor GC仍然存活,会晋升到老年代。
大对象直接分配到老年代。避免在Eden区和两个Survivor区之间发生大量的内存拷贝-XX:PretenureSizeThreshold
动态对象年龄判定。在survivor区中相同年龄的所有对象大小大于survivor空间的一半,则大于或等于该年龄的对象晋升老年代
老年代空间分配担保。Young GC后Survivor空间不足时,剩余存活对象进入老年代

Young GC前检查老年代:Young GC前老年代可用的连续内存空间<新生代历次Young GC后升入老年代的对象的平均大小,说明本次Young GC后可能升入老年代的对象大小可能超过老年代当前可用内存空间
Young GC后老年代空间不足:老年代没有足够的内存空间存放新生代对象GC年龄到达阈值需要晋升的对象
老年代空间不足,内存使用率过高
空间分配担保失败Promotion Failure,新生代的To区放不下从Eden和From拷贝过来对象
方法区内存空间不足:永久代空间不足
System.gc()、jmap -dump 等命令触发

.7. jvm垃圾回收器分类、jdk版本及作用?垃圾收集器选择(线上)及原因?jvm参数选用(线上)及原因 √

自动管理Java程序的运行时内存。负责识别可回收内存,并释放这些内存以便重新使用。减少了手动管理内存的负担,降低了内存泄漏和溢出的风险
STW(stop the world):所有工作线程暂停

分代收集器CMS和分区收集器G1和ZGC

Serial/Serial Old收集器,单线程,收集时会STW,适用于小内存(大约 100 MB)应用和没有停顿时间要求的单线程环境。

ParNew/Serial Old收集器,多线程Serial

Parallel Scavenge/Parallel Old收集器,多线程。关注吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)、峰值性能但不关注停顿时间或者可以接受 1 秒或更长的停顿时间,不适用延迟敏感.

CMS Concurrent Mark Sweep并发收集,低延迟,停顿时间短,标记清除导致内存碎片,并发清除阶段用户并发分配对象产生的浮动垃圾Floating Garbage下次垃圾收集才能处理。增加GC频率。适合中等堆内存且延迟敏感的应用,-XX:+UseConcMarkSweepGC
执行过程
初始标记Initial Mark:标记所有从GC Roots直接可达的对象,STW
并发标记Concurrent Mark:并发标记所有从GC Roots可达的对象。
重新标记Remark:重新扫描并标记在并发标记阶段由于用户线程新创建或变动的对象引用,会STW。
并发清除Concurrent Sweep:清除未被标记的对象,回收占用的内存空间

G1 Garbage-First Garbage Collector多线程,低暂停时间和高吞吐量,通过并发标记和分区式手机缩短STW时间。年轻代标记复制,老年代标记整理避免碎片,控制STW时间灵活回收Region,提高回收效率和减少停顿时间-XX:MaxGCPauseMillis。jdk9+默认
用于较大内存100MB~10GB中有效地控制暂停时间,适合大多数通用应用程序和硬件配置。对堆内存和CPU的消耗高,用于低延迟和高吞吐量平衡。需要控制停顿时间的场景
区域化管理使G1更灵活地垃圾收集:把堆划分为多个大小相等的区域Region,每个区域都能作为Eden、survivor、老年代,Humongous区存放超过分区容量50%的大对象
运行过程:
初始~ 并发~ 重新~
混合收集Mixed Collection,G1计算并回收最多垃圾的区域。包括部分新生代和老年代区域。
复制清理Compaction将存活对象复制到其他Region,释放旧内存,避免老年代内存碎片

ZGC Z Garbage Collector 用于超大内存100GB+,低延迟,停顿时间小于10ms,适用于特定的高性能应用场景。如大数据处理,关注响应时间。但它可能会消耗更多的CPU资源,特别是在较小的堆内存和多任务并行的场景
jvmzgc.png
运行过程:
并发转移:识别需要转移的对象以及它们的新内存位置。
并发标记:对堆中的对象进行标记,确定哪些对象是存活的。
通过读屏障跟踪对象引用更新,以便垃圾及时回收
通过指针染色技术,用64位指针的高4位第 42-45 位染色来记录对象存活信息。寄存器访问比访问内存速度更快
并发预备重分配(Concurrent Relocate Prepare):确定需要转移的对象和内存区域。
并发重分配(Concurrent Relocate):利用并发拷贝机制异步转移已标记的存活对象至新位置,回收旧内存区域。

堆设置
-Xms:初始堆大小 -Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
-Xmx:最大堆大小 -Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-XX:NewSize=n:设置年轻代初始大小
-XX:Xmn 设置年轻代大小
-XX:PermSize 初始化永久代大小
-XX:MaxPermSize=n:设置持久代大小
-XX:NewRatio=n:年轻代和年老代的比值。为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的 1/4
-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个Survivor区占整个年轻代的 1/5

收集器设置
-XX:+UseSerialGC serial新生代使用标记复制,Serial Old老年代标记整理
-XX:+UseParNewGC:ParNew新生代使用标记复制,Serial Old老年代标记整理。
-XX:+UseParallelGC 年轻代使用ParallelGC垃圾回收器标记复制,老年代使用串行回收器
-XX:+UseParalledlOldGC年轻代使用ParallelGC垃圾回收器标记复制,老年代使用ParallelOldGC垃圾回收器标记-整理
-XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
-XX:+UseG1GC
-XX:+UseZGC

并行收集器参数设置
-XX:ParallelCMSThreads=n 设置并行收集器收集时使用的 CPU 数。并行收集线程数
-XX:MaxGCPauseMillis=n:设置并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)默认值为99,也就是垃圾回收时间不能超过1%
-XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况
-XX:ParallelGCThreads=n:设置并发收集器年轻代手机方式为并行收集时,使用的 CPU 数。并行收集线程数,n的值与逻辑处理器的数量相同,最多8
-XX:ConcGCThreads=n设置并行标记的线程数。将n设置为并行垃圾回收线程数 (ParallelGCThreads)的 1/4 左右
-XX:UseAdaptiveSizePolicy自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡
-XX:G1HeapRegionSize=n设置G1区域的大小。值是2的幂,1-32MB。目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000

垃圾回收统计信息 (https://gceasy.ioopen in new window
-XX:+PrintGC:开启打印gc信息
-XX:+PrintGCDetails:打印 gc 详细信息
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013‐05‐04T21:53:59.234+0800)
-Xloggc:filename ../logs/gc.log 日志文件的输出路径(GC Easy工具可以直接选择日志文件查看)
‐XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

设置系统属性参数
-D<名称>=<值> 可通过System.getProperty("名称");获得
查看正在运行的java进程所有参数jinfo ‐flags pid
查看正在运行的java进程具体参数jinfo ‐flag MaxHeapSize pid
完整命令ps -ef |grep java |grep -w x.jar|grep -v 'grep'|awk '{print $2}'| xargs -i{} jinfo ‐flags {}

jdk8默认收集器java -XX:+PrintCommandLineFlags -version查看->-XX:+UseParallelGC表示的是新生代用的Parallel Scavenge收集器,老年代用的是Parallel Old 收集器。
采用Parallel Scavenge + Parallel Old的组合。系统业务相对复杂,但并发并不是非常高,希望尽可能利用处理器资源,出于提高吞吐量的考虑
采用Parallel New+CMS的组合,我们比较关注服务的响应速度,所以采用了 CMS 来降低停顿时间。
采用 G1 垃圾收集器,因为它不仅满足我们低停顿的要求,而且解决了 CMS 的浮动垃圾问题、内存碎片问题

最大暂停时间目标G1 GC将尝试在此目标内完成垃圾回收。:-XX:MaxGCPauseMillis=<N>设置为200ms
初始和最大堆大小:最小为最大内存的一半,最大为最大内存-Xms4G -Xmx8G
并发GC线程数(CPU核心数的一半):-XX:ConcGCThreads=2
并行GC线程数(与CPU核心数相同):-XX:ParallelGCThreads=4
堆区域大小:-XX:G1HeapRegionSize=16M

java -Xms4G -Xmx8G -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16M -XX:ConcGCThreads=2 -XX:ParallelGCThreads=4 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1MixedGCCountTarget=8 -XX:G1ReservePercent=10 -XX:G1MixedGCLiveThresholdPercent=85 -jar your-application.jar

.8. OopMap、安全点概念?对象一定分配在堆中吗?逃逸分析技术?好处?解释执行和编译执解释和编译的区别

用于记录在特定位置(如安全点)时,栈帧中存储对象引用Reference和原始数据(int)的位置的数据结构。垃圾回收时正确标记和回收内存中的对象。在类加载和即时编译时被创建

Java线程执行过程中能够安全暂停线程进行垃圾回收的位置。JVM在所有线程到达安全点时才触发GC。如果线程分配对象时对象的地址还在寄存器中,线程失去了CPU时间片,而此时STW GC发现没有任何GC ROOTS与该对象关联起来,被认为是垃圾并被回收了,之后CPU重新获得时间片后发现此时对象已经不存在了,程序出错。SafePoint位置包括:循环的末尾 (防止大循环的时其他线程在等待它进入Safepoint)、方法返回前、调用方法的Call之后、抛出异常的位置

不一定。JVM在JIT(即时编译)中用逃逸分析用来确定对象是否会逃逸出方法或线程的范围。判断该对象在堆或者栈上分配,分类如下
方法逃逸:对象在创建后被传递到其他方法调用中,或者作为返回值返回给调用者
线程逃逸:对象被其他线程访问(例如存储在共享的静态变量、全局变量或成员变量中)
以上都只能在堆中分配
jvmescape.png

栈上分配:对象不会线程逃逸则在栈上分配,对象占用的内存随着栈帧出栈而销毁,降低垃圾收集的压力,减少内存堆分配压力,提高性能
同步消除:变量不会线程逃逸则变量的读写不会有竞争,变量的同步可以消除
标量替换:标量替换把对象的成员变量改为基本数据类型来访问。对象不会逃逸且能标量替换,则让对象的成员变量在栈上分配和读写

解释/编译:将源代码逐行/一次性转换为机器码
Java解释型语言,因为Java编译器先将源代码编译成字节码,在运行时由JVM的解释器逐行将字节码转换为机器码然后执行。也是Java慢的原因
JIT会将热点代码编译后放入CodeCache,当下次执行再遇到则从CodeCache中直接读取机器码执行。提升执行效率
jvmexplain.png

.9. 类的加载机制?过程/类加载器是如何加载Class文件?类的生命周期吗?jvm类加载器?双亲委派模型?作用?怎么自定义加载?Tomcat类加载机制?

JVM把Class文件中描述类的数据结构加载到内存中并进行校验、解析和初始化的过程
processofloadingclass.png
类加载过程:顺序执行,但在动态绑定的情况下,解析阶段会发生在初始化阶段之后
加载Loading,类加载器通过类的全限定名来获取class文件的二进制字节流并转化为java.lang.Class对象,同时缓存class对象避免重复加载
连接Linking将类的二进制数据合并到JVM
验证Verification:对二进制字节流进行校验,确保.class文件符合JVM要求
准备Preparation:对类变量(静态变量,static修饰的变量)分配内存并初始化为数据类型的默认值
解析Resolution:将常量池内的符号引用替换为直接引用的过程。主要针对类或接口、字段、类方法、接口方法、成员方法等
初始化Initialization:类变量将被赋值。执行类的静态代码块(javap中的 <clinit>() 方法)

BootstrapLoader:启动类加载器,负责加载JVM的核心类库,如rt.jar和位于JAVA_HOME/jre/lib目录下的类。无法被Java程序直接引用
ExtClassLoader:扩展类加载器 加载扩展类(继承类和实现类),加载\lib\ext目录或者被 java.ext.dirs系统变量指定的路径中的所有类库
AppClassLoader:加载来Java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义类加载器
用户自定义类加载器User-Defined ClassLoader,用于加载网络上的类、执行热部署(动态加载和替换应用程序的组件)或为了安全

双亲委派模型Parent Delegation Mode:ClassLoader会首先检查已加载的类缓存,如果找到则直接返回。如果找不到则委托父类加载器加载,直到bootstrap ClassLoader。当所有父类加载器都未加载时,才会加载并将类放入缓存中,以便下次直接返回

保证Java程序的稳定运行,避免类重复加载(JVM通过类名和加载类的类加载器区分类),
保证Java的核心API不被篡改。网络传递java.lang.Integer类并不会重新加载

代码实现:在ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法加载
自定义类加载器继承ClassLoader类并重写loadClass()、findClass()方法

Tomcat中不同应用程序可能会依赖同一个类库不同版本的同名类。双亲委派机制无法加载~
Tomcat为每个web容器提供WebAppClassLoader类加载器,负责加载本身的目录下的class文件,加载不到时由CommonClassLoader加载,和双亲委派相反