分布式微服务系统架构第99集:缓存系统的实战级优化案例

加群联系作者vx:xiaoda0423

仓库地址:https://webvueblog.github.io/JavaPlusDoc/

https://1024bat.cn/

频繁创建短生命周期对象(如 JSON 对象、临时列表等)

go 复制代码
public class ObjectReuseDemo {

    private static final int LOOP = 10_000_000;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        // 改进:使用对象池模拟复用(或线程复用对象)
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < LOOP; i++) {
            sb.setLength(0); // 清空
            sb.append("user_").append(i).append("_info");
            String result = sb.toString();

            if (i % 2_000_000 == 0) {
                System.out.println(result);
            }
        }

        System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
    }
}

✅ 优化点

  • 避免频繁创建临时对象,降低 Eden GC 压力

  • StringBuilder 在单线程场景比 String + 高效

默认大对象直接进入老年代,增加 Full GC 频率

go 复制代码
public class LargeObjectDemo {

    public static void main(String[] args) {
        System.out.println("分配大数组...");
        byte[] big = new byte[10 * 1024 * 1024]; // 10MB

        System.out.println("大对象创建完毕,等待回收...");
    }
}

✅ 启动参数优化建议

go 复制代码
-XX:+UseG1GC
-XX:G1HeapRegionSize=2m                  # 控制 region 大小
-XX:PretenureSizeThreshold=5m            # >5MB 的对象直接进入老年代

💡 提示

  • 在 G1 中,可以控制大对象是否直接进入老年代

  • 可通过 GC 日志观察大对象分配路径

通用 JVM 启动调优参数模板(适用于以上)

go 复制代码
-Xms512m -Xmx512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=./heap.hprof \
-Xlog:gc*:file=gc.log:time,level,tags

ThreadLocal 使用不当,导致内存泄漏

go 复制代码
public class ThreadLocalLeakDemo {

    private static final ThreadLocal<byte[]> local = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 1000; i++) {
            pool.submit(() -> {
                // 模拟请求处理,ThreadLocal 绑定大对象
                local.set(new byte[1024 * 1024]); // 1MB
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ignored) {
                } finally {
                    // ⚠️ 不清理会导致内存泄漏(线程池中的线程长期持有 ThreadLocal 值)
                    local.remove();
                }
            });
        }

        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MINUTES);
    }
}

✅ JVM 优化建议

  • 在线程池中使用 ThreadLocal 一定要记得 remove()

  • 否则容易造成长时间占用堆内存

  • 可配合 -Xmx256m + -XX:+HeapDumpOnOutOfMemoryError 验证泄漏现象

频繁反射使用,CPU & 内存双压

go 复制代码
import java.lang.reflect.Method;

public class ReflectionOveruseDemo {

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.String");

        for (int i = 0; i < 10_000_000; i++) {
            Method method = clazz.getMethod("toUpperCase");
            String result = (String) method.invoke("hello");
            if (i % 1_000_000 == 0) {
                System.out.println(result);
            }
        }
    }
}

✅ 优化建议

  • 反射很慢且会缓存方法句柄,可能导致 PermGen/MetaSpace 增长

  • 尽量缓存 Method 对象 或使用 MethodHandle

  • 启动参数中可设定 -XX:MaxMetaspaceSize=128m 观察增长

频繁使用 Lambda/匿名类 导致 GC 压力

go 复制代码
import java.util.function.Function;

public class LambdaLeakDemo {

    public static void main(String[] args) {
        for (int i = 0; i < 10000000; i++) {
            Function<String, String> func = s -> s.toUpperCase(); // 每次创建新的匿名类
            func.apply("hello");
        }

        System.out.println("执行完成");
    }
}

✅ 优化建议

  • 使用静态 Lambda 或缓存 Lambda 表达式(尤其是热点路径)

  • JVM 在某些版本中对 Lambda 实现仍是匿名内部类,可能造成类加载压力

  • 查看 Metaspace 利用率是否飙高,搭配 -XX:+PrintCompilation 观察 JIT 情况

频繁创建线程(绕过线程池)导致内存耗尽

  • 每个线程占用约 1MB 栈空间(-Xss1m

  • 线程数过多 → 内存耗尽 → 报错:

    go 复制代码
    java.lang.OutOfMemoryError: unable to create new native thread

✅ 优化建议

  • 使用线程池代替手动创建

  • 减小 -Xss(如 -Xss512k

  • 限制最大并发线程数

Netty 或 NIO DirectMemory 泄漏

go 复制代码
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class DirectMemoryLeakDemo {

    public static void main(String[] args) throws InterruptedException {
        List<ByteBuffer> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
            list.add(buffer); // 没有释放!

            if (i % 100 == 0) {
                System.out.println("已分配 DirectBuffer:" + i + " MB");
                Thread.sleep(100);
            }
        }
    }
}

❌ 问题

  • DirectBuffer 走的是堆外内存(不走 GC)

  • 默认限制很小,容易 OOM:

    go 复制代码
    java.lang.OutOfMemoryError: Direct buffer memory

✅ 优化建议

  • 增加:-XX:MaxDirectMemorySize=512m

  • 使用 buffer.clear() 或手动释放(如 Netty 的 release()

  • 开启 Netty 泄漏检测:

    go 复制代码
    ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);

静态集合引用,导致类无法卸载

go 复制代码
import java.util.ArrayList;
import java.util.List;

public class StaticLeakDemo {

    public static List<byte[]> staticList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            staticList.add(new byte[1024 * 100]); // 100KB
            if (staticList.size() % 100 == 0) {
                System.out.println("staticList.size = " + staticList.size());
                Thread.sleep(500);
            }
        }
    }
}

❌ 问题

  • 静态变量引用的对象永远不会被 GC 回收

  • 热部署、类卸载失败、Perm/MetaSpace 占满

✅ 优化建议

  • 避免静态变量引用大对象(尤其缓存类)

  • 用弱引用 WeakReference、或放入 WeakHashMap

  • 分析类加载器占用:jcmd pid GC.class_stats

服务启动时初始化太慢(类加载 + 大量对象)

go 复制代码
import java.util.ArrayList;
import java.util.List;

public class SlowStartupDemo {

    public static final List<String> cache = new ArrayList<>();

    static {
        for (int i = 0; i < 1000000; i++) {
            cache.add("data_" + i); // 模拟加载配置、预热缓存
        }
        System.out.println("静态初始化完成");
    }

    public static void main(String[] args) {
        System.out.println("主程序开始执行");
    }
}

❌ 问题

  • 类加载阶段内存暴增,GC 频繁

  • 启动慢,影响容器健康探针

✅ 优化建议

  • 延迟加载 / 懒加载

  • 启动分析工具:-XX:+PrintCompilation -XX:+TraceClassLoading

  • 拆分初始化逻辑,或放到异步线程中

Netty/Redis连接泄漏模拟

go 复制代码
public class FakeRedisLeak {

    public static void main(String[] args) throws Exception {
        List<FakeConnection> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            FakeConnection conn = new FakeConnection();
            list.add(conn); // 模拟连接没释放
            Thread.sleep(50);
        }
    }

    static class FakeConnection {
        private final byte[] data = new byte[1024 * 512]; // 模拟缓冲区
    }
}

✅ 适合分析

  • 服务没正确释放外部资源(池化没生效)

  • 连接池配置不当导致"潜在 OOM"

  • 搭配 GC log 和 jmap -histo 看堆增长来源

内存管理优化(避免频繁的 GC)

在缓存系统中,频繁的对象创建和销毁会加大 GC 压力。我们通过以下方法减少内存占用和垃圾产生:

  • 使用 ThreadLocal 或对象池来缓存一些常用的临时对象,例如 SimpleDateFormatPattern 等。
go 复制代码
// 使用 ThreadLocal 缓存线程私有的 SimpleDateFormat 对象
private static final ThreadLocal<SimpleDateFormat> threadLocalDateFormat =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

优化点:

  • 减少临时对象的创建:减少常用对象的频繁创建,避免 GC 频繁发生。

  • 避免对象拆箱装箱:尽量使用原始类型或高效集合来避免自动装箱。

高并发下避免锁竞争

对于高并发请求,减少锁的竞争十分重要。ReentrantLockReadWriteLockAtomic 类可以有效减少锁的竞争,提高性能。

go 复制代码
private final ReadWriteLock lock = new ReentrantReadWriteLock();

public String getData(String key) {
    lock.readLock().lock();
    try {
        // 首先尝试读取缓存
        String value = localCache.getIfPresent(key);
        if (value == null) {
            // 如果缓存中没有,获取数据库数据并更新缓存
            lock.writeLock().lock();
            try {
                value = databaseService.getDataFromDb(key);
                localCache.put(key, value);
            } finally {
                lock.writeLock().unlock();
            }
        }
        return value;
    } finally {
        lock.readLock().unlock();
    }
}

优化点:

  • 读写锁 :读操作时使用 readLock,写操作时使用 writeLock,避免了过多的锁竞争。

  • 高并发下读多写少的场景:使用读写锁提升并发读的效率。

垃圾回收优化
  • 启动时配置 G1 垃圾收集器,提升内存管理效率。
go 复制代码
java -Xms2g -Xmx4g -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError
  • 使用 对象池缓存池 避免频繁的对象创建,减少 GC 压力。

最终代码:

go 复制代码
public class CacheService {
    private final Cache<String, String> localCache;
    private final RedisCache redisCache;
    private final DatabaseService databaseService;
    private final ReadWriteLock lock;

    public CacheService() {
        localCache = CacheBuilder.newBuilder().maximumSize(10000).expireAfterWrite(10, TimeUnit.MINUTES).build();
        redisCache = new RedisCache();  // 假设 Redis 配置已完成
        databaseService = new DatabaseService();  // 假设数据库服务已完成
        lock = new ReentrantReadWriteLock();
    }

    public String getDataFromCache(String key) {
        lock.readLock().lock();
        try {
            String value = localCache.getIfPresent(key);
            if (value == null) {
                value = redisCache.get(key);
                if (value != null) {
                    localCache.put(key, value);
                } else {
                    lock.writeLock().lock();
                    try {
                        value = databaseService.getDataFromDb(key);
                        redisCache.put(key, value);
                        localCache.put(key, value);
                    } finally {
                        lock.writeLock().unlock();
                    }
                }
            }
            return value;
        } finally {
            lock.readLock().unlock();
        }
    }
}
相关推荐
安防视频中间件/视频资源汇聚平台35 分钟前
SVMSPro分布式综合安防管理平台--地图赋能智慧指挥调度新高度
分布式
SYC_MORE2 小时前
vLLM实战:多机多卡大模型分布式推理部署全流程指南
分布式
弧襪5 小时前
K8S-证书过期更新
云原生·容器·kubernetes
@PHARAOH5 小时前
HOW - 缓存 React 自定义 hook 的所有返回值(包括函数)
前端·react.js·缓存
拉不动的猪5 小时前
设计模式之--------工厂模式
前端·javascript·架构
Justice link5 小时前
部署redis cluster
数据库·redis·缓存
何似在人间5755 小时前
多级缓存模型设计
java·jvm·redis·缓存
程序猿阿伟6 小时前
《解锁分布式软总线:构建智能设备统一管理平台》
分布式
程序猿阿伟6 小时前
《从底层逻辑剖析:分布式软总线与传统计算机硬件总线的深度对话》
分布式
工业甲酰苯胺6 小时前
zk基础—zk实现分布式功能
分布式