Java 内存管理完整笔记
第一部分:内存泄漏与FGC的关系
1. 内存泄漏会导致FGC - 确定答案:会
1.1 基本机制
// 内存泄漏 → 老年代持续增长 → 触发FGC
public class MemoryLeakToFGC {
// 1. 内存泄漏导致对象无法被回收
// 2. 老年代空间逐渐被占满
// 3. JVM触发FGC尝试回收内存
// 4. 泄漏对象无法回收,FGC效果差
// 5. 老年代很快又满了,再次触发FGC
// 6. 形成恶性循环
}
1.2 典型的内存泄漏场景
// 场景1:静态集合持续增长
public class StaticCollectionLeak {
private static Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
cache.put(key, value); // 只放不清理
// 随着时间推移,cache越来越大
// 这些对象永远不会被GC回收
}
}
// 场景2:监听器未移除
public class ListenerLeak {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
// 如果listener使用完后不移除
// 会一直被listeners引用,无法回收
}
}
// 场景3:ThreadLocal未清理
public class ThreadLocalLeak {
private static ThreadLocal<List<Object>> threadLocal = new ThreadLocal<>();
public void processData() {
List<Object> data = new ArrayList<>();
// 添加大量数据...
threadLocal.set(data);
// 方法结束后如果不调用remove()
// 在线程池环境下会导致内存泄漏
// threadLocal.remove(); // 忘记调用
}
}
2. 内存泄漏导致FGC的演进过程
2.1 时间线分析
timeline
title 内存泄漏导致FGC的演进过程
初期阶段 : 应用正常运行
: Minor GC正常
: FGC很少发生
泄漏积累 : 泄漏对象逐渐增多
: 老年代使用率上升
: FGC开始偶尔发生
问题显现 : 老年代持续增长
: FGC频率明显增加
: 每次FGC回收效果差
严重阶段 : FGC非常频繁
: 应用响应变慢
: 可能出现OOM
2.2 内存使用模式
// 正常应用的内存使用模式
public class NormalMemoryPattern {
/*
内存使用率随时间变化:
正常模式:
Memory | /\ /\ /\
Usage | / \ / \ / \
| / \ / \ / \
|__/______\/______\/______\___> Time
特点:有升有降,GC后内存明显下降
*/
}
// 内存泄漏的内存使用模式
public class LeakMemoryPattern {
/*
内存泄漏模式:
Memory | /\ /\ /\
Usage | / \ / \ / \
| /\ / \ / \/ \
| / \/ \ \
|____/________________________\___> Time
特点:整体趋势上升,GC后内存下降幅度小
*/
}
3. 实际案例分析
3.1 案例1:缓存导致的内存泄漏
// 问题代码
@Service
public class UserService {
// 静态缓存,永远不清理
private static Map<Long, User> userCache = new ConcurrentHashMap<>();
public User getUser(Long userId) {
User user = userCache.get(userId);
if (user == null) {
user = userRepository.findById(userId);
userCache.put(userId, user); // 只放不清理
}
return user;
}
}
// 问题现象
/*
运行时间 老年代使用率 FGC频率
1小时 30% 0次/小时
6小时 60% 1次/小时
12小时 85% 5次/小时
24小时 95% 20次/小时
最终 OOM 应用崩溃
*/
3.2 案例2:监控数据显示
# 使用jstat监控内存泄漏应用
jstat -gc <pid> 5s
# 正常应用的GC模式
# OU(老年代使用) FGC FGCT
# 204800.0 10 2.5
# 198400.0 10 2.5 # FGC后内存明显下降
# 205600.0 10 2.5
# 201200.0 11 2.8 # 又一次FGC,内存下降
# 内存泄漏应用的GC模式
# OU(老年代使用) FGC FGCT
# 512000.0 15 8.2
# 510400.0 16 9.1 # FGC后内存几乎不下降
# 513600.0 17 10.3
# 515200.0 18 11.8 # FGC频率越来越高
4. 如何识别内存泄漏导致的FGC
4.1 关键指标
// 内存泄漏的典型特征
public class LeakIndicators {
// 1. FGC频率持续增加
// 正常:每小时0-2次 → 泄漏:每小时10+次
// 2. FGC回收效果差
// 正常:FGC后老年代使用率下降30%+
// 泄漏:FGC后老年代使用率下降<10%
// 3. 老年代使用率持续上升
// 正常:在某个范围内波动
// 泄漏:整体趋势向上,不断接近100%
// 4. 应用响应时间恶化
// 泄漏:随着FGC频率增加,响应时间越来越慢
}
4.2 诊断脚本
#!/bin/bash
# 内存泄漏检测脚本
PID=$1
MONITOR_TIME=300 # 监控5分钟
echo "开始监控进程 ${PID} 是否存在内存泄漏..."
# 记录初始状态
INITIAL_GC=$(jstat -gc ${PID} | tail -1)
INITIAL_FGC=$(echo $INITIAL_GC | awk '{print $14}')
INITIAL_OU=$(echo $INITIAL_GC | awk '{print $8}')
sleep ${MONITOR_TIME}
# 记录结束状态
FINAL_GC=$(jstat -gc ${PID} | tail -1)
FINAL_FGC=$(echo $FINAL_GC | awk '{print $14}')
FINAL_OU=$(echo $FINAL_GC | awk '{print $8}')
# 计算变化
FGC_INCREASE=$((FINAL_FGC - INITIAL_FGC))
OU_CHANGE=$(echo "$FINAL_OU - $INITIAL_OU" | bc)
echo "监控结果:"
echo "FGC增加次数: ${FGC_INCREASE}"
echo "老年代内存变化: ${OU_CHANGE}KB"
# 判断是否可能存在内存泄漏
if [ $FGC_INCREASE -gt 2 ] && [ $(echo "$OU_CHANGE > 0" | bc) -eq 1 ]; then
echo "⚠️ 可能存在内存泄漏!"
echo "建议:生成heap dump进行详细分析"
else
echo "✅ 内存使用正常"
fi
5. 解决方案
5.1 代码层面修复
// 修复缓存泄漏
@Service
public class UserServiceFixed {
// 使用有界缓存
private final Cache<Long, User> userCache = CacheBuilder.newBuilder()
.maximumSize(10000) // 限制大小
.expireAfterWrite(1, TimeUnit.HOURS) // 设置过期时间
.build();
public User getUser(Long userId) {
return userCache.get(userId, () -> userRepository.findById(userId));
}
}
// 修复ThreadLocal泄漏
public class ThreadLocalFixed {
private static ThreadLocal<List<Object>> threadLocal = new ThreadLocal<>();
public void processData() {
try {
List<Object> data = new ArrayList<>();
threadLocal.set(data);
// 处理业务逻辑...
} finally {
threadLocal.remove(); // 确保清理
}
}
}
// 修复监听器泄漏
public class ListenerFixed {
private final Set<EventListener> listeners = ConcurrentHashMap.newKeySet();
public void addListener(EventListener listener) {
listeners.add(listener);
}
public void removeListener(EventListener listener) {
listeners.remove(listener); // 提供移除方法
}
// 使用WeakReference避免强引用
private final Set<WeakReference<EventListener>> weakListeners =
ConcurrentHashMap.newKeySet();
}
5.2 监控和预警
// 内存泄漏监控
@Component
public class MemoryLeakMonitor {
@Scheduled(fixedRate = 60000) // 每分钟检查
public void checkMemoryLeak() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
long used = heapUsage.getUsed();
long max = heapUsage.getMax();
double usageRatio = (double) used / max;
// 老年代使用率超过85%告警
if (usageRatio > 0.85) {
log.warn("内存使用率过高: {}%, 可能存在内存泄漏",
String.format("%.2f", usageRatio * 100));
// 发送告警...
sendAlert("Memory usage too high: " + usageRatio);
}
// 检查FGC频率
List<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcBean : gcBeans) {
if (gcBean.getName().contains("Old") ||
gcBean.getName().contains("G1")) {
long fgcCount = gcBean.getCollectionCount();
// 记录并分析FGC频率变化...
}
}
}
}
6. 预防措施
6.1 编码规范
// 1. 集合使用规范
public class CollectionBestPractices {
// ✅ 使用有界集合
private final Map<String, Object> cache = new LRUCache<>(1000);
// ✅ 及时清理
public void cleanup() {
cache.clear();
}
// ❌ 避免静态集合无限增长
// private static List<Object> staticList = new ArrayList<>();
}
// 2. 资源管理规范
public class ResourceManagement {
public void processWithResources() {
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用try-with-resources确保资源释放
} catch (IOException e) {
log.error("处理文件异常", e);
}
}
}
// 3. 监听器管理规范
public class ListenerManagement {
private final List<WeakReference<Listener>> listeners = new ArrayList<>();
public void addListener(Listener listener) {
listeners.add(new WeakReference<>(listener));
// 定期清理失效的WeakReference
cleanupDeadReferences();
}
private void cleanupDeadReferences() {
listeners.removeIf(ref -> ref.get() == null);
}
}
6.2 工具和检查
# 1. 定期生成heap dump分析
jmap -dump:format=b,file=heap_$(date +%Y%m%d_%H%M%S).hprof <pid>
# 2. 使用MAT分析工具
# - Leak Suspects Report
# - Dominator Tree
# - Histogram
# 3. 集成到CI/CD
# 在测试环境运行内存泄漏检测
7. 内存泄漏与FGC总结
7.1 关键要点
public class KeyTakeaways {
// 1. 内存泄漏必然导致FGC频率增加
// 2. 泄漏的特征是FGC后内存回收效果差
// 3. 及早发现比事后修复更重要
// 4. 代码审查要重点关注集合、缓存、监听器
// 5. 建立监控和告警机制
}
7.2 诊断流程
graph TD
A[发现FGC频繁] --> B[检查FGC回收效果]
B --> C{回收效果差?}
C -->|是| D[怀疑内存泄漏]
C -->|否| E[其他原因]
D --> F[生成heap dump]
F --> G[MAT分析]
G --> H[定位泄漏对象]
H --> I[修复代码]
I --> J[验证效果]
第二部分:为什么叫"内存泄漏"而不是"内存占用"
1. 术语来源和含义差异
1.1 "泄漏"vs"占用"的本质区别
// 正常的内存占用 - 这不是泄漏
public class NormalMemoryUsage {
private List<User> activeUsers = new ArrayList<>(); // 业务需要的数据
public void addUser(User user) {
activeUsers.add(user); // 这是正常占用,业务需要
}
public void removeUser(User user) {
activeUsers.remove(user); // 可以正常释放
}
}
// 内存泄漏 - 问题在于"失控"
public class MemoryLeak {
private static Map<String, Object> cache = new HashMap<>();
public void cacheData(String key, Object data) {
cache.put(key, data); // 放进去了
// 但是!没有任何机制能把它拿出来
// 程序员"失去了控制权",无法释放这块内存
// 这就是"泄漏" - 内存流失了,找不回来了
}
}
1.2 "泄漏"一词的精妙之处
// 为什么用"泄漏"这个词?
public class WhyLeak {
/*
想象一个水桶:
正常使用:
┌─────────┐
│ 水 ←─── │ 可以倒出来
│ │
└─────────┘
内存泄漏:
┌─────────┐
│ 水 │
│ ↓ │ 从底部漏掉了!
└────○────┘ 再也收不回来
关键:不是水(内存)本身有问题
而是容器(程序)有漏洞,失去了控制
*/
}
2. 历史和语言学角度
2.1 术语的历史演进
// C语言时代的内存泄漏
void memory_leak_example() {
char* ptr = malloc(1024); // 分配内存
// 做一些操作...
// 忘记调用 free(ptr);
// 这块内存永远"泄漏"了,程序无法再访问
// 就像水从破洞流走,再也回不来
}
历史背景:
- 1960年代,"memory leak"这个术语在系统编程中出现
- 类比物理世界的"泄漏"现象:液体从容器中流失
- 强调的是失控和不可逆的特性
2.2 中英文对照理解
| 英文 | 中文 | 核心含义 |
|---|---|---|
| Memory Leak | 内存泄漏 | 内存"流失",失去控制 |
| Memory Usage | 内存使用 | 正常的内存占用 |
| Memory Occupation | 内存占用 | 中性的内存使用状态 |
3. "泄漏"vs"占用"的技术区别
3.1 可控性差异
// 内存占用 - 可控的
public class ControlledMemoryUsage {
private List<String> businessData = new ArrayList<>();
// ✅ 有明确的生命周期管理
public void loadData() {
businessData.addAll(loadFromDatabase());
}
public void clearData() {
businessData.clear(); // 程序员可以主动释放
}
// ✅ 有边界控制
public void addData(String data) {
if (businessData.size() < MAX_SIZE) { // 有上限控制
businessData.add(data);
}
}
}
// 内存泄漏 - 失控的
public class UncontrolledMemoryLeak {
private static Map<String, Object> cache = new HashMap<>();
// ❌ 无法控制的增长
public void addToCache(String key, Object value) {
cache.put(key, value);
// 没有清理机制
// 没有大小限制
// 程序员失去了对这块内存的控制权
}
// ❌ 即使想清理也做不到
// 因为key可能已经丢失,或者不知道何时清理
}
3.2 意图性差异
// 故意的内存占用
public class IntentionalUsage {
// 这是应用缓存,故意占用内存来提高性能
private final Cache<String, User> userCache = CacheBuilder.newBuilder()
.maximumSize(10000) // 明确的大小限制
.expireAfterWrite(1, TimeUnit.HOURS) // 明确的过期策略
.build();
// 这不是泄漏,是有意的内存使用策略
}
// 无意的内存泄漏
public class UnintentionalLeak {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
// 程序员本意是临时使用
// 但忘记了移除,导致意外的"泄漏"
}
}
4. 从业务角度理解
4.1 类比日常生活
// 生活中的类比
public class LifeAnalogy {
/*
内存占用 = 租房子
- 你知道自己租了房子
- 可以选择续租或退租
- 有明确的合同和控制权
内存泄漏 = 房子被偷偷占用
- 有人偷偷住进你的房子
- 你不知道,也无法赶走
- 房子被"泄漏"给了别人,你失去了控制
*/
}
4.2 商业影响的差异
// 正常内存占用的商业考量
public class BusinessMemoryUsage {
// 这是投资:用内存换性能
private Map<String, String> configCache = new HashMap<>();
// 商业价值:减少数据库查询,提高响应速度
// 成本可控:配置数量有限,内存占用可预期
}
// 内存泄漏的商业损失
public class BusinessMemoryLeak {
// 这是损失:内存白白浪费
private static List<Object> unknownObjects = new ArrayList<>();
// 商业损失:
// 1. 服务器成本增加(需要更多内存)
// 2. 应用性能下降(频繁GC)
// 3. 系统稳定性风险(可能OOM)
// 4. 维护成本增加(排查问题)
}
5. 技术社区的约定俗成
5.1 为什么不改名?
// 如果改成"内存占用"会有什么问题?
public class WhyNotChange {
/*
1. 失去了问题的严重性标识
"占用" - 听起来很正常
"泄漏" - 立即意识到这是问题
2. 无法区分正常和异常
所有内存使用都是"占用"
但只有失控的才是"泄漏"
3. 国际交流障碍
全世界都用"Memory Leak"
改成其他词汇会造成沟通困难
*/
}
5.2 术语的精确性
// 精确的术语分类
public class PreciseTerminology {
// Memory Usage - 内存使用(中性)
// Memory Consumption - 内存消耗(中性)
// Memory Allocation - 内存分配(中性)
// Memory Leak - 内存泄漏(负面,问题)
// Memory Waste - 内存浪费(负面,但不一定是泄漏)
// 每个词都有其精确的含义和使用场景
}
6. 实际案例对比
6.1 案例:缓存系统
// 这是内存占用,不是泄漏
@Component
public class ProperCache {
private final Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 有界限
.expireAfterAccess(30, TimeUnit.MINUTES) // 会过期
.removalListener(entry -> { // 有清理通知
log.info("缓存项被移除: {}", entry.getKey());
})
.build();
// 这是有意的、可控的内存占用
// 为了业务性能而付出的内存成本
}
// 这是内存泄漏
@Component
public class LeakyCache {
private static final Map<String, Object> cache = new ConcurrentHashMap<>();
public void cache(String key, Object value) {
cache.put(key, value);
// 没有清理机制
// 没有大小限制
// 内存"泄漏"了,永远收不回来
}
}
6.2 监控告警的差异
// 监控系统如何区分
public class MonitoringDifference {
// 对于正常内存占用的监控
public void monitorNormalUsage() {
// 关注:使用率是否合理
// 告警:超过预期阈值时提醒
// 处理:评估是否需要扩容或优化
}
// 对于内存泄漏的监控
public void monitorMemoryLeak() {
// 关注:使用率是否持续上升
// 告警:发现泄漏趋势时立即报警
// 处理:紧急修复代码,重启服务
}
}
7. 术语辨析总结
7.1 为什么"泄漏"这个词更准确?
public class WhyLeakIsBetter {
/*
1. 强调失控性
- "占用"是中性的,"泄漏"强调问题
- 突出了程序员失去控制的本质
2. 体现不可逆性
- 泄漏的内存很难回收
- 就像水从破洞流走,找不回来
3. 历史约定
- 60多年的技术术语历史
- 全球技术社区的共同语言
4. 问题导向
- 一听到"泄漏"就知道要修复
- "占用"听起来像正常现象
*/
}
7.2 核心区别总结
| 特征 | 内存占用 | 内存泄漏 |
|---|---|---|
| 可控性 | 可控制释放 | 失去控制 |
| 意图性 | 有意使用 | 无意发生 |
| 边界性 | 有明确边界 | 无限增长 |
| 业务价值 | 有业务价值 | 纯粹浪费 |
| 问题严重性 | 正常现象 | 需要修复 |
综合总结
核心要点回顾
-
内存泄漏必然导致FGC频繁
- 泄漏对象无法回收,老年代持续增长
- FGC效果差,形成恶性循环
-
"泄漏"术语的精妙性
- 强调失控和不可逆的特性
- 区别于正常的内存占用
- 体现问题的严重性和紧迫性
-
实践指导
- 建立完善的监控体系
- 遵循良好的编码规范
- 及时发现和修复泄漏问题
结论:内存泄漏是导致FGC频繁的主要原因之一,而"内存泄漏"这个术语准确地描述了问题的本质——失控的、不可逆的内存流失,需要通过代码规范、监控告警、定期分析来预防和解决。