JVM优化之分配速率

在实际的开发过程中,使用Java语言开发的应用基本上都会遇到性能问题,比如接口超时、服务器负载高、并发数低、数据库性能低或死锁等,并且现在随着互联网的发展,“猛快糙”的开发方式会让代码变得越来越臃肿,随着系统访问量的增加,各种性能问题就随之而来了。

应用的性能问题非常多,比如磁盘、内存、网络IO、应用代码、数据库、缓存、JVM等,有前辈总结过可以将Java性能优化分为4个层级:

  1. 应用层优化:也就是代码层,主要是代码上的优化,这个主要就要靠代码review和扎实的个人基础知识了,可以通过Java线程栈定位问题代码
  2. 数据库层优化:优化数据库读写方面的优化,分析SQL、定位死锁、分库分表
  3. 框架层优化:为应用选择合适的框架是最重要的,合适的框架能够带来更优的性能
  4. JVM层优化:JVM是应用的最底层,属于是最难也是最容易出现性能瓶颈的一层,GC、JVM参数合理使用

优化难度逐层增加,涉及的知识和解决的问题也不同,我们本文主要讲解一下JVM的年轻代GC方面的优化知识。


运行代码:

import java.util.concurrent.locks.LockSupport;

public class Boxing {

    private static volatile Double sensorValue;

    private static void readSensor() {
        while (true) {
            sensorValue = Math.random();
        }
    }

    private static void processSensorValue(Double value) {
        if (value != null) {
            LockSupport.parkNanos(1000);
        }
    }

    public static void main(String[] args) {
        int iterations = args.length > 0 ? Integer.parseInt(args[0]) : 1_000_000;

        initSensor();

        for (int i = 0; i < iterations; i++) {
            processSensorValue(sensorValue);
        }
    }

    private static void initSensor() {
        Thread sensorReader = new Thread(Boxing::readSensor);

        sensorReader.setDaemon(true);
        sensorReader.start();
    }
}

JVM参数设置为-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xmx32m,运行代码,查看GC情况:

image-20200529013346025

这里先说一下什么叫分配速率(Allocation Rate),分配速率是指单位时间内分配的内存量,通常使用MB/sec作为单位,也可以使用PB/year来表示,分配速率过高就会严重影响程序的性能,在JVM中会导致巨大的GC开销

计算上一次GC之后与下一次GC之前的年轻代使用量,两者差值除以时间,就是分配速率

从上图GC日志中,我们计算一下信息:

  • 在JVM启动后391ms,共创建8704kb的对象,第一次YGC之后,年轻代中还有1004kb的存活对象
  • 在JVM启动后459ms,年轻代的使用量再次增加到9708kb,触发第二次YGC,GC之后年轻代的使用量缩减到1004kb
  • 在JVM启动后471ms,年轻代的使用量为9708kb,GC后为1020kb

然后我们现在来计算一下这三次GC的分配速率:

EventTimeYGC beforeYGC afterAllocated DuringAllocation Rate
1st YGC391ms8704kb1004kb8704kb22MB/sec
2nd YGC459ms9708kb1004kb8704kb51MB/sec
3rd YGC471ms9708kb1020kb8704kb709MB/sec
Total471ms  26112kb55MB/sec

从表中我们看到,该程序的内存分配速率在55MB/sec。


分配速率的意义

分配速率的变化会增加或降低STW的频率,从而影响吞吐量,但仅仅只有年轻代的YGC会受分配速率的影响,老年代GC的频率和持续时间不收分配速率的直接影响,而是受到提升速率的影响,也就是Major GC是受Minor GC影响的。

我们知道年轻代中分为Eden、Survivor from和Survivor to三个区,因为分配速率直接影响Minor GC,所以我们先看下修改Eden的大小是否会减小Minor GC的频率,提升分配速率。

使用JVM参数-XX:NewSize-XX:MaxNewSize-XX:SurvivorRatio设置Eden和Survivor区的大小,我们将Eden区分别设置为100M和500M,看一下GC日志:

  • Eden区100M

    JVM参数:-XX:NewSize=125m -XX:MaxNewSize=125m -XX:SurvivorRatio=8

    image-20200529015605343

    EventTimeYGC beforeYGC afterAllocated DuringAllocation Rate
    1st YGC686ms102400kb1967kb102400kb146MB/sec
    2nd YGC820ms104367kb1548kb102400kb747MB/sec
    3rd YGC947ms103948kb1548kb102400kb788MB/sec
    Total947ms  307200kb317MB/sec

    分配速率为317MB/sec

  • Eden区500M

    JVM参数:-XX:NewSize=625m -XX:MaxNewSize=625m -XX:SurvivorRatio=8

    image-20200529020327070

    EventTimeYGC beforeYGC afterAllocated DuringAllocation Rate
    1st YGC1126ms512000kb1967kb512000kb445MB/sec
    2nd YGC1752ms513967kb1836kb512000kb799MB/sec
    3rd YGC2429ms513836kb1772kb512000kb739MB/sec
    Total2429ms  1536000kb618MB/sec

    分配速率为618MB/sec

随着Eden区大小越来越大,分配速率也越来越大,因为减少了GC频率,就等于减少了任务线程的停顿,就可以做更多的工作,也就创建了更多的对象,所以对于同一个Java应用来说,分配速率越高,性能越高。


高分配速率对JVM的影响

如果创建了过多的朝生夕死的对象,Minor GC的频率就会增加,在并发较大的情况下,会严重的影响吞吐量,从上面的三个场景可以看出来,当年轻代越大时Minor GC的次数就会越来越少,但是分配速率并没有降低,如果每次GC后只有少量的对象存活,Minor GC的暂停时间也不会明显的增加。

但是有时候增加年轻代的大小并不能彻底的解决问题,我们通过工具jvisualvm查看堆信息

image-20200529022229415

大部分堆内存都被Double对象占用了,这个对象是在readSensor()方法中创建的,最简单的代码层面的优化就是将包装类Double换成原生类型,因为原生类型不算是对象,所以也就不会在堆中分配内存,而是之间覆盖一个属性域即可,不会产生GC事件,所以GC基本上完全消除。并且JVM通过逃逸分析技术来避免过度分配。

import java.util.concurrent.locks.LockSupport;

public class Boxing {

    private static volatile double sensorValue = Double.NaN;

    private static void readSensor() {
        while (true) {
            sensorValue = Math.random();
        }
    }

    private static void processSensorValue(double value) {
        if (Double.isNaN(value)) {
            LockSupport.parkNanos(1000);
        }
    }

    public static void main(String[] args) {
        int iterations = args.length > 0 ? Integer.parseInt(args[0]) : 1_000_000;

        initSensor();

        for (int i = 0; i < iterations;) {
            processSensorValue(sensorValue);
        }
    }

    private static void initSensor() {
        Thread sensorReader = new Thread(Boxing::readSensor);

        sensorReader.setDaemon(true);
        sensorReader.start();
    }
}

image-20200529023002189

image-20200529023022780

控制台未输出任何GC日志,而jvisualvm上监控到的堆使用情况也极低,由此可见在代码中可以在适当情况下使用原生类型代替包装类。


总结

在年轻代使用上,应当适当的提高分配速率,减少Minor GC的频率,可以通过两种方式实现

  1. 增大新生代大小
  2. 使用原生类型代替包装类,减少堆内对象的创建

简单点说就是少创建对象、多分配空间,以减少GC次数,加大系统吞吐量

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值