一、运行时数据区域
程序计数器
记录正在执行的Java虚拟机字节码指令的地址(如果正在执行的是本地方法,则为空)。
Java虚拟机栈
每个Java方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作符栈、常量池引用等信息。从方法调用直至执行完成的过程,就是栈帧在Java虚拟机栈从入栈到出栈的过程。
可以通过-Xss
这个Java虚拟机参数来指定每个线程的Java虚拟机栈内存大小:
java -Xss2M HackTheJava
该区域可能会抛出以下异常:
- 线程请求的栈的深度超过最大值时,会抛出StackOverflowError异常;
- 栈进行动态扩展时,如果无法申请到足够的内存,会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈和Java虚拟机栈类似,它们之间的区别只不过是本地方法栈是为本地方法服务的。
本地方法一般是用其他语言(C,C++,汇编语言等)编写的,并且被编译成基于机器硬件和操作系统的程序,对待这些方法需要特殊处理。
堆
所有的对象都在这里分配内存,是垃圾回收的主要区域(GC堆)。
现代的垃圾收集器基本上都是采用的分代收集算法。其主要思想是针对不同类型的对象采用不同的垃圾回收算法,可以将堆分成两部分:
- 新生代
- 老年代
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError异常。
可以通过-Xms
和-Xmx
这两个Java虚拟机参数指定堆的内存大小,第一个参数设置初始值,第二个参数设置最大值:
java -Xms1M -Xmx2M HackTheJava
方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样,方法区不需要连续内存,可以动态扩展,扩展失败会抛出OutOfMemoryError异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和类的卸载,但是一般比较难实现。
HotSpot虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,它受很多因素的影响,而且每次Full GC之后,永久代的大小都会被改变,所以经常会抛出OutOfMemoryError异常。在JDK1.8以后,永久代被移除,方法区被移至元空间,它在本地内存中,而不是虚拟机内存中。
方法区是JVM的一个规范,永久代和元空间都是一种实现方式。在JDK1.8以后,原来永久代的数据被分到堆和元空间中,元空间存放类的元信息,静态变量和常量池存放到堆中。
运行时常量池
运行时常量池是方法区的一部分。
class文件的常量池(编译期生成的字面量和字符引用等)在类加载后放在这个区域。
除了编译期生成的常量,也允许动态生成,例如String类的intern()。
直接内存
在JDK1.4后,新引入了NIO类,可以使用Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著增强性能,因为避免了在堆内存和堆外内存来回拷贝数据。
二、垃圾收集
垃圾收集主要是在堆和方法区。程序计数器、Java虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在与线程的生命周期内,线程结束后就会消失,不需要进行垃圾回收。
判断一个对象是否可被回收
1. 引用计数算法
为对象增加一个引用计数器,当对象增加一个引用时,计数器加1;引用失效时,计数器减1;当引用计数为0的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。正是因为循环引用的存在,因此Java虚拟机不使用引用计数算法。
2. 可达性分析法
以GC Roots为起点进行搜索,可达的对象都是存活的,不可达的都是可被回收的。
Java虚拟机一般采用这种算法来判断一个对象是否可被回收。GC Roots一般包含以下内容:
- Java虚拟机栈中局部变量表中引用的对象
- 本地方法栈中JNI中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
3. 方法区的回收
方法区主要存放永久代对象,而永久代对象的回收效率比新生代低很多,所以对方法区的回收性价比不高。
主要是对常量池的回收和类的卸载。
为了避免内存的溢出,在大量使用反射和动态代理的场景中都要求虚拟机具备类卸载的功能。
类的卸载条件很多,需要满足以下三个条件,而且即使满足了这些条件也不一定会被卸载:
- 该类的所有实例已被回收,此时堆中已不存在该类的任何实例;
- 加载该类的ClassLoader已被回收;
- 该类的class对象在任何地方都没有被引用,在任何地方都不能通过反射访问该类方法;
4. finalize()
和C++的析构函数类似,用于关闭外部资源。但是try ... finally
可以完成的更好,而且使用finalize()
的运行代价很高,不确定性很大,无法保证各个对象的调用顺序,因此最好不要使用。
引用类型
1. 强引用
被强引用关联的对象不会被回收。
用new
来创建强引用:
Object obj = new Object();
2. 软引用
被软引用关联的对象只在内存不够的时候会被回收。
用SoftReference
类创建软引用:
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 对象只被软引用关联
3.弱引用
被弱引用关联的对象一定会被回收,也就是说它只能活到下一次垃圾回收发生之前。
用WeakReference
类创建弱引用:
Object obj = new Object();
WeakRefernce<Object> wf = new WeakReference<Object>(obj);
obj = null;
4.虚引用
又称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用获取一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
用PhantomReference
来创建虚引用:
Object obj = new Object();
PhantomReference<Object> pr = new PhantomReference<Object>();
垃圾收集算法
1. 标记-清除
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收和取消标记,另外,会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为“空闲链表”的单向链表中,之后进行分配时,只需要遍历这个空闲链表,就可找到这个分块。
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小size
的分块block
。如果他找到的分块等于size
,会直接返回这个分块;如果找到的分块大于size
,会将块分割为大小为size
与(block - size)
两部分,返回大小为size
的分块,并把大小为(block - size)
的分块返回个空闲链表。
不足:
- 标记和清除的过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2. 标记-整理
让所有存活的对象都移向一端,然后直接清理掉端边界以外的内存。
优点:
- 不会产生内存碎片
缺点:
- 需要移动大量对象,处理效率低
3. 复制
将内存分为大小相等的两块,每次只使用其中一块,当这块内存用完了就将还存活的对象复制到另一块上面,然后对使用过的内存空间进行一次清理。
现在的商业虚拟机都是采用的这种收集算法回收新生代,但是不是划分为大小相等的两块,而是一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。在回收时,将Eden和Survivor中还存活的对象全部复制到另一块Survivor上,最后清理Eden和使用过的那一块Survivor。
HotSpot虚拟机 的Eden和Survivor大小比例默认为8:1
,保证了内存的利用率达到90%。如果每次回收有多于10%的对象存活,那么一块Survivor就不够用了,这时就要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
4. 分代收集
现代的商业虚拟机采用的是分代收集算法,根据对象的存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆划分为新生代和老年代:
- 新生代:复制算法
- 老年代:标记-清除 或 标记-整理 算法
垃圾收集器
以上是HotSpot虚拟机的7个垃圾收集器,连线表示垃圾收集器可以配合使用;
- 单线程和多线程:单线程指的是垃圾收集器只使用一个线程 ,而多线程是使用多个线程
- 串行和并行:串行指的是垃圾收集器和用户程序交替执行,这意味着垃圾收集器在执行的时候会停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了G1和CMS,其他垃圾收集器都以串行的方式执行。
1. Serial收集器
Serial的翻译是串行,也就是说它是以串行的方式执行。
它是单线程收集器,只使用一个线程进行垃圾收集。
它的优点是简单高效,在单个CPU的环境下,由于没有线程交互的开销,拥有最高的单线程收集效率。
Serial是Client场景下默认的新生代收集器,因为在这种场景下,内存一般不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
2. ParNew收集器
它是Serial收集器的多线程版本。
它是Server场景下默认的新生代收集器,除了性能原因外,主要是因为除了Serial收集器,只有它能和CMS收集器配合使用
3. Parallel Scavenge收集器
与ParNew一样都是多线程收集器。
其他的收集器目标都是尽可能缩短垃圾收集时,用户程序的顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”的收集器。这里的吞吐量是指CPU运行用户时间占总时间的比值。
停顿时间越短越适合需要与用户交互的程序,良好的响应时间能提升用户体验。高吞吐量可以高效率地利用CPU时间,尽快完成运算任务,适合在后台运算而不需要太多与用户交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间为代价的:新生代空间变小,垃圾回收变得频繁,吞吐量降低。
可以通过一个开关参数打开GC的自适应调节策略,这样就不需要手动调节新生代大小、Eden和Survivor区的比例、晋升老年代的对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4. Serial Old收集器
是Serial收集器的老年代版本,也是给Client场景下的虚拟机使用。如果是Server场景下,它有两大用途:
- 在JDK1.5以及之前的版本(Parallel Old诞生之前),与Parallel收集器配合使用;
- 作为CMS的后备预案,在并发收集发生
Concurrent Mode Failure
使用。
5. Parallel Old收集器
是Parallel收集器的老年代版本。
在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
6. CMS收集器
CMS全称Concurrent Mark Sweep,标记-清除算法。
主要包含以下四个流程:
- 初始标记:仅仅是标记一下GC Roots所关联的对象,速度很快,需要停顿;
- 并发标记:进行GC Roots Tracing的过程,它在回收过程中耗时最长,不需要停顿;
- 重新标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿;
- 并发清除:不需要停顿。
在整个回收过程中耗时最长的并发标记和并发清除过程中,收集器线程可以与用户线程一起工作,不需要停顿。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导师CPU利用率不够高;
- 无法处理浮动垃圾,可能会出现
Concurrent Mode Failuer
:浮动垃圾是指并发清除阶段因用户程序继续运作而产生的垃圾,这部分垃圾只能等到下一次GC才能进行回收。由于浮动垃圾的存在,必须预留一部分内存用于存放浮动垃圾,意味着CMS收集不能像其他收集器那样等到老年代快满了再进行回收。如果预留的内存不足以存放浮动垃圾,就会出现Concurrent Mode Failure
,这时虚拟机将临时启用Serial Old代替CMS。 - 标记-清除算法导致空间碎片,往往出现老年代空间剩余,但无法找到足够大的连续空间来分配给当前对象,不得不提前出发一次Full GC。
7. G1 收集器
G1(Garbage First),它是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是在未来替代CMS收集器。
堆被分为新生代和老年代,其他的收集器进行收集的范围都是整个新生代或者老年代,而G1可以直接对新生代和老年代一起回收。
G1把堆划分为多个大小相等的独立区域(Region),新生代和老年代不在物理隔离。
通过引入Region的概念,可以将原来的一整块内存空间划分为多个的小空间,使得每个小空间都可以单独地进行垃圾回收,使得可预测的停顿时间模型成为可能。通过记录每个Region的回收时间和回收获得的空间(这两个值通过过去的会后经验获得),维护一张优先列表,每次根据允许的回收时间,优先回收价值最大的Region。
每个Region都有一个Remembered Set,记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析时可以避免全堆扫描。
如果不计算Remembered Set的操作,G1收集器的运作大致可分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续执行而导致标记变动的那部分对象的标记记录,虚拟机将这段时间的对象变化记录在线程的Remembered Set Logs里,在最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。这阶段需要停顿线程,但是可以并行执行。
- 筛选回收:首先对各个Region中回收的价值和成本进行排序,然后根据用户所期望的GC停顿时间来指定回收计划。此阶段其实也可以做到与用户程序并发执行,但是只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备以下特点:
- 空间整合:整体上看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)看是基于“复制”算法实现的,意味着不会产生空间碎片;
- 可预测的停顿:让使用者明确指定在一段长度为M毫秒的时间片段里,消耗在GC的时间不能超过N毫秒。
三、内存分配和回收策略
Minor GC和Full GC
- Minor GC:回收新生代,由于新生代对象的存活时间很短,因此Minor GC会频繁执行,执行的速度一般也会比较快;
- Full GC:回收新生代和老年代,老年代存活时间较长,因此Full GC很少执行,执行速度会比Minor GC慢很多。
内存分配策略
1. 对象优先在Eden分配
一般来说,对象在新生代Eden进行分配,当Eden空间不够时,发起Minor GC。
2. 大对象直接进入老年代
大对象是指需要连续空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold
,大于此值的对象直接在老年代分配,避免在Eden和Survivor之间的大量内存复制。
3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在Eden出生经过Minor GC依然存活,将移动到Survivor中,年龄将加1,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold
用来定义年龄的阈值。
4. 动态对象年龄分配
虚拟机并不是永远要求对象的年龄必须达到MaxTensuringThreshold
才能晋升老年代,如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于改年龄的对象可以直接进入老年代,无需等到MaxTensuringThreshold
中要求的年龄。
5. 空间分配担保
在Minor GC之前,虚拟机检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么Minor GC就可以确认是安全的。
如果条件不成立,虚拟机会查看HandlePromotionFailure
的值是否允许担保失败,如果允许就会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试进行一次Minor GC;如果小于,或者HandlePromotionFailure
的值不允许冒险,那么就要进行一次Full GC。
Full GC的触发条件
对于Minor GC,其触发条件非常简单,当Eden空间满时,就将触发一次Minor GC。而Full FC则相对复杂,有以下条件:
1. 调用System.GC()
只是建议让虚拟机执行Full GC,但是虚拟机不一定真正去执行。不建议这种方式,而是让虚拟机管理内存。
2. 老年代空间不足
老年代空间不足的常见场景为前文所见的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过-Xmn
虚拟机参数调大新生代大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过
-XX:MaxTensuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3. 空间分配担保失败
使用复制算法的Minor GC会使用老年代的空间做担保,如果担保失败会执行一次Full GC。
4. JDK1.7以前永久代空间不足
在JDK1.7以前,HotSpot虚拟机的方法区是以永久代实现的,永久代中存放的为一些Class的信息、常量、静态变量等数据。
当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError。
为了避免以上原因引起的Full GC,可采用的方法为增大永久代空间或转为使用CMS GC。
5. Concurrent Mode Failure
执行CMS GC的过程中同时有对象放入老年代,而老年代的空间不足(可能是GC过程中产生的浮动垃圾过多而导致暂时性的空间不足),便会报Concurrent Mode Failure错误,并触发Full GC。
四、类加载机制
类是运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
类的生命周期
类的加载过程
包含了加载、验证、准备、解析和初始化这5个阶段。
1. 加载
加载是类加载的一个阶段。
加载过程完成以下三件事:
- 根据类的完全限定名称获取定义该类的二进制字节流;
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构;
- 在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口。
其中二进制字节流可以从以下方式获取:
- 从ZIP包读取,成为JAR、EAR、WAR格式的基础;
- 从网络中获取,最典型的应用是Applet。
- 运行时计算生成,例如动态代理技术,在java.lang.Proxy中使用ProxyGenerator.generateProxyClass的代理类的二进制的字节流。
- 有其他文件生成,例如有JSP文件生成对应的Class类。
2. 验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3. 准备
类变量是被static
修饰的变量,在准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,他会在对象实例化时随着对象译器被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为0值。
如果类变量是常量(final
),那么它将初始化为表达式所定义的值而不是0。
4. 解析
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下,可以在初始化之后再开始,只是为了支持Java的动态绑定。
5. 初始化
初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段是虚拟机执行类构造器<clinit>()
方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
<clinit>()
是由编译器自动收集类中所有变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
由于父类中的<clinit>()
方法先执行,也就意味着父类中定义的静态语句块的执行要先于子类。
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()
方法。但接口与类不同的是,执行接口的<clinit>()
不需要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时,也不会执行接口的<clinit>()
方法。
虚拟机会保证一个类的<clinit>()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的<clinit>()
方法,其他线程都会阻塞等待,直到活动线程执行<clinit>()
方法完毕。如果在一个类的<clinit>()
方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
类初始化时机
1. 主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
- 遇到
new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,如果没有进行过初始化,则必须先触发其初始化。最常见的生成这4条指令的场景是:使用new
关键字实例化对象的时候,读取或设置一个类的静态字段(被final
修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态字段的时候。 - 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先出发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含
main()
方法的那个类),虚拟机会先初始化这个主类; - 当使用JDK1.7的动态语言支持时,如果一个
java.lang.invoke.MethodHanle
实例最后的解析结果为REF_getStatic
,REF_putStatic
,REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。
2. 被动引用
以上5种场景中的行为被称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,成为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类的初始化。
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object的子类,其中包含了数组的属性和方法。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类与类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果为true
,也包括使用instaceof
关键字做对象所属关系判定结果为true
。
类加载器分类
从Java虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分;
- 所有其它类的加载器,使用Java实现,独立于虚拟机,继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器(Bootstrap ClassLoader)此类加载器负载将存放在
<JRE_HOME>\lib
目录中的,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar
,名字不符合的类库即使放在lib
目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用null
即可。 - 扩展类加载器(Extension ClassLoader)这个类加载器是由
ExtClassLoader (sun.misc.Launcher$ExtClassLoader)
实现的。它负责将<JAVA_HOME>/lib/ext
或者被java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader)这个类加载器是由AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是ClassLoader中的
getSystemClassLoader()
方法的返回值,因此一般称为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
下图展示了类加载器之间的层次关系,称为双亲委派模型。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系来实现,而不是继承关系。
1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
2. 好处
使得Java类随着它的类加载器译器具有一种带有优先级的层次关系,从而使得基础类得到统一。
自定义类加载器实现
实例中的FileSystemClassLoader
是自定义类加载器,继承自java.lang.ClassLoader
,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class
文件),最后通过defineClass()
方法来把这些字节代码转换成java.lang.Class
类的实例。
java.lang.ClassLoader
的loadClass()
实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写findClass()
方法。