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

加群联系作者vx:xiaoda0423

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

1024bat.cn/

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

ini 复制代码
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 频率

csharp 复制代码
public class LargeObjectDemo {

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

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

✅ 启动参数优化建议

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

💡 提示

  • 在 G1 中,可以控制大对象是否直接进入老年代
  • 可通过 GC 日志观察大对象分配路径

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

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

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

csharp 复制代码
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 & 内存双压

ini 复制代码
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 压力

arduino 复制代码
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

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

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

✅ 优化建议

  • 使用线程池代替手动创建
  • 减小 -Xss(如 -Xss512k
  • 限制最大并发线程数

Netty 或 NIO DirectMemory 泄漏

java 复制代码
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:

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

✅ 优化建议

  • 增加:-XX:MaxDirectMemorySize=512m

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

  • 开启 Netty 泄漏检测:

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

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

java 复制代码
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

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

java 复制代码
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连接泄漏模拟

arduino 复制代码
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 等。
arduino 复制代码
// 使用 ThreadLocal 缓存线程私有的 SimpleDateFormat 对象
private static final ThreadLocal<SimpleDateFormat> threadLocalDateFormat =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

优化点:

  • 减少临时对象的创建:减少常用对象的频繁创建,避免 GC 频繁发生。
  • 避免对象拆箱装箱:尽量使用原始类型或高效集合来避免自动装箱。

高并发下避免锁竞争

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

csharp 复制代码
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 垃圾收集器,提升内存管理效率。
ruby 复制代码
java -Xms2g -Xmx4g -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError
  • 使用 对象池缓存池 避免频繁的对象创建,减少 GC 压力。

最终代码:

ini 复制代码
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();
        }
    }
}
相关推荐
Asthenia04122 分钟前
深入分析 Java Iterator:从随机访问到高效删除
后端
Asthenia041214 分钟前
深入分析 ConcurrentSkipListSet 数据结构
后端
独泪了无痕22 分钟前
MyBatis中特殊符号处理总结
后端·mybatis
拉不动的猪1 小时前
ES2024 新增的数组方法groupBy
前端·javascript·面试
Asthenia04121 小时前
深入剖析 Java 中的 CompareTo 和 Equals 方法
后端
tokepson1 小时前
GPT-SoVITS Windows 配置与推理笔记(自用)
windows·ai·github
uhakadotcom2 小时前
使用 Google Pay API 集成 Web 应用
后端
Asthenia04122 小时前
为何在用 Netty 实现 Redis 服务时,要封装一个 BytesWrapper?
后端
来自星星的坤2 小时前
Spring Boot 邮件发送配置遇到的坑:解决 JavaMailSenderImpl 未找到的错误
java·开发语言·spring boot·后端·spring
小草cys2 小时前
github发布个人中英文简历网站CaoYongshengcys.github.io
github·cv·个人简历