java性能调优

java性能调优

一、怎样做好性能调优?

  1. 扎实的计算机基础

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

  3. 善于追问和总结

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

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

三、常用的调优策略

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

四、实际运用

1、java编程性能调优

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

public static void main(String[] args) {
    int size = 100000; // 十万
    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);
}
    /**
    程序最后运行结果:
    29
    7
    6
    */

当size达到230万时,几乎无差异,达到300万,第一种最快

public static String toString(int i) {
    if (i == Integer.MIN_VALUE)
        return "-2147483648";
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    char[] buf = new char[size];
    getChars(i, size, buf);
    return new String(buf, true);
}

b.ArrayList还是LinkedList?

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

ArrayList是如何实现的

ArrayList的实现类:

public class ArrayList extends AbstractList
        implements List, 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 item;
    Node next;
    Node prev;

    Node(Node prev, E element, Node next) {
        this.item = element;    // 元素item
        this.next = next;       // 后指针
        this.prev = prev;       // 前指针
    }
}

LinkedList的实现类:

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

LinkedList的属性:

transient int size = 0;
transient Node first;
transient Node last;

LinkedList新增元素

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node l = last;
    final Node 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 succ) {
    // assert succ != null;
    final Node pred = succ.prev;
    final Node newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

LinkedList 删除元素

E unlink(Node x) {
    // assert x != null;
    final E element = x.item;
    final Node next = x.next;
    final Node 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 node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node 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性能调优)