- 1 JVM与Java体系结构
- 1.1 JVM 结构简图
- 1.2 Java代码执行流程
- 1.3 JVM的架构模型
- 1.4 JVM的生命周期
- 1 JVM与Java体系结构
- 一、内存结构
- 1. 程序计数器
- 2. 虚拟机栈
- 2.1 定义
- 2.2 栈内存溢出
- 2.3 线程运行诊断
- 3. 本地方法栈
- 4. 堆
- 4.1 定义
- 4.2 堆内存溢出
- 4.3 堆内存诊断
- 1)jps工具
- 2)jmap工具
- 3)jconsole工具
- 4)jvisualvm
- 5) jstack
- 5. 方法区
- 5.1 定义
- 5.2 组成
- 5.3 方法区内存溢出
- 5.4 运行时常量池
- 5.5 StringTable
- 5.6 StringTable 垃圾回收
- 5.7 StringTable 性能调优
- 6. 直接内存
- 6.1 定义
- 6.2 直接内存释放
- 二、垃圾回收
- 1. 如何判断对象可以回收
- 1.1 引用计数法
- 1.2 可达性分析法
- 1.3 四种引用
- 强引用
- 2. 垃圾回收算法
- 2.1 标记清除 Mark Sweep
- 2.2 标记整理 Mark Compact
- 2.3 复制 Copy
- 3. 分代垃圾回收
- 3.1 相关VM参数
- 4. 垃圾回收器
- 4.1 串行
- 4.2 吞吐量优先
- 4.3 响应时间优先
- 4.4 G1 (Garbage First)
- G1回收阶段
- 5. 垃圾回收调优
- 5.1 调优领域
- 5.2 确定目标
- 5.3 最快的GC是不发生GC
- 5.4 新生代调优
- 5.5 老年代调优
- 5.6 案例
- 1. 如何判断对象可以回收
- 三、类加载
- 1. 类文件结构
- 2. 字节码指令
- 3. 编译器处理
- 4. 类加载阶段
- 5. 类加载器
- 双亲委派机制
- 自定义类加载器
- 6. 运行期优化
- 6.1 即时编译 - JIT
- 分层编译
- 方法内联
- 字段优化
- 6.1 即时编译 - JIT

-
内存与垃圾回收篇
-
字节码与类的加载篇 ✈
-
性能监控与调优篇
-
大厂面试篇
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.classLast modified 2025年5月27日; size 442 bytesSHA-256 checksum 675d068f509639e5a688e3ca50cbc237c8459f36e78347be553c9e8f98f736acCompiled from "StackStruTest.java"
public class com.JVM.chapter01.StackStruTestminor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #7 // com/JVM/chapter01/StackStruTestsuper_class: #2 // java/lang/Objectinterfaces: 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: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/JVM/chapter01/StackStruTest;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=1, locals=2, args_size=10: iconst_51: istore_12: returnLineNumberTable:line 5: 0line 7: 2LocalVariableTable:Start Length Slot Name Signature0 3 0 args [Ljava/lang/String;2 1 1 i I
}
SourceFile: "StackStruTest.java"
由于跨平台的特性, Java的指令都是根据栈来设计的
栈型指令系统: 跨平台性, 指令集小, 指令多; 执行性能比寄存器差
1.4 JVM的生命周期
- 虚拟机的启动
- 虚拟机的执行
- 虚拟机的退出

JDK 24 Documentation - Home
一、内存结构
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 + ai++;}}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] pidjmap [option] executable corejmap [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] pidjstack [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]; // 10MBSystem.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.classLast modified 2025年5月31日; size 551 bytesSHA-256 checksum 685b230badb0ecfbb0269e956bbb39aa205913c435f86d19c86a7f40d34224a3Compiled from "Pool.java"
public class com.JVM.chapter01.Poolminor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #21 // com/JVM/chapter01/Poolsuper_class: #2 // java/lang/Objectinterfaces: 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: ()Vflags: (0x0001) ACC_PUBLICCode:stack=2, locals=1, args_size=10: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #13 // String Hello World5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 5: 0line 6: 8LocalVariableTable:Start Length Slot Name Signature0 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就是abString s6 = s4.intern(); // s4在StringTable中没有"ab"时会自动放入串池,反之则否System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.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. 垃圾回收器
新一代垃圾回收器ZGC的探索与实践 - 美团技术团队
Java 中的垃圾回收 - Dev.java
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()); // 启动类加载器无法直接访问,将打印nullLoader 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*/@Overrideprotected 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();//调用父类defineClassreturn defineClass(name, bytes, 0, bytes.length);} catch (IOException e) {throw new RuntimeException(e);}}
}