java性能调优

作者: 蓝胖子的白日梦丶 | 来源:发表于2020-07-08 11:04 被阅读0次

    java性能调优

    一、怎样做好性能调优?

    1. 扎实的计算机基础

    2. 习惯透过源码了解技术本质

    3. 善于追问和总结

    二、为什么要做性能调优?

    • 一款线上产品如果不做性能测试,那它就好比一颗定时炸弹。
    • 了解产品承受极限。
    • 节约服务器资源。

    三、常用的调优策略

    • 应用调优
      • 优化代码
      • 优化设计
      • 优化算法
    • 调优策略
      • 时间换空间
      • 空间换时间
    • 系统调优
      • JVM参数调优
    • 兜底策略
      • 限流
      • 扩容

    四、实际运用

    1、java编程性能调优

    a.数字转字符串的效率问题

    public static void main(String[] args) {
        int size = 1000000;
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            String a = i + "";
        }
        System.out.println(System.currentTimeMillis() - start);
        
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            String b = String.valueOf(i);
        }
        System.out.println(System.currentTimeMillis() - start1);
        
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            String c = Integer.toString(i);
        }
        System.out.println(System.currentTimeMillis() - start2);
    }
    
        /**
        程序最后运行结果:
        125
        35
        35
        */
    

    b.ArrayList还是LinkedList?

    常规回答:ArrayList 和 LinkedList 在新增、删除元素时,LinkedList 的效率要高于 ArrayList,而在遍历的时候,ArrayList 的效率要高于 LinkedList。

    ArrayList是如何实现的

    ArrayList的实现类:

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    

    ArrayList的属性:

     //默认初始化容量    
     private static final int DEFAULT_CAPACITY = 10;    
     //对象数组    
     transient Object[] elementData;     
     //数组长度    
     private int size;
    

    为什么ArrayList实现了Serializable,但是elementData却用transient关键字修饰?

    ArrayList的构造:

    public ArrayList(int initialCapacity) {
        //初始化容量不为零时,将根据初始化值创建数组大小
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始化容量为零时,使用默认的空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }
    
    public ArrayList() {
        //初始化默认为空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    ArrayList新增元素:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    public void add(int index, E element) {
        rangeCheckForAdd(index);
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    ArrayList删除元素:

    public E remove(int index) {
        rangeCheck(index);
    
        modCount++;
        E oldValue = elementData(index);
    
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }
    

    ArryayList遍历元素:

    public E get(int index) {
        rangeCheck(index);
    
        return elementData(index);
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    LinkedList 是如何实现的
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
    
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;    // 元素item
            this.next = next;       // 后指针
            this.prev = prev;       // 前指针
        }
    }
    

    LinkedList的实现类:

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    

    LinkedList的属性:

    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
    

    LinkedList新增元素

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }
    
    public void add(int index, E element) {
        checkPositionIndex(index);
    
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
    

    LinkedList 删除元素

    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
    
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
    
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
    
        x.item = null;
        size--;
        modCount++;
        return element;
    }
    
    Node<E> node(int index) {
        // assert isElementIndex(index);
    
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    

    在 LinkedList 删除元素的操作中,我们首先要通过循环找到要删除的元素,如果要删除的位置处于 List 的前半段,就从前往后找;若其位置处于后半段,就从后往前找。这样做的话,无论要删除较为靠前或较为靠后的元素都是非常高效的,但如果 List 拥有大量元素,移除的元素又在 List 的中间段,那效率相对来说会很低。

    LinkedList 遍历元素

    LinkedList 的获取元素操作实现跟 LinkedList 的删除元素操作基本类似,通过分前后半段来循环查找到对应的元素。但是通过这种方式来查询元素是非常低效的,特别是在 for 循环遍历的情况下,每一次循环都会去遍历半个 List。所以在 LinkedList 循环遍历时,我们可以使用 iterator 方式迭代循环,直接拿到我们的元素,而不需要通过循环查找 List。

    ArrayList与LinkedList 对比
    新增元素操作测试
    • 从集合头部位置新增元素
    • 从集合中间位置新增元素
    • 从集合尾部位置新增元素

    测试结果 (花费时间):

    • ArrayList > LinkedList
    • ArrayList < LinkedList
    • ArrayList < LinkedList
    删除元素操作测试

    与新增原理类似,不做演示

    遍历元素操作测试
    • for(;;) 循环
    • 迭代器迭代循环

    测试结果 (花费时间):

    • ArrayList < LinkedList
    • ArrayList ≈ LinkedList

    2、jvm性能调优

    a.如何优化垃圾回收机制

    回收发生在哪里?

    JVM 的内存区域中,程序计数器、虚拟机栈和本地方法栈这 3 个区域是线程私有的,随着线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。

    那么垃圾回收的重点就是关注堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。

    对象在什么时候可以被回收?

    一般一个对象不再被引用,就代表该对象可以被回收。

    引用计数算法

    这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。

    可达性分析算法

    GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。

    GC 算法
    标记-清除算法(Mark-Sweep)
    标记-清除算法

    优点:

    不需要移动对象,简单高效

    缺点:

    标记-清除过程效率低,GC产生内存碎片

    复制算法(Copying)
    复制算法

    优点:

    简单高效,不会产生内存碎片

    缺点:

    内存使用率低,有可能产生频繁复制

    标记-整理算法(Mark-Compact)
    标记-整理算法

    优点:

    综合了前两种算法的优点

    缺点:

    仍然需要移动局部对象

    分代收集算法(Generational Collection)
    分代收集算法

    1.部分垃圾回收器使用的模型

    2.新生代 + 老年代 + 永久代(1.7)/元数据区(1.8)Metaspace

    ​ 1.永久代 元数据 - Class

    ​ 2.永久代需要指定大小限制,元数据可以设置,也可以不设置,无上限(受限于物理内存)

    ​ 3.字符串常量 1.7 - 永久代,1.8 - 堆

    3.新生代 = Eden + 2个suvivor区

    ​ 1.YGC回收之后,大多数对象会被回收,或者的对象进入s0

    ​ 2.再次YGC,活着的对象eden + s0 -> s1

    ​ 3.再次YGC,eden + s1 -> s0

    ​ 4.年龄足够,进入老年代

    ​ 5.s区装不下 -> 老年代

    4.老年代

    ​ 1.老年代满了,Full GC

    优点:

    分区回收

    缺点:

    对长时间活跃对象的场景回收不明显,甚至起到反作用

    GC 调优策略
    降低 Minor GC 频率

    通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。

    可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次 Minor GC 的时间吗?如果单次 Minor GC 的时间增加,那也很难达到我们期待的优化效果呀。

    我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,那么正常情况下,Minor GC 的时间为 :T1+T2。

    当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:两次扫描新生代,即 2T1。可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。通常在虚拟机中,复制对象的成本要远高于扫描成本。

    降低 Full GC 的频率

    通常情况下,由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销。我们可以使用哪些方法来降低 Full GC 的频率呢?

    减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于 web 端显示。例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。

    我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。

    增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。

    b.如何优化JVM内存分配

    参考指标

    GC 频率:高频的 FullGC 会给系统带来非常大的性能消耗,虽然 MinorGC 相对 FullGC 来说好了许多,但过多的 MinorGC 仍会给系统带来压力。

    内存:这里的内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。首先我们要分析堆内存大小是否合适,其实是分析年轻代和老年代的比例是否合适。如果内存不足或分配不均匀,会增加 FullGC,严重的将导致 CPU 持续爆满,影响系统性能。

    吞吐量:频繁的 FullGC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。

    延时:JVM 的 GC 持续时间也会影响到每次请求的响应时间。

    具体调优方法

    调整堆内存空间减少 FullGC:通过日志分析,如果发现堆内存被用完了,而且存在大量 FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调大堆内存空间。

    -Xms4g -Xmx4g //可以通过设置JVM参数解决
    

    调整年轻代减少 MinorGC:通过调整堆内存大小,我们可以提升整体的吞吐量,降低响应时间。那还有优化空间吗?我们还可以将年轻代设置得大一些,从而减少一些 MinorGC。

    -Xms4g -Xmx4g -Xmn3g //可以通过设置JVM参数解决
    

    设置 Eden、Survivor 区比例:在 JVM 中,如果开启 AdaptiveSizePolicy,则每次 GC 后都会重新计算 Eden、From Survivor 和 To Survivor 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。这个时候 SurvivorRatio 默认设置的比例会失效。

    在 JDK1.8 中,默认是开启 AdaptiveSizePolicy 的,我们可以通过 -XX:-UseAdaptiveSizePolicy 关闭该项配置,或显示运行 -XX:SurvivorRatio=8 将 Eden、Survivor 的比例设置为 8:2。大部分新对象都是在 Eden 区创建的,我们可以固定 Eden 区的占用比例,来调优 JVM 的内存分配性能。

    3、数据库性能优化

    a.索引的优化

    覆盖索引

    根据索引查询字段,如果该字段包含在索引里,则不需要进行回表检索。

    最左匹配原则

    最左前缀可以是联合索引的最左N个字段,也可是最左M个字符。

    索引下推

    MYSQL5.6,引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

    b.什么时候需要分库分表

    如何分表分库?

    通常,分表分库分为垂直切分和水平切分两种。

    垂直分库是指根据业务来分库,不同的业务使用不同的数据库。例如,订单和消费券在抢购业务中都存在着高并发,如果同时使用一个库,会占用一定的连接数,所以我们可以将数据库分为订单库和促销活动库。

    而垂直分表则是指根据一张表中的字段,将一张表划分为两张表,其规则就是将一些不经常使用的字段拆分到另一张表中。例如,一张订单详情表有一百多个字段,显然这张表的字段太多了,一方面不方便我们开发维护,另一方面还可能引起跨页问题。这时我们就可以拆分该表字段,解决上述两个问题。

    分表分库之后面临的问题
    1、分布式事务问题

    在提交订单时,除了创建订单之外,我们还需要扣除相应的库存。而订单表和库存表由于垂直分库,位于不同的库中,这时我们需要通过分布式事务来保证提交订单时的事务完整性。通常,我们解决分布式事务有两种通用的方式:两阶事务提交(2PC)以及补偿事务提交(TCC)。

    通常有一些中间件已经帮我们封装好了这两种方式的实现,例如 Spring 实现的 JTA。

    2、跨节点 JOIN 查询问题

    用户在查询订单时,我们往往需要通过表连接获取到商品信息,而商品信息表可能在另外一个库中,这就涉及到了跨库 JOIN 查询。通常,我们会冗余表或冗余字段来优化跨库 JOIN 查询。

    对于一些基础表,例如商品信息表,我们可以在每一个订单分库中复制一张基础表,避免跨库 JOIN 查询。而对于一两个字段的查询,我们也可以将少量字段冗余在表中,从而避免 JOIN 查询,也就避免了跨库 JOIN 查询。

    3、全局主键 ID 问题

    在分库分表后,主键将无法使用自增长来实现了,在不同的表中我们需要统一全局主键 ID。因此,我们需要单独设计全局主键,避免不同表和库中的主键重复问题。

    使用 UUID 实现全局 ID 是最方便快捷的方式,即随机生成一个 32 位 16 进制数字,这种方式可以保证一个 UUID 的唯一性,水平扩展能力以及性能都比较高。但使用 UUID 最大的缺陷就是,它是一个比较长的字符串,连续性差,如果作为主键使用,性能相对来说会比较差。

    我们也可以基于 Redis 分布式锁实现一个递增的主键 ID,这种方式可以保证主键是一个整数且有一定的连续性,但分布式锁存在一定的性能消耗。

    我们还可以基于 Twitter 开源的分布式 ID 生产算法——snowflake 解决全局主键 ID 问题,snowflake 是通过分别截取时间、机器标识、顺序计数的位数组成一个 long 类型的主键 ID。这种算法可以满足每秒上万个全局 ID 生成,不仅性能好,而且低延时。

    五、总结

    系统性能调优,考验的不仅是我们的基础知识,还包括开发者的综合素质。首当其冲就是我们的实践能力了,善于动手去实践所学的知识点,不仅可以更深刻地理解其中的原理,还能在实践中发现更多的问题。

    相关文章

      网友评论

        本文标题:java性能调优

        本文链接:https://www.haomeiwen.com/subject/nwvqcktx.html