2021-10-06

JVM内存分配与回收策略的代码实战


前言

在上篇文章中,我们使用了jconsole分析jvm内存,当出现了jvm中常见的OOM问题时,我们优先要反思自己的内存分配策略和垃圾回收策略。本文章暂时不讨论垃圾回收策略,选用JDK1.8 默认的垃圾回收器称为parallel Scavenge,该收集器是面向堆的大吞吐量要求而进行设计。该文通过实例代码,对parallel Scavenge进行实战解析。


调优参数设置案例

JVM options设置
在idea->run-edit configuration中对VM options作以下配置:

-verbose: gc -Xms200M-Xmx200M-Xmn100M -XX:+PrintGCDetails

其中
Xms200M、-Xmx200M、-Xmn100M这三个参数限制了Java堆大小为200MB,不可扩展,其中100MB分配给新生代(eden space:80M,from space 10M, to space 10M),剩下的100MB分配给老年代。
-XX:+PrintGCDetails通过控制台输出收集器日志信息

编码验证理论

以下是在上述调优参数设置的情况下,进行的编码验证以下四种情况
1、对象优先在Eden分配

public class Gc {
    //定义一个静态常量代表1M空间
    public final static int M = 1024 * 1024;

    public static void main(String[] args) {

        byte[] memory1 = new byte[2 * M];
        byte[] memory2 = new byte[2 * M];
        
    }
}

2、新生代转入老年代
Minor GC指发生在新生代的垃圾收集动作

public class Gc {

    //定义一个静态常量代表1M空间
    public final static int M = 1024 * 1024;

    public static void main(String[] args) {

        byte[] memory1 = new byte[20 * M];
        byte[] memory2 = new byte[20 * M];
        byte[] memory3 = new byte[20 * M];
        //Eden区大小为80M,前面已经占有60M,触发一次Minor GC,新生代转入老年代
        byte[] memory4 = new byte[40* M];

    }
}

3、大对象直接进入老年代
对象内存分配超过eden space

public class Gc {

    //定义一个静态常量代表1M空间
    public final static int M = 1024 * 1024;

    public static void main(String[] args) {
        //Eden区大小为80M,对象大小超过80M,**对象内存分配超过eden space,直接进入老年代**
        byte[] memory1 = new byte[81 * M];


    }
}

4、Full GC
Full GC主要指新生代、老年代、metaspace上的全部GC。

public class Gc {

    //定义一个静态常量代表1M空间
    public final static int M = 1024 * 1024;

    public static void main(String[] args) {

        byte[] memory1 = new byte[20 * M];
        byte[] memory2 = new byte[20 * M];
        byte[] memory3 = new byte[20 * M];
        
        //触发一次Minor GC,新生代转入老年代
        byte[] memory4 = new byte[21* M];
        
        //触发full GC,对内存进行回收
        memory1=memory2=memory3=memory4=null;
        memory1 = new byte[80*M];

    }
}

关于java调优的一些建议

1.堆外内存溢出
堆外内存区域只有在JVM 发生Full GC或是程序中手动调用System.gc()时才会被进行垃圾回收。但如果JVM打开了-XX:+DisableExplicitGC开关,System.gc()就会被禁止使用,在程序一直没进行Full GC时,虽然堆外内存中有许多可回收内存,但也不得不抛出OOM。
JVM 常见内存区域如下,这些内存总和受到本机内存和操作系统进程最大内存的限制:

  • 堆外内存,Redis保存BigKey时抛出堆外内存溢出异常,Redis 内部使用Netty,而Netty又使用了Java
    NIO分配堆外内存,堆外内存不足导致OOM。可通过-XX:MaxDirectMemorySize调整堆外内存大小。
  • 线程堆栈,可通过-Xss调整大小,内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)
    或者OutOfMemoryError(如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)。
  • Socket缓存区:每个Socket连接都有Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接
    多的话这块内存占用也比较可观。如果无法分配,可能会抛出IOException:Too many open files异常。

2.虚拟机进程崩溃
如开放API 操作较耗时,在上次操作还未结束,调用方又通过异步发送了许多请求,时间一长,导致等待的线程和Socket 接口越来越多,到超过虚拟机承受能力时导致虚拟机进程崩溃。

3.数据结构不合适
不正确的数据结构,在数据量较大且单个元素占用内存较小时,使用Map 构建会造成很大的空间浪费。
如Map<Integer, Integer>, 有效数据仅为8个字节,而创建Map.Entry 等的开销远大于此。

4.合理规划堆内存、合理编码
合理分配年轻代(Eden、survivor比例)、老年代内存比例,降低Full GC频率。
可以通过以下几个参数要求虚拟机生成GC日志:-XX:+PrintGCTimeStamps(打印GC停顿时间)、
-XX:+PrintGCDetails(打印GC详细信息)、-verbose:gc(打印GC信息,输出内容已被前一个参数包括,可以不写)、-Xloggc:gc.log。
Minor GC 耗时较短,影响不大,而Full GC 相对来说耗时较长, 所以应该将程序 Full GC的频率控制得足够低。
控制Full GC频率的关键是老年代的相对稳定,这取决应用中的对象是否为符合’朝生夕死’原则,
程序应尽量避免成批量的、长时间存在的大对象产生,如此才能保障老年代的稳定。

5.选择合适的垃圾收集器
针对不同的应用场景,选择适合的垃圾收集器,例如有的注重低时延,有的注重高吞吐量。在一般情况,我们优先使用jdk默认的垃圾回收器,默认的垃圾回收器能保障大部分情况。我个人建议在考虑到内存连续,回收速度等情况下,可以优先考虑G1垃圾回收器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值