Java JVM

前言

1、JVM 架构

  • JVM 全称是 Java Virtual Machine(Java 虚拟机),Java 虚拟机是一种程序虚拟机(相对操作系统虚拟机),Java 的运行环境实现跨平台。

1.1 JVM 架构图分析

  • JVM 被分为三个主要的子系统:类加载器子系统运行时数据区执行引擎

  • 类加载器子系统

    • Java 的动态类加载功能是由类加载器子系统处理。
    • 当它在运行时(不是编译时)首次引用一个类时,它加载、链接并初始化该类文件。
  • 运行时数据区

    • 分为 5 个主要组件:方法区、堆区、栈区、PC 寄存器、本地方法栈。
    • 这 5 大部分中,只有 堆 和 方法区 会发生 GC 垃圾回收,由此可见,OOM 问题有很大的可能就会出现在堆和方法区。
    • 由于方法区和堆区的内存由多个线程共享,所以存储的数据不是线程安全的。
    • 方法区:所有类级别数据将被存储在这里,包括静态变量。每个 JVM 只有一个方法区,它是一个共享的资源。
    • 堆区:所有的对象和它们相应的实例变量以及数组将被存储在这里。每个 JVM 同样只有一个堆区。
    • 栈区:对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。
    • PC 寄存器:每个线程都有一个单独的 PC 寄存器来保存当前执行指令的地址,一旦该指令被执行,PC 寄存器会被更新至下条指令的地址。
    • 本地方法栈:本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。
  • 执行引擎

    • 分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
    • 解释器:解释器能快速的解释字节码,但执行却很慢。解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。
    • 编译器:JIT 编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用 JIT 编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。
    • 垃圾回收器:收集并删除未引用的对象。可以通过调用 System.gc() 来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM 的垃圾回收只收集哪些由 new 关键字创建的对象。所以,如果不是用 new 创建的对象,可以使用 finalize 函数来执行清理。
    • Java 本地接口 (JNI):JNI 会与本地方法库进行交互并提供执行引擎所需的本地库。
    • 本地方法库:它是一个执行引擎所需的本地库的集合。

1.2 JVM 性能调优参数

  • JVM 三大性能调优参数:-Xms –Xmx –Xss。

    • -Xms –Xmx 是对 堆 的性能调优参数。
    • -Xss 是对每一个线程 栈 的性能调优参数,
  • -Xms:表示初始化 Java 堆的大小及该进程刚创建出来的时候,他的专属 Java 堆的大小。

    • 一旦对象容量超过了 Java 堆的初始容量,Java 堆将会自动扩容到 -Xmx 大小。
  • -Xmx:表示 Java 堆可以扩展到的最大值。

    • 在很多情况下,通常将 -Xms 和 -Xmx 设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
  • -Xss:规定了每个线程虚拟机栈及堆栈的大小。

    • 一般情况下,256k 是足够的。
    • 此配置将会影响此进程中并发线程数的大小。

1.3 Java 程序对内存的使用

  • 从 JVM 调用的角度分析 Java 程序对内存空间的使用。

    • 当 JVM 进程启动的时候,会从类加载路径中找到包含 main 方法的入口类 HelloJVM。
    • 找到 HelloJVM 会直接读取该文件中的二进制数据,并且把该类的信息放到运行时的 Method 内存区域中。
    • 然后会定位到 HelloJVM 中的 main 方法的字节码中。
    • 并开始执行 main 方法中的指令。
  • 此时会创建 Student 实例对象,并且使用 student 来引用该对象(或者说给该对象命名)。

    • 第一步:JVM 会直接到 Method 区域中去查找 Student 类的信息,此时发现没有 Student 类,就通过类加载器加载该 Student 类文件。
    • 第二步:在 JVM 的 Method 区域中加载并找到了 Student 类之后会在 Heap 区域中为 Student 实例对象分配内存,并且在 Student 的实例对象中持有指向方法区域中的 Student 类的引用(内存地址)。
    • 第三步:JVM 实例化完成后会在当前线程中为 Stack 中的 reference 建立实际的对应关系,此时会赋值给 student 接下来就是调用方法。
  • 在 JVM 中方法的调用一定是属于线程的行为,也就是说方法调用本身会发生在线程的方法调用栈(Method Stack Frames)。

    • 线程的方法调用栈中,每一个方法的调用就是方法调用栈中的一个 Frame,该 Frame 包含了方法的参数,局部变量,临时数据等 student.sayHello()
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.spark.jvm;

    public class HelloJVM {

    // 在 JVM 运行的时候会通过反射的方式到运行时的 Method 区域找到入口方法 main。main 方法也是放在 Method 方法区域中的
    public static void main(String[] args) {

    // student (小写的) 是放在主线程中的 Stack 区域中的
    // Student 对象实例是放在所有线程共享的 Heap 区域中的
    Student student = new Student("spark");

    // 首先会通过 student 指针(或句柄)(指针就直接指向堆中的对象,句柄表明有一个中间的,student 指向句柄,句柄指向对象)
    // 找 Student 对象,当找到该对象后会通过对象内部指向方法区域中的指针来调用具体的方法去执行任务
    student.sayHello();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Student {

    // name 本身作为成员是放在 stack 区域的,但是 name 指向的 String 对象是放在 Heap 中
    private String name;

    public Student(String name) {
    this.name = name;
    }

    // sayHello 这个方法是放在方法区中的
    public void sayHello() {
    System.out.println("Hello, this is " + this.name);
    }
    }

2、垃圾回收机制

  • 垃圾收集被称为 GC,判断对象是否还存活进行回收,垃圾回收作用的区域就是堆区和方法区,主要是堆区。

  • 垃圾回收可以有效的防止内存泄漏,有效的利用可使用的内存,不用考虑内存的管理,但是自动内存管理对于检测内存泄漏和内存溢出问题更加困难。

2.1 引用计数法

  • 给每个创建的对象添加一个引用计数器,每当有一个地方引用此对象时,计数器就加 1,引用失效就减 1,为 0 表示对象不能被使用。
  • 缺点:如果 A 和 B 互相引用,却没被其他任何对象引用,那么这个两个对象其实已经是垃圾对象,但是他们引用数不为 0,无法回收。
  • Java 并没有使用此算法,所以循环引用的对象仍然会被回收。

2.2 可达性分析算法

  • 可达性分析算法是主流编程语言使用的垃圾回收算法。
  • 通过一系列的 “GC Roots” 的对象作为起始点,从起始点开始向下搜索到对象的路径。
  • 搜索所经过的路径称为引用链 (Reference Chain),当一个对象到任何 GC Roots 都没有引用链时,则表明对象 “不可达”,即该对象是不可用的。
  • Java 其实就是从各种根节点往下搜,不可达就是垃圾对象。

2.3 判断对象是否存活

  • 在可达性分析算法中判定为不可达的对象,也不是 “非死不可” 的,这时候它们暂时还处于 “缓刑” 阶段。
  • 要真正宣告一个对象死亡,至少要经历两次标记过程。

  • 第一次标记,对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,做第一次标记,标记之后再判断这个对象是否要执行 finalize() 方法,此方法就是为回收对象创造的逃脱机制。如果没有重写 finalize 方法或者 finalize 方法已经被虚拟机调用了,那就按原本计划下去,直到被回收。如果重写了 finalize() 方法,该对象将会被放置在一个名为 F-Queue 的 队列之中,之后收集器将对 F-Queue 中的对象进行第二次小规模的标记,如果此时对象重新与引用链上的任何一个对象建立关联,即可逃脱被垃圾回收的命运,由于 finalize 方法只会被执行一次,因此自救也只会有一次。

2.4 对象的引用

  • JDK1.2 之后,Java 对引用的概念做了扩充,将引用分为 强引用 (Strong Reference) 、软引用 (Soft Reference) 、弱引用 (Weak Reference) 和 虚引用 (Phantom Reference) 四种,这四种引用的强度依次递减。

  • 强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。比如我们常用的 A a = new A 就是强引用。

  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用:用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用相比较于软引用生命周期更短,且内存是否充足不影响回收。

  • 虚引用:最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列 ReferenceQueue 联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

3、OOM 内存溢出

  • OOM 全称 “Out Of Memory”,内存溢出,即 “内存用完了”,来源于 java.lang 包下的一个类 OutOfMemoryError

  • OOM 属于 Error,一般当 JVM 虚拟机内存不够用的时候,就会抛出该错误。

3.1 堆内存溢出

  • java.lang.OutOfMemoryError: java heap space 堆内存溢出,这个 OOM 是最常见的。

  • JVM 堆是 Java 存放对象的位置,当堆空间已经被占满到无法再创建新的对象时,就会抛出这个 OOM 错误。

  • 下面这个例子是通过不断给加大对象在堆内存中占用的空间,最终造成了堆内存溢出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 事先把堆内存配小,方便测试结果
    // -verbose:gc -Xms10M -Xmx10M -XX:MaxDirectMemorySize=5M -Xss160k -XX:+PrintGCDetails

    public class HeapSpaceTest {

    public static void main(String[] args) {

    String str = "test";

    // 起一个无限循环往 str 里加内容,直到撑爆堆
    while (true){
    str += str + new Random(1111111111) + new Random(1111111111);
    }
    }
    }
  • 可能会造成堆内存溢出的原因

    • 创建了一个超级大的对象。
    • 快速的创建海量的对象,直到最后来不及 GC。
    • 内存泄漏,一些对象创建之后没有释放,比如一些文件对象。
    • 过度使用 finalizer 终结器。
  • 堆内存溢出的解决方法

    • 使用 -Xmx 参数调高堆内存空间。
    • 对某些场景做限流,降低存在短期内创建海量对象的可能。
    • 排查是否出现内存泄漏。
  • finalizer 终结方法,是每个类都会有的特殊方法,当其被 JVM 调用时,就能帮助 JVM 清理资源。

    • 但是它有个问题就是:不保证及时性。
    • 如果你给类提供 finalizer 执行的时候,其开始到被 JVM 回收的这段时间是不可预见的.
    • 且 JVM 会延迟执行 finalizer,从而导致类一直无法被回收。
    • 如果量一大就会有堆内存溢出的风险。
    • 使用 finalizer 会使创建并销毁对象的时间变成原来的 400~500 倍。

3.2 元空间内存溢出

  • java.lang.OutOfMemoryError: Metaspace 元空间内存溢出。

  • hotspot 虚拟机在 java8 后,将原本的方法区(永久代)彻底移除,取而代之的是元空间(Metaspace)。

    • 常量池与静态变量转移到了堆中储存,元空间中只存了类的信息(class 定义,名称,方法),字节码文件等信息。
    • 元空间已经不占 JVM 虚拟机空间,而是使用本地内存。
    • 但是仍然有一个配置设置了元空间的大小,-XX:MaxMetaspaceSize
  • 造成内存溢出的原因

    • 当元空间中,加载的 class 数目太多或者体量太大时,占满元空间的时候。
    • 就会出现 OutOfMemoryError: Metaspace 元空间内存溢出的错误。
  • 解决方法

    • -XX:MaxMetaspaceSize 配置更大的元空间,一般用于启动时就报错的情况。
    • 重启 JVM 虚拟机,可能是一些应用没有重启导致了加载多份 class 的原因。
    • 设置 -XX:+CMSClassUnloadingEnabled-XX:+UseConcMarkSweepGC 这两个参数允许 JVM 卸载 class。因为 JVM 默认是不会卸载 class 的,但是可能会有程序动态创建太多 class 的情况出现。

3.3 永久代内存溢出

  • java.lang.OutOfMemoryError: Permgen space 永久代内存溢出。

  • 与上述元空间内存溢出类似,可使用 -XX:MaxPermSize 改动永久代大小。

3.4 方法区溢出

  • java.lang.OutOfMemoryError: GC overhead limit exceeded 方法区溢出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ConstantOutOfMemory {

    public static void main(String[] args) throws Exception {

    System.out.println("方法区内存溢出案例");

    try {
    int item = 0;
    List<String> strList = new LinkedList<String>();
    while (true) {
    strList.add(String.valueOf(item++).intern());
    }
    } catch (Exception e) {
    e.printStackTrace();
    throw e;
    }
    }
    }
  • 原因

    • 当内存基本被耗尽,JVM 虚拟机反复进行 GC 垃圾回收却几乎无法回收垃圾的时候,就会抛出这个错误。
    • 通俗的说就是,GC 垃圾收集器反复反复进行垃圾回收却没啥用的时候,它就放弃治疗了。
  • 解决方法

    • 其实与堆内存溢出情况类似,就算不出这个异常,再创建几个对象也会出现堆内存溢出的情况。
    • 因此解决方法可以参考堆内存溢出的解决方法。

3.5 本地线程过多

  • java.lang.OutOfMemoryError: unable to create native Thread 本地线程过多。

  • Java 底层是调用 native 本地方法创建的线程,但是当线程创建的数量到达机器允许的上限时就会触发该问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class NativeThreadTest {

    public static void main(String[] args) {

    for (int i = 0; ; i++) {
    System.out.println("Thread ["+i+"]");

    new Thread(() -> {
    try {
    // 让线程一直睡
    Thread.sleep(Integer.MAX_VALUE);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }).start(); // 注意:开个虚拟机验证,千万别在自己电脑跑!不然冒烟了别找我
    }
    }
    }
  • 注意:开个虚拟机验证,千万别在自己电脑跑!!!不然冒烟了别找我!

3.6 非 JVM 内存溢出

  • java.lang.OutOfMemoryError: Direct buffer memory

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 配置 -verbose:gc -Xms10M -Xmx10M -XX:MaxDirectMemorySize=10M -Xss160k -XX:+PrintGCDetails

    import java.nio.ByteBuffer;

    public class DirectMemoryOutOfMemory {

    private final static int ONE_GB = 1024*1024*1024;
    private static int count = 1;

    public static void main(String[] args) {

    try {
    while (true) {
    ByteBuffer buffer = ByteBuffer.allocateDirect(ONE_GB);
    count++;
    }
    } catch (Exception e) {
    System.out.println("Exception: instance create " + count);
    e.printStackTrace();
    } catch (Error e) {
    System.out.println("Error: instance create " + count);
    e.printStackTrace();
    }
    }
    }
  • Java 的 NIO 包中的 Direct Bytebuffer 可以直接访问堆外内存,搭配内存映射文件(MemoryMapFile),能够实现高速 IO 操作。

    • 但是,其实这个能够访问提堆外内存大小是有限制的。一般默认是 64M。
    • 所以使用 NIO 时一定要注意不要超过限制大小,否则会包该错误。
  • 解决方法

    • 可以通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值。

3.7 栈内存溢出

  • java.lang.StackOverflowError 栈内存溢出

  • 每个线程有自己的线程私有区,stack 就是线程私有的。

  • 当栈堆满的时候会出现栈溢出。

  • 例如循环调用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 配置 -verbose:gc -Xms10M -Xmx10M -XX:MaxDirectMemorySize=5M -Xss160k -XX:+PrintGCDetails

    public class StackOverFlow {

    private static int counter;

    public void count() {
    counter++;
    count(); // 循环调用
    }

    // main 方法发生占内存泄露
    public static void main(String[] args) {

    System.out.println("StackOverFlow");
    StackOverFlow sof = new StackOverFlow();

    try{
    sof.count();
    } catch (Exception e) {
    System.out.println("栈的深度:" + (++counter));
    e.printStackTrace();
    }
    }
    }

4、Tomcat 调优

4.1 测试 Tomcat 的吞吐量等信息

  • 修改 tomcat-users.xml 配置 Tomcat 管理用户。

  • 修改 webapps/manager/META-INF/context.xml 配置可以访问 Server Status。

  • Apache Jmeter 是开源的压力测试工具,我们借助于此工具进行测试,将测试出 Tomcat 的吞吐量等信息。

4.2 调整 Tomcat 参数进行优化

  • Tomcat 在 server.xml 中配置了两种连接器。

    • 一种监听 8080,负责和其他的 HTTP 服务器建立连接。
    • 一种监听 8009,负责和其他的 HTTP 服务器建立连接。
    • 第二种如要要用可以配置 nginx 实现负载均衡,所以可以直接注释掉。
  • 禁用 AJP 服务,AJP 是定向包协议。

  • 设置执行器 (线程池),频繁地创建线程会造成性能浪费,所以使用线程池来优化,通过修改 server.xml 文件。

  • 设置最大等待队列,默认情况下,请求发送到 Tomcat,如果 Tomcat 正忙,那么该请求会一直等待,可以设置超过就不等待,请求失败,降低服务器负载。

  • 设置 nio2 的运行模式,Tomcat 8 以前默认是 bio,同步并阻塞,性能低下无优化,8 之后是 nio,同步非阻塞,还有个性能更高的 nio2,异步非阻塞,可以更改设置。

5、JVM 调优

5.1 调整 JVM 参数进行优化

  • 设置并行垃圾回收器,提高并发效率。修改 catalina.sh。

  • 合理分配年轻代和老年代空间大小,通过 GC easy 查看 GC 日志文件,查看空间大小分配是否合理并调整优化。修改 catalina.sh。

  • 设置 G1 垃圾收集器,G1 的性能是非常强悍的,能用 G1 的情况下优先使用,能极大提高程序运行性能。修改 catalina.sh。

6、Linux 性能优化

  • Linux 性能的基本指标、工具,相应的观测、分析和调优方法。

  • 包括 CPU 性能、磁盘 I/O 性能、内存性能及网络性能。

  • 其实最终目的就是为了实现高并发和吞吐快,也就是延时和吞吐。

  • 调优的步骤

    • 1)选择指标评估应用程序和系统性能。
    • 2)设置性能目标。
    • 3)进行性能基准测试。
    • 4)性能分析,如果未达到目标,分析定位瓶颈。
    • 5)根据分析结果优化应用程序和系统。
    • 6)进行性能监控和阀值警告机制。
  • 性能优化方法论

    • 应用程序维度:吞吐量和请求延迟评估应用程序性能。
    • 系统资源维度:CPU 使用率评估系统 CPU 使用情况。
    • 多个性能问题同时存在,二八原则,即优先优化问题最大最重要的,也是提示最大的。
    • 多种优化方案,选择提升性能最明显的,复杂的优化方案会降低程序可维护性。

6.1 CPU 优化

6.1.1 性能统计

  • 平均负载率

    • 单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数,平均负载率高不一定 CPU 使用率也高,比如 I/O 密集架进程,等待 I/O。
    • 可运行状态的进程:正在使用 CPU 和等待 CPU 的进程,ps 指令,处于R状态的进程。
    • 不可中断状态的进程:处于内核态关键流程中的进程,如 I/O 响应,ps 指令,处于 D 状态的进程。

    • 查看负载命令 uptime

    • 主要看最后三个值,也就是 1 分钟,5 分钟,15 分钟平均负载,相差不大说明系统负载稳定。

    • 平均负载最理想的情况是等于 CPU 个数,从上图来说,如果系统只有 1 个 CPU,1 分钟的负载率就高达 501%,说明有多个进程正在争抢这个 CPU,已经超载,最佳方案是平均负载不能高于 CPU 数量的 70%。

    • Linux 系统压力测试工具 Stress

    • Linux 系统性能监控和分析工具 Sysstat

    • mpstat 命令:实时查看每个 CPU 的性能指标以及所有 CPU 的平均指标。

    • pidstat 命令:实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。

  • 上下文切换

    • Linux是 多任务操作系统,支持远大于 CPU 数量的任务同时运行,通过频繁的上下文切换,将 CPU 轮流分配给不同任务。CPU 上下文切换就是保存当前任务的上下文,即 CPU 寄存器和程序计数器,加载新任务的上下文到 CPU 寄存器和程序计数器中,调转程序计数器所指的新位置,运行新任务,保存的上下文存储在系统中,通过任务重新调度执行加载。

    • 上下文切换时机,CPU 会给每一个进程划分时间片,时间片耗尽上下文切换。

    • 进程资源不足,如内存不够,就挂起
    • 通过 sleep 函数休眠
    • 有优先级更高的进程
    • 发生故障,硬件中断。

    • 从 Ring 0 内核空间即内核态到 Ring 3 用户空间即用户态权限递减,即 Ring 0 能访问所有。

    • 进程的上下文切换:一个进程到另一个进程

    • 系统调用:同一个进程运行,即上面模型图中的特权模式切换,即内核态和用户态切换,比如读取一个文件,先是把用户开发文件,保存用户态指令,然后切换到内核态执行,再切换回用户态响应结果。
    • 进程上下文切换:进程是资源拥有的结伴单位,切换就两步,保存上下文,切换上下文。
    • CPU 挑选进程运行原则:每个 CPU 都维护了一个等待队列,按优先级和CPU等待时间排序,即优先级和先入先出原则执行进程。
    • 线程上下文切换:线程是调度的基本单位,切换分两种,两个线程属于不同进程和进程切换一致,属于同一进程,共有资源不动,只切换私有资源。
    • 中断上下文切换:受到中断信号,保存当前进程,等待中断结束继续运行,中断优先级比进程上下文切换高,并发也不会同时发生。

    • 通过 vmstat 工具分析上下文切换情况及中断次数

    • 命令 vmstat 3 5 即 3 秒打印一次结果一共输出5次

    • 查看进程对应情况命令 pidstat -u -w 3

  • CPU 使用率

    • 单位时间内 CPU 的使用情况,百分比显示。CPU 使用率 = 1-(空间时间 / 总 CPU 时间),性能工具是按间隔时间求平均得到使用率。
    • top (总体)、ps(所有进程)、pidstat(单个进程)是最常用性能分析工具

6.1.2 调优策略

  • CPU 优化分应用程序维度和系统的角度,即代码工程优化和系统参数设置优化。

  • 应用程序优化:排除所有不必要操作保留核心逻辑

    • 1)编译器优化,大部分编译器都提高优化选项,开启之后,在编译阶段就能对工程优化,提高性能。
    • 2)减少循环的次数、减少递归、减少动态内存分配
    • 3)算法优化,使用复杂度更低的算法。
    • 4)异步处理,提高程序并发处理能力,避免程序因等待资源一直阻塞,比如把轮询替换为事件通知。
    • 5)多线程代替多进程,多线程之前的切换比多进程切换成本低
    • 6)多用缓存,经常访问的数据和计算过程的步骤放入缓存,加快程序处理速度。
  • 系统角度:利用 CPU 缓存的本地性并控制进程的 CPU 使用情况

    • 1)进程优先级调整
    • 2)进程设置资源限制,避免过多消耗系统资源
    • 3)中断负载均衡,任何中断处理程序都会消耗大量 CPU 资源,把中断处理过程均衡分配到多个 CPU 上。

6.2 内存优化

6.2.1 性能统计

  • 要先知道内存的各个内存量,才能根据指标进行优化,分系统内存和进程内存。

  • 系统内存使用情况 free 命令 free -h -c 2 -s 2 间隔两秒输出两次 并人性化输出所有信息。

  • 从头开始列名含义:总内存大小、已使用内存大小、未使用内存大小、共享内存大小、缓存和缓冲区内存大小、新进程可用内存大小(包含未使用和可回收内存)。

  • Buffer 是对磁盘数据的缓存,Cache 是文件数据的缓存,读写请求都可以使用。

  • 缓存和缓冲区的缓存命中率越高,使用缓存带来的收益就会越高,应用程序的性能就会越好。

  • 通过 top 指令查看进程内存。

6.2.2 调优策略

  • 调优策略

    • 1)禁止 Swap,使用本地内存空间,避免服务器因物理内存不够用,使用 Swap 分区空间。
    • 2)减少内存的动态分配,使用如内存池,大页等。
    • 3)尽量使用缓存和缓冲区来访问数据。如 redis 组件。
    • 4)使用 cgroups 限制进程内存使用情况。
    • 5)通过 /proc/pid/oom_adj 调整核心应用的 oom_score,设置为 0,保证内存紧张,核心应用不会被 OOM 杀死。

6.3 磁盘 IO 优化

6.3.1 性能统计

  • 文件数据的储存:超级块、索引节点区、数据块区

  • 超级块:整个文件系统的状态

  • 索引节点区:存储索引节点,便于查找

  • 数据块区:存储文件的数据

  • 统计参数

    • 1)每秒 IO 数(IOPS):每秒磁盘连续读次数和连续写次数之和。
    • 2)吞吐量(Throughput):硬盘传输数据流的速度,即读写数据之和。
    • 3)平均 IO 数据尺寸:吞吐量 / IO 数目。
    • 4)磁盘活动时间百分比:磁盘处于活动时间的比率,即磁盘利用率。
    • 5)服务时间:磁盘进行读和写执行时间。
    • 6)IO 等待队列长度:等待磁盘处理的 IO 请求长度。
    • 7)等待时间:磁盘读或写等待执行的时间。
  • IO 性能观测工具 iostat,通过他查看磁盘的使用率、IOPS、吞吐量,使用命令 iostat -d -x 1

6.3.2 调优策略

  • 应用程序优化策略

    • 1)追加写代替随机写。
    • 2)利用缓存 IO 降低 IO 次数。
    • 3)使用 redis 外部缓存。
    • 4)频繁读写同一块磁盘空间,使用 mmap 代替 read/write,减少内存拷贝次数。
    • 5)同步写场景,将请求合并,不要多次请求同步写入磁盘。
    • 6)多应用程序共享磁盘,使用 cgroups 进行限制。
  • 文件系统优化策略

    • 1)如果做了负载均衡,选择最适配的文件系统,如 Ubuntu 默认 ext4,CentOS 7 默认 xfs。
    • 2)优化文件系统配置。
    • 3)优化文件系统缓存。
  • 磁盘优化策略

    • 1)选择更好的磁盘,如固态硬盘 SSD 代替机械硬盘 HDD。
    • 2)使用 RAID 把多块磁盘组合成一个矩阵逻辑磁盘。
    • 3)选择最合适的 IO 调度算法,如虚拟机和 SSD 使用 noop 调度算法,数据库应用可以改为 deadline 算法。
    • 4)对应用程序数据进行磁盘级别隔离。
    • 5)读场景较多,增大磁盘预读数据。
    • 6)如果对磁盘操作频繁,调整磁盘队列的长度,增大磁盘吞吐量。
文章目录
  1. 1. 前言
  2. 2. 1、JVM 架构
    1. 2.1. 1.1 JVM 架构图分析
    2. 2.2. 1.2 JVM 性能调优参数
    3. 2.3. 1.3 Java 程序对内存的使用
  3. 3. 2、垃圾回收机制
    1. 3.1. 2.1 引用计数法
    2. 3.2. 2.2 可达性分析算法
    3. 3.3. 2.3 判断对象是否存活
    4. 3.4. 2.4 对象的引用
  4. 4. 3、OOM 内存溢出
    1. 4.1. 3.1 堆内存溢出
    2. 4.2. 3.2 元空间内存溢出
    3. 4.3. 3.3 永久代内存溢出
    4. 4.4. 3.4 方法区溢出
    5. 4.5. 3.5 本地线程过多
    6. 4.6. 3.6 非 JVM 内存溢出
    7. 4.7. 3.7 栈内存溢出
  5. 5. 4、Tomcat 调优
    1. 5.1. 4.1 测试 Tomcat 的吞吐量等信息
    2. 5.2. 4.2 调整 Tomcat 参数进行优化
  6. 6. 5、JVM 调优
    1. 6.1. 5.1 调整 JVM 参数进行优化
  7. 7. 6、Linux 性能优化
    1. 7.1. 6.1 CPU 优化
      1. 7.1.1. 6.1.1 性能统计
      2. 7.1.2. 6.1.2 调优策略
    2. 7.2. 6.2 内存优化
      1. 7.2.1. 6.2.1 性能统计
      2. 7.2.2. 6.2.2 调优策略
    3. 7.3. 6.3 磁盘 IO 优化
      1. 7.3.1. 6.3.1 性能统计
      2. 7.3.2. 6.3.2 调优策略
隐藏目录