2025-05-27-Tue-T-JVM

-
内存与垃圾回收篇
-
字节码与类的加载篇 ✈
-
性能监控与调优篇
-
大厂面试篇
1 JVM与Java体系结构

JVM根本不关心在其内部的程序是由什么语言编写的, 它只关心字节码文件
JVM特点:
- 自动内存管理
- 自动垃圾回收功能
1.1 JVM 结构简图
终于搞懂了Java8的内存结构,再也不纠结方法区和常量池了!-腾讯云开发者社区-腾讯云


1.2 Java代码执行流程

1.3 JVM的架构模型
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构
- 基于栈的指令集:适合跨平台、安全性高的场景(如 JVM),但效率较低。
- 基于寄存器的指令集:适合高性能本地执行(如 CPU 指令),但依赖硬件。
- JVM 的权衡:通过栈架构实现跨平台,再通过 JIT 编译优化为寄存器指令,兼顾灵活性和性能。
最终结论:
Java 选择栈架构是为了跨平台和安全性,而现代 JVM 通过运行时优化(JIT)弥补了性能差距。
执行反编译
PS D:\Users\fei\gitrepo\gulimall\java-foundation\target\classes\com\JVM\chapter01> javap -v .\StackStruTest.class
Classfile /D:/Users/fei/gitrepo/gulimall/java-foundation/target/classes/com/JVM/chapter01/StackStruTest.class
Last modified 2025年5月27日; size 442 bytes
SHA-256 checksum 675d068f509639e5a688e3ca50cbc237c8459f36e78347be553c9e8f98f736ac
Compiled from "StackStruTest.java"
public class com.JVM.chapter01.StackStruTest
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // com/JVM/chapter01/StackStruTest
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // com/JVM/chapter01/StackStruTest
#8 = Utf8 com/JVM/chapter01/StackStruTest
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/JVM/chapter01/StackStruTest;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 i
#19 = Utf8 I
#20 = Utf8 SourceFile
#21 = Utf8 StackStruTest.java
{
public com.JVM.chapter01.StackStruTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/JVM/chapter01/StackStruTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_5
1: istore_1
2: return
LineNumberTable:
line 5: 0
line 7: 2
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 args [Ljava/lang/String;
2 1 1 i I
}
SourceFile: "StackStruTest.java"
由于跨平台的特性, Java的指令都是根据栈来设计的
栈型指令系统: 跨平台性, 指令集小, 指令多; 执行性能比寄存器差
1.4 JVM的生命周期
- 虚拟机的启动
- 虚拟机的执行
- 虚拟机的退出

一、内存结构
1. 程序计数器
Program Counter Register 程序计数器(寄存器)
作用: 记住下一条JVM指令的执行地址
特点:
- 线程私有的: 时间片结束, 依然可以获取本线程的程序计数器. 每个线程有自己的程序计数器
- 不会存在内存溢出: JVM规范中规定了不允许程序计数器出现内存溢出
2. 虚拟机栈
2.1 定义
栈: 线程运行时需要的内存空间
栈帧: 每个栈由多个栈帧组成, 栈帧是每个方法运行需要的内存空间
每个线程只能有一个活动栈帧, 对应着当前正在执行的方法
问题辨析:
- 垃圾回收是否涉及栈内存? 不涉及, 栈内存中只有与方法调用相关的栈帧内存,栈帧内存在每次方法调用结束后会自动释放. 垃圾回收用于管理堆内存
- 栈内存分配越大越好吗? (Xss用于指定栈内存大小) 栈内存越大, 总内存不变, 所运行的线程就会变少
- 方法内的局部变量是否线程安全? 逃逸分析
- 如果局部变量没有逃离该方法, 则是线程安全的
2.2 栈内存溢出
- 栈帧过多导致栈内存溢出: 栈帧的内存超过了线程所分配的栈内存 (例如 错误的递归调用)
- 栈帧过大导致栈内存溢出:
2.3 线程运行诊断
案例1: CPU占用过多
# 1. 用top定位哪个进程对cpu的占用过高
top
# 2. 用ps定位哪个线程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id # ps用于查询进程线程信息
# 3. jstack: 根据线程id查看有问题的线程
jstack 进程id
# 4. 根据提示信息找到对应的代码行数
案例2: 程序运行很长时间没有结果
# 根据jstack提示信息找到死锁对应的代码行数
jstack 进程id
3. 本地方法栈

本地方式是只操作系统的原生方法, Java代码通过调用这些方法与cpu和内存打交道
在Java代码中, 使用native关键字标识本地方法
本地方法栈: 用于运行本地方法的栈
4. 堆
线程共享区域
4.1 定义
Heap(堆)通过new关键字, 创建对象都会使用堆内存
特点:
- 线程共享, 堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出
案例
public class HeapTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
int i = 0;
a = "hello"
try {
while (true) {
list.add(a);
a = a + a
i++;
}
}catch (Throwable e) {
System.out.println(i);
e.printStackTrace();
}
}
}
自定已堆空间: -Xmx8m: 修改堆空间为8M
4.3 堆内存诊断
1)jps工具
查看当前系统中有哪些Java工程,以及对应的进程id
jps [options] [hostid]
-q 不输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数
2)jmap工具
查看堆内存占用情况
jmap -heap pid 或 jhsdb jmap --heap --pid 41416
jmap [option] pid
jmap [option] executable core
jmap [option] [server-id@]remote-hostname-or-ip
很常用的情况是:用jmap把进程内存使用情况dump到文件中,再用jhat分析查看。jmap进行dump命令格式如下:
jmap -dump:format=b,file=dumpFileName 进程id
# 例子
root@ubuntu:/# jmap -dump:format=b,file=/tmp/dump.dat 21711
Dumping heap to /tmp/dump.dat ...
Heap dump file created
3)jconsole工具
图形界面的,多功能的监测工具,可以连续监测
4)jvisualvm
5) jstack
jstack主要用来查看某个Java进程内的线程堆栈信息。语法格式如下:
jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip
-l long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况
-m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)
案例1: 诊断工具使用
public class HeapDemo2 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1. 堆内存测试");
Thread.sleep(30000);
byte[] arr = new byte[1024 * 1024 * 10]; // 10MB
System.out.println("2. 10MB创建成功");
Thread.sleep(30000);
arr = null;
System.gc();
System.out.println("3. 垃圾回收");
Thread.sleep(30000);
}
}
案例2: 垃圾回收后,内存占用依然很高
jps --> jmap --> jvisualvm
5. 方法区


5.1 定义
存储类的
逻辑上是堆的组成部分
方法区(Method Area) 是 Java 虚拟机(JVM) 运行时数据区的一部分,用于存储 类信息、常量、静态变量、即时编译器编译后的代码 等数据。它是所有线程共享的内存区域,在 JVM 启动时创建,逻辑上属于 堆(Heap)的一部分,但具体实现可能不同
5.2 组成

5.3 方法区内存溢出
- Java1.8以前会导致永久代内存溢出
- 1.8之后会导致元空间内存溢出
演示元空间内存溢出
/**
* 演示元空间内存溢出
* -XX:MaxMetaspaceSize=8m
*/
public class MetaSpaceOutOfMemory extends ClassLoader{
public static void main(String[] args) {
int j = 0;
try {
MetaSpaceOutOfMemory m = new MetaSpaceOutOfMemory();
for (int i = 0; i < 10000; i++,j++) {
//ClassWriter是用于生成 类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
// 版本号, public,类名,包名,父类,实现的接口
classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class" + i,null,"java/lang/Object",null);
//返回二进制字节码
byte[] byteArray = classWriter.toByteArray();
//执行类的加载
m.defineClass("Class" + i, byteArray, 0, byteArray.length);
}
}finally {
System.out.println(j);
}
}
}
场景:
- spring
- mybatis
5.4 运行时常量池

Java程序需要运行, 就需要先转化为二进制的字节码文件, 而字节码由类的基本信息、类的常量池、类的方法定义三部分以及虚拟机指令组成。
案例:使用javap -c 类名.class反编译字节码文件
//原始代码
public class Pool {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
// javap -v Pool.class反编译后的代码
// 1. 类的基本信息
Classfile /D:/Users/fei/gitrepo/gulimall/java-foundation/target/classes/com/JVM/chapter01/Pool.class
Last modified 2025年5月31日; size 551 bytes
SHA-256 checksum 685b230badb0ecfbb0269e956bbb39aa205913c435f86d19c86a7f40d34224a3
Compiled from "Pool.java"
public class com.JVM.chapter01.Pool
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // com/JVM/chapter01/Pool
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
//2. 常量池
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello World
#14 = Utf8 Hello World
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // com/JVM/chapter01/Pool
#22 = Utf8 com/JVM/chapter01/Pool
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcom/JVM/chapter01/Pool;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 SourceFile
#33 = Utf8 Pool.java
// 3. 类的方法定义
{
public com.JVM.chapter01.Pool(); // 构造方法
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello World
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Pool.java"
-
常量池就是一张表,虚拟机指令根据这些常量表找到要执行的类名,方法名,参数类型、字面量(值)等信息
-
运行时常量池,常量池是
*.class文件中的,当该类被加载,它的常量池信息就会被放入到运行时常量池,并把里面的符号地址变为真正的内存地址
public class StringTable {
/**
* 常量池中的信息都会被加载到运行时常量池, 这时候 a, b , ab都是常量池中的符号,还没有转化为Java字符串对象
* 0: ldc #7 会把a转化为 "a" 字符串对象, 然后在StringTable中找,是否有相同的字符串。 StringTable在数据结构上时Hash表,无法扩容
* astore_1 没有找到,将 "a" 存储到StringTable
*/
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new String("ab"); // s4在堆中,因为 s1, s2为变量,编译时不能确定s4的值.
String s5 = "a" + "b"; // javac 在编译期间的优化, a, b为常量, 编译时能确定s5就是ab
String s6 = s4.intern(); // s4在StringTable中没有"ab"时会自动放入串池,反之则否
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
}
}

5.5 StringTable
-
常量池中的字符串仅仅是符号,第一次用到时才变为对象。
-
利用串池(stringTable)的机制, 避免重复创建相同的字符串
-
字符串的拼接原理时StringBuilder(1.8)
-
字符串拼接原理时编译期优化
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
s4.intren()
--XX:UseGCOverheadLimit阻止应用长时间无响应(大于98%的时间花在了垃圾回收 and 小于2%堆空间被恢复)
5.6 StringTable 垃圾回收
- 演示StringTable垃圾回收
//-Xmx10m 堆内存10m
//-XX:+PrintStringTableStatistics 打印串池中统计信息
//-XX:+PrintGCDetails -verbose:gc 打印垃圾回收细节
public class StringTableGC {
public static void main(String[] args) {
int i = 0;
try {
for(int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
i++;
}
}catch (Throwable e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
5.7 StringTable 性能调优
实际就是调整StringTable中哈希表的桶的个数
// -XX:StringTableSize=20000
// java17 自动扩容为2的阶乘
在进行大量字符串操作时,可以将字符串放入StringTable,减少堆内存的使用。因为StringTable中不会存在相同的字符串。
6. 直接内存
6.1 定义
直接内存是操作系统内存。
- 常见于NIO操作,用于数据缓存区
Java NIO(New Input/Output 或 Non-blocking I/O)是从Java 1.4版本开始引入的一种新的I/O系统,它解决了传统的I/O系统中同步阻塞的问题,提供了更高效的I/O处理方法。Java NIO不仅可以提高性能,还可以提供更好的可伸缩性。
在Java NIO中,数据总是从通道读入缓冲区,或者从缓冲区写入通道。Selector会不断地轮询注册在其上的通道,当发现某个通道有I/O事件时,就能够快速定位到可以进行I/O操作的通道,进行数据处理。
- Java进行分配和回收成本比较高,但读写性能高
- 不受JVM内存回收管理
- 使用Java缓冲区(堆内存中)
new byte[1024*1024]

- 分配直接内存:
ByteBuffer.allocateDirect(1024*1024)

6.2 直接内存释放
-
通过Unsafe对象进行管理(Unsafe对象只能通过反射创建)。主动调用freeMemory方法
-
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一但ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
参数介绍
-XX:+DisableExplicitGC 禁用显示调用垃圾回收System.gc();
使用此参数后,则分配直接内存后无法显示直接释放
ByteBuffer buffer = ByteBuffer.allocateDirect(1024*1024);// 10MB
// 以下操作无效
buffer = null;
System.gc();
//可以使用Unsafe对象对直接内存进行释放
二、垃圾回收
1. 如何判断对象可以回收
1.1 引用计数法
定义一个count来记录某个对象被引用了多少次
可能出现循环引用的问题
1.2 可达性分析法
JVM采用的本方法。
-
确定根对象:一定不能被垃圾回收的对象叫根对象
- 是否是基本类对象
- 是否是加锁对象(synchronized)
- 是否在活动栈帧中被引用
-
JVM种的垃圾回收器采用可达性分析来探索所有存活的对象
-
扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
1.3 四种引用
强引用
byte[] b = new byte[1024]; //强引用
软引用
//1. 直接用法
SoftReference<byte[]> ref = new SoftReference<>(new byte[1024]); // 软引用(java.lang.ref.SoftReference)
//2. 使用引用队列释放软引用
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
SoftReference<byte[]> ref = new SoftReference<>(new byte[1024],queue); // byte[]回收时,ref被加入queue
queue.poll();
弱引用 Full GC清理
WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]); // 弱引用
// 也可使用RefrenceQueue来对弱引用本身进行释放
// Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable.
虚引用:
终结器引用

2. 垃圾回收算法
2.1 标记清除 Mark Sweep
- 通过标记需要清除的内存地址,进行逻辑清除
- 速度快
- 容易产生内存碎片
2.2 标记整理 Mark Compact
- 先
标记需要清除的对象地址 - 再将其余对象地址进行
整理,让已分配内存更紧凑,减少内存碎片 - 没有内存碎片
2.3 复制 Copy
- 将内存区划分为大小相等的
两个区,FROM区、TO区 - 在FROM区进行内存分配,TO区空闲
- 先对需要清理的对象地址进行
标记 - 将FROM尚存活的对象顺序复制到TO区
- 交换FROM 和 TO指向,原来的FROM变成TO区, TO区变成FROM区
- 不产生内存碎片
- 空间利用率低
JVM 会根据情况采用上述三种算法进行垃圾回收
3. 分代垃圾回收

- 对象首先分配到伊甸园
- 若伊甸园空间不足, 触发
minor GC, 引发stop the world. 伊甸园中存活的对象复制到幸存区To,FROM区存活次数小于某个特定值的,且存活的对象复制到TO区,FROM存活次数超过某个特定值的对象赋值到老年代,FROM和TO指向地址互换 - 若老年代空间不足, 先尝试触发
minor GC, 如果空间还是不足, 触发full GC, 触发的STW时间更长. 老年代和新生代的都会进行GC, 新生代部分和minor gc一个规则进行GC. 如果full GC之后还是内存不足, 则报内存溢出异常

3.1 相关VM参数
| 含义 | 参数 | |
|---|---|---|
| 堆的初始大小 | -Xms |
start |
| 堆的最大大小 | -Xmx或-XX:MaxHeapSize=size |
max |
| 新生代大小 | -Xmn或-XX:NewSize=size -XX:MaxNewSize=size |
new |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio或-XX:+UseAdaptiveSizePolicy |
|
| 幸存区比例 | -XX:SurvivorRatio=ratio |
大的对象无法放入幸存区时,直接放入老年代 |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
|
| 晋升详情 | -XX:+PrintTenuringDistribution |
|
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
|
| FullGC前MinorGC | +XX:+ScanvengeBeforeFullGC |
|
- 大对象
Eden放不下的大对象会直接放到老年代,不会触发minor GC
4. 垃圾回收器
4.1 串行
- 单线程
- 堆内存较小、适合个人电脑
4.2 吞吐量优先
- 多线程
- 堆内存较大、多核CPU
- 回收数量大,高延迟
4.3 响应时间优先
- 多线程
- 堆内存较大、多核CPU
- 快速回收、低延迟
4.4 G1 (Garbage First)

G1回收阶段


5. 垃圾回收调优
5.1 调优领域
- 内存
- 锁竞争
- cpu占用
- io
5.2 确定目标
- 低延迟:G1 GC, Z1
- 高吞吐量:ParallelGC
5.3 最快的GC是不发生GC
查看FullGC前后的内存占用,考虑以下几个问题:
- 数据是不是太多
- 例如:
resultSet = statement.execute("select * from 大表")因该进行分页查询
- 例如:
- 数据表是不是太臃肿
- 对象图: 需要哪个对象查哪个。
- 对象大小:包装类型->基本类型
- 是否存在内存泄露
static Map- 对于长时间存在的对象,创建软引用或者弱引用
- 不使用map,采用第三方缓存
5.4 新生代调优
-
新生代特点:
- 所有new操作的内存分配非常廉价:TLAB(tread-local allocation buffer)
- 死亡对象回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
-
-Xmn
- Oracle建议新生代分配占堆内存的1/4~1/2
- 新生代容量= 并发量 * (请求-响应大小)
-
幸存区
- 最大能保留:当前活跃对象+需要晋升的对象
- 真正需要长时间存活的对象才将它晋升到老年代
-
晋升阈值配置得当
-
长时间存活的对象尽快被提升
XX:MaxTenuringThreshold=thresholdXX:+PrintTenuringDistribution
-
5.5 老年代调优
-
没有Full GC,老年代一般不调优
-
出现Full GC,优先考虑新生代调优
-
如果新生代已调优,还是出现Full GC, 将老年代内存预设调大1/4 ~ 1/3
-XX: CMSInitialtingOccupancyFraction=percent
5.6 案例
- 案例一:Full GC 和 Minor GC频繁
- 查看堆内存分配
- 查询运行中的Java:
jps - 查看堆内存分配:
jmap <pid>或jhsdb jmap --heap --pid <pid>
- 查询运行中的Java:
- 如果发现新生代内存小,则适当增加新生代内存大小,并提交晋升阈值
- 案例二:请求高峰发生Full GC, 单次暂停时间特长
- 可能新生代内存小,随着创建的对象变多,晋升阈值降低,晋升到老年代的对象增多,老年代内存满了之后触发Full GC
- 案例三:老年代充裕的情况下,发生Full GC
- 内存碎片过多?
- jdk版本老?方法区在老年代中
三、类加载

1. 类文件结构

javap -c xxx.class
0~3字节:表示它是否是class文件: ca fe ba be
4~7字节: 表示jdk版本
2. 字节码指令
3. 编译器处理
4. 类加载阶段
-
加载
加载和链接可能是交替进行
-
链接
- 验证
- 准备
- 解析
-
初始化
5. 类加载器

public class GetClassLoader {
public static void main(String[] args) {
ClassLoader classLoader = A.class.getClassLoader();
System.out.println(classLoader);
System.out.println(Object.class.getClassLoader()); // 启动类加载器无法直接访问,将打印null
}
}
class A {
public String a = "a";
}
双亲委派机制
双亲(parent)委派就是指调用类加载器的loadClass方法时,查找类的规则 (tips: parent被翻译成双亲,实际翻译成上级更为恰当)
委派本级先做类的加载,本级没有,再由上级进行类加载
从下往上进行已加载类查找,然后再从上往下进行class查找并加载。
自定义类加载器
什么时候需要自定义类加载器
- 想加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
- 继承ClassLoader父类
- 遵从双亲委派机制,重写
findClass方法- 注意不是重写
loadClass方法,否则不会走双亲委派机制
- 注意不是重写
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
package com.JVM.classLoader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class GetClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
// ClassLoader classLoader = A.class.getClassLoader();
// System.out.println(classLoader);
// System.out.println(Object.class.getClassLoader()); // 启动类加载器无法直接访问,将打印null
Loader loader = new Loader();
Class<?> a = loader.loadClass("com.JVM.javas.A");
System.out.println(a);
System.out.println(a.getClassLoader());
}
}
class Loader extends ClassLoader {
/**
*
* @param name 类文件名
* The <a href="#binary-name">binary name</a> of the class
*
* @return
* @throws RuntimeException
*/
@Override
protected Class<?> findClass(String name) throws RuntimeException {
String path = "D:\\Users\\fei\\gitrepo\\gulimall\\java-foundation\\src\\main\\java\\" + name + ".class"; // 拼接全路径
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
Files.copy(Paths.get(path), outputStream);
//得到字节数组
byte[] bytes = outputStream.toByteArray();
//调用父类defineClass
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

浙公网安备 33010602011771号