1. Java垃圾回收机制
Java的垃圾回收(GC)是自动内存管理的重要机制。
主要包含以下要点:
- 垃圾判定算法
- 引用计数法:计算对象被引用的次数
- 可达性分析:从GC Roots开始搜索,不可达的对象就是垃圾
- 垃圾收集算法
graph LR
A[垃圾收集算法] --> B[标记-清除]
A --> C[复制算法]
A --> D[标记-整理]
A --> E[分代收集]
- 垃圾收集器
- Serial收集器:单线程
- ParNew收集器:多线程版Serial
- CMS收集器:并发标记清除
- G1收集器:分区域的增量式收集
2. 四种引用类型
graph TD
A[引用类型] --> B[强引用]
A --> C[软引用]
A --> D[弱引用]
A --> E[虚引用]
B --> F[永不回收 new Object]
C --> G[内存不足时回收 SoftReference]
D --> H[GC时回收 WeakReference]
E --> I[随时回收 PhantomReference]
详细说明:
- 强引用
Object obj = new Object(); // 最常见的引用类型
String str = "hello";
- 特点:只要引用存在,永远不会被垃圾回收
- 即使内存不足,JVM也不会回收
- 软引用
SoftReference<String> softRef = new SoftReference<String>(new String("hello"));
- 特点:内存充足时保留,内存不足时回收
- 常用于实现内存敏感的缓存
- 弱引用
WeakReference<String> weakRef = new WeakReference<String>(new String("hello"));
- 特点:下一次GC时必定回收
- 常用于WeakHashMap等数据结构
- 虚引用
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<String>(new String("hello"), queue);
- 特点:随时可能被回收
- 主要用于跟踪对象的回收状态
- 必须和ReferenceQueue一起使用
引用强度对比:
强引用 > 软引用 > 弱引用 > 虚引用
这样的图解和说明是否更清晰?每种引用类型都配有代码示例和具体使用场景。
3. 类加载机制
类加载过程:
graph LR
A[加载] --> B[验证]
B --> C[准备]
C --> D[解析]
D --> E[初始化]
- 加载:读取class文件
- 验证:确保安全性
- 准备:分配内存
- 解析:符号引用转直接引用
- 初始化:执行类构造器
4. JVM、Dalvik、ART比较
graph TB
A[虚拟机类型] --> B[JVM]
A --> C[Dalvik]
A --> D[ART]
B -->|"基于栈架构"| E[Java SE/EE]
C -->|"基于寄存器"| F[Android 5.0前]
D -->|"预编译"| G[Android 5.0后]
主要区别:
- JVM:运行Java字节码
- Dalvik:运行DEX文件
- ART:安装时预编译,运行更快
让我用流程图详细对比这三种虚拟机的工作原理和类加载流程:
1. 三种虚拟机的工作流程
graph TB
subgraph JVM工作流程
A1[Java源代码.java] --> B1[javac编译]
B1 --> C1[字节码.class]
C1 --> D1[JVM解释执行]
D1 --> E1[机器码]
end
graph TB
subgraph Dalvik工作流程
A2[Java源代码.java] --> B2[javac编译]
B2 --> C2[字节码.class]
C2 --> D2[dx工具转换]
D2 --> E2[DEX文件]
E2 --> F2[Dalvik解释执行]
F2 --> G2[机器码]
end
graph TB
subgraph ART工作流程
A3[Java源代码.java] --> B3[javac编译]
B3 --> C3[字节码.class]
C3 --> D3[dx工具转换]
D3 --> E3[DEX文件]
E3 --> F3[安装时编译AOT]
F3 --> G3[机器码]
end
2. 类加载详细流程
graph LR
A[加载Loading] --> B[验证Verification]
B --> C[准备Preparation]
C --> D[解析Resolution]
D --> E[初始化Initialization]
E --> F[使用Using]
F --> G[卸载Unloading]
3. 每个阶段的具体工作
graph TB
subgraph 加载阶段
A1[读取类文件] --> A2[转换为方法区数据结构]
A2 --> A3[在堆中生成Class对象]
end
graph TB
subgraph 验证阶段
B1[文件格式验证] --> B2[元数据验证]
B2 --> B3[字节码验证]
B3 --> B4[符号引用验证]
end
graph TB
subgraph 准备阶段
C1[静态变量分配内存] --> C2[设置初始值]
end
graph TB
subgraph 解析阶段
D1[符号引用] --> D2[直接引用]
end
graph TB
subgraph 初始化阶段
E1[执行静态代码块] --> E2[静态变量赋值]
end
4. 三种虚拟机主要特点对比
graph TB
subgraph JVM特点
JVM1[基于栈架构] --> JVM2[跨平台]
JVM2 --> JVM3[.class文件]
end
graph TB
subgraph Dalvik特点
D1[基于寄存器] --> D2[专为移动设备优化]
D2 --> D3[.dex文件]
end
graph TB
subgraph ART特点
ART1[预编译机制] --> ART2[更快的运行速度]
ART2 --> ART3[更多的存储空间]
end
5. 代码示例
// 类加载示例
public class ClassLoadDemo {
// 准备阶段:分配内存,设置默认值 number = 0
// 初始化阶段:设置为 123
private static int number = 123;
// 初始化阶段才执行
static {
System.out.println("类初始化");
number = 456;
}
// 使用阶段
public static void main(String[] args) {
// 主动使用,触发类加载
System.out.println(number);
}
}
6. 类加载器层次结构
graph TD
A[Bootstrap ClassLoader] --> B[Extension ClassLoader]
B --> C[Application ClassLoader]
C --> D[Custom ClassLoader]
7. 关键区别总结
- JVM vs Dalvik:
- JVM:
.class文件,基于栈 - Dalvik:
.dex文件,基于寄存器 - 内存占用:Dalvik 更少
- Dalvik vs ART:
- 执行方式:
- Dalvik:JIT(即时编译)
- ART:AOT(预先编译)
- 性能:ART 更快
- 安装时间:ART 更长
- 存储空间:ART 需要更多
8. 实际应用场景
// JVM应用场景
public class ServerApplication {
public static void main(String[] args) {
// 服务器端应用
// 大内存,高并发
}
}
// Android应用场景
public class AndroidApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 移动设备应用
// 注重启动速度和电池效率
}
}
这样的流程图和解释是否更清晰了?每种虚拟机都有其特定的应用场景和优势,理解它们的区别对于选择合适的开发平台和优化应用性能都很重要。
5. Java内存回收机制
内存区域划分:
graph TB
A[JVM内存区域] --> B[堆]
A --> C[方法区]
A --> D[虚拟机栈]
A --> E[本地方法栈]
A --> F[程序计数器]
回收过程:
- 新生代:Minor GC
- 老年代:Major GC
- 全堆回收:Full GC
让我用生动的比喻来解释Java的垃圾回收机制:
🏠 Java堆内存结构图
graph TB
A[Java堆内存] --> B[新生代 Young Generation]
A --> C[老年代 Old Generation]
B --> D[Eden区]
B --> E[Survivor 0]
B --> F[Survivor 1]
🎯 各个区域的比喻
想象一个购物中心的商品管理:
-
新生代(Young Generation)= 快闪店区域
- Eden区 = 新品展示区
- Survivor区 = 畅销商品区
- 特点:商品更新快,周转频繁
-
老年代(Old Generation)= 常规商铺区域
- 存放长期畅销的商品
- 变动较少,相对稳定
🔄 垃圾回收过程
1. Minor GC(新生代回收)
// 类比:快闪店商品上新
public class NewObject {
public void create() {
// 新对象首先在Eden区创建
Object obj = new Object(); // 进入Eden区
// Eden区满了会触发Minor GC
}
}
就像快闪店的商品管理:
- Eden区满了就像新品区货架满了
- 清理过程:
- 把还在使用的商品(存活对象)转移到Survivor区
- 清空Eden区
- 两个Survivor区交替使用,像是商品的轮换展示
2. Major GC(老年代回收)
// 类比:常规商铺的库存清理
public class OldObject {
// 长期存活的对象
private static final Cache cache = new Cache(); // 最终会进入老年代
}
就像商场定期大清仓:
- 主要清理老年代的对象
- 过程比Minor GC慢,影响更大
3. Full GC(全堆回收)
// 类比:商场年度大清仓
System.gc(); // 建议进行Full GC(但不一定立即执行)
相当于商场全场大清仓:
- 新老商品一起清理
- 影响最大,相当于商场暂时停业清理
♻️ 垃圾判定方式
1. 引用计数法
graph LR
A[对象A] --> B[对象B]
C[对象C] --> B
D[引用计数=2]
就像商品的受欢迎程度:
- 每个人喜欢这个商品就+1
- 不喜欢了就-1
- 当计数为0时,说明没人喜欢,可以清理
2. 可达性分析
graph TD
A[GC Roots] --> B[活跃对象1]
B --> C[活跃对象2]
D[垃圾对象1] --> E[垃圾对象2]
就像追踪商品的使用链:
- GC Roots就像商场的基本必需品
- 从这些商品出发,看哪些商品经常被顾客一起购买
- 找不到关联的商品就可以考虑清理
🎯 回收触发条件
- Eden区满了:
// 频繁创建新对象
for (int i = 0; i < 10000; i++) {
new Object(); // Eden区很快会满
}
- 老年代空间不足:
// 大对象直接进入老年代
byte[] bigArray = new byte[4 * 1024 * 1024]; // 4MB
- 显式调用(不建议):
System.gc(); // 建议JVM进行垃圾回收
💡 实际例子
public class GCDemo {
public static void main(String[] args) {
// 1. 创建大量临时对象(触发Minor GC)
for (int i = 0; i < 100000; i++) {
createTemporaryObject();
}
// 2. 创建大对象(可能触发Major GC)
byte[] largeArray = new byte[10 * 1024 * 1024];
// 3. 内存紧张(可能触发Full GC)
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 持续分配1MB空间
}
}
private static void createTemporaryObject() {
new Object();
}
}
🔍 如何监控GC
- 使用JVM参数:
java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps YourApp
- 使用工具:
- JVisualVM
- JConsole
- VisualGC
这样理解垃圾回收机制是不是更清晰了?就像是一个商场的商品管理系统,通过不同的区域和策略,高效地管理内存资源。
6. JMM(Java内存模型)
主要问题:
- 可见性
- 原子性
- 有序性
解决方案:
// 1. volatile关键字
volatile int value;
// 2. synchronized关键字
synchronized void method() {
// 临界区代码
}
// 3. Lock接口
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
JMM通过happens-before原则保证内存可见性:
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 传递性规则
这些机制共同保证了Java程序的并发安全性。
详细解释Java内存模型(JMM):
Java内存模型(JMM)详解
graph TB
subgraph 线程1
A1[工作内存] --> B1[本地变量副本]
end
subgraph 主内存
C[共享变量]
end
subgraph 线程2
A2[工作内存] --> B2[本地变量副本]
end
B1 -.读写.-> C
B2 -.读写.-> C
JMM的核心内容:
- 主内存与工作内存
// 示例:多线程访问共享变量
public class SharedVariable {
// 存储在主内存中的共享变量
private static int counter = 0;
public void increment() {
// 线程从主内存读取counter到工作内存
// 在工作内存中执行+1操作
// 将结果写回主内存
counter++;
}
}
- 内存交互操作
- read(读取):从主内存读取到工作内存
- load(载入):将读取的值载入工作内存副本
- use(使用):使用工作内存中的变量值
- assign(赋值):对工作内存中的变量赋值
- store(存储):将工作内存值存储到主内存
- write(写入):将store的值写入主内存
- lock(锁定):对主内存变量加锁
- unlock(解锁):对主内存变量解锁
JMM的三大特性:
- 可见性
// volatile保证可见性
public class Visibility {
private volatile boolean flag = false;
public void write() {
flag = true; // 修改立即对其他线程可见
}
public void read() {
while (!flag) {
// 能够看到其他线程对flag的修改
}
}
}
- 原子性
public class Atomicity {
// synchronized保证原子性
private int count = 0;
public synchronized void increment() {
count++; // 复合操作的原子性
}
// 使用原子类
private AtomicInteger atomicCount = new AtomicInteger(0);
public void safeIncrement() {
atomicCount.incrementAndGet(); // 原子操作
}
}
- 有序性
public class Ordering {
private int a = 0;
private volatile boolean flag = false;
public void write() {
a = 1; // 1
flag = true; // 2
}
public void read() {
if (flag) { // 3
int b = a; // 4
// 由于volatile的happens-before规则
// 如果flag为true,则a必定是1
}
}
}
JMM的happens-before规则:
graph LR
A[happens-before规则] --> B[程序顺序规则]
A --> C[volatile规则]
A --> D[传递性规则]
A --> E[监视器锁规则]
A --> F[start规则]
A --> G[join规则]
内存屏障类型:
graph TD
A[内存屏障] --> B[LoadLoad屏障]
A --> C[StoreStore屏障]
A --> D[LoadStore屏障]
A --> E[StoreLoad屏障]
解决方案示例:
- volatile解决可见性:
public class VolatileExample {
private volatile boolean flag = false;
private int count = 0;
public void writer() {
count = 100; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
// 如果读到flag为true,则count必定是100
System.out.println(count); // 4
}
}
}
- synchronized解决原子性:
public class SynchronizedExample {
private static class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
}
- Lock实现的内存语义:
public class LockExample {
private ReentrantLock lock = new ReentrantLock();
private int value = 0;
public void setValue(int value) {
lock.lock();
try {
this.value = value;
} finally {
lock.unlock();
}
}
}
JMM是Java并发编程的基础,它通过定义程序中各个变量的访问规则,保证了Java程序在各种平台下都能达到一致的内存访问效果。理解JMM对于编写正确的并发程序至关重要。
更加形象的解释:
让我用生活中的例子,通俗易懂地解释Java内存模型(JMM):
🏢 Java内存模型的比喻
想象一个大型办公楼(这就是我们的Java程序):
-
主内存 = 中央档案室
- 所有的公共文件都存放在这里
- 每个人要用文件都要从这里取
-
工作内存 = 个人办公桌
- 每个员工(线程)都有自己的办公桌
- 要处理文件时,需要从档案室取出复印件到自己桌上
🎯 三大特性的生活例子
- 可见性 - 公告栏效应
graph LR
A[总经理贴公告] --> B[公告栏]
B --> C[所有员工能看到]
就像公司的公告栏:
- volatile关键字就像是公告栏
- 一旦贴出公告(修改变量),所有人立即可以看到
- 没有用volatile就像是把通知放在自己抽屉里,别人看不到
- 原子性 - 转账操作
// 想象银行转账
public class BankAccount {
private int balance; // 账户余额
// synchronized确保取钱过程不会被打断
public synchronized void withdraw(int amount) {
// 就像柜员办理业务时,其他人不能插队
if (balance >= amount) {
balance -= amount;
}
}
}
就像银行柜员办理业务:
- 一个柜员(synchronized)同一时间只能服务一个客户
- 服务过程不能被打断(原子性)
- 其他客户必须等待(线程等待)
- 有序性 - 食堂打饭流程
graph LR
A[取餐盘] --> B[打主食]
B --> C[打菜]
C --> D[结账]
就像食堂打饭:
- 必须先取餐盘,再打饭
- 不能先打饭再拿餐盘
- 这就是程序执行的顺序性
🎮 内存交互的形象比喻
想象你在玩一个多人游戏:
-
主内存 = 游戏服务器
- 存储所有玩家的真实数据
-
工作内存 = 玩家客户端
- 每个玩家在自己电脑上看到的游戏画面
- 需要和服务器实时同步
graph TB
subgraph 玩家1电脑
A1[本地游戏画面]
end
subgraph 游戏服务器
B[真实游戏数据]
end
subgraph 玩家2电脑
A2[本地游戏画面]
end
A1 -.同步.-> B
A2 -.同步.-> B
🔒 解决方案的生活例子
- volatile = 即时通讯
// 就像微信群通知
volatile boolean isNewMessage = false;
// 发消息的人
void sendMessage() {
writeMessage();
isNewMessage = true; // 所有人立即看到"新消息"提醒
}
// 接收消息的人
void checkMessage() {
if (isNewMessage) {
readMessage();
}
}
- synchronized = 卫生间占用
// 就像上厕所要关门
synchronized void useWashroom() {
// 进去后锁门
// 其他人看到占用,要等待
// ...使用中
// 用完开门离开
}
- 原子操作 = 食堂打饭
// AtomicInteger就像食堂窗口的排号系统
AtomicInteger number = new AtomicInteger(0);
// 取号
int myNumber = number.incrementAndGet();
// 保证每个人都能拿到唯一的号码
📝 总结
- JMM就像是一个大公司的管理制度
- 主内存是公司的档案室
- 工作内存是员工的办公桌
- volatile是公告栏
- synchronized是会议室预订系统
- 原子操作是排队系统
这样理解起来是不是更容易了?每个概念都能找到生活中的对应物,让抽象的内存模型变得具体和易懂。