环境准备

  • JDK
    • JavaDevelopmentKit
    • 开发Java应用程序的开发工具集合
      • JRE JavaRuntimeEnvironment
        • JVM JavaVirtualMachine虚拟机
          • JVM规范要求
          • 满足JVM规范要求的一种具体实现(一种计算机程序)
          • 一个JVM运行实例
        • 核心类
        • 支持文件
      • Java解释器
      • Java编译器Javac
      • Java归档jar
      • Java文档生成器Javadoc
    • 分类
      • OpenJDK
      • OracleJDK
  • 关系
    • JDK = JRE + 开发工具
    • JRE = JVM + 类库

JDK.png
Java开发.png

常用性能指标

解决流程

  1. 分析系统性能问题
    1. 是否达到预期
    2. 资源层面是否有问题
    3. 系统业务流程、技术流程是否有问题
  2. 通过工具收集系统状态
    1. 打点、日志
    2. 监控关键性能指标数据
  3. 根据结果分析、调优
    1. 资源配置调整
    2. 持续监控和分析

      资源分类

  • CPU
    • 浪费和过载都不是理想状态
  • 内存
    • 资源有限
    • GC配置不合理容易产生OOM
  • IO

    • 存储和网络
    • 分布式环境

      常用套路

  • 28原则

  • 首先排查基础资源是否是瓶颈
    • 如何成本可控,加资源是最快速的解决方案
    • 如果资源没到瓶颈,需要进一步分析
  • 与JVM相关的资源
    • CPU
    • 内存
  • 常用手段

    • 使用JDWP或开发工具做本地/远程调试
    • 系统和JVM状态监控,收集指标
    • 性能分析:CPU/内存分配
    • 日志分析:Dump分析/GC日志
    • 调整JVM启动参数/GC策略

      衡量系统性能维度

      常用分类1

  • 延迟

    • Latency
    • 一般衡量的是响应时间ResponseTime
    • 一般要看的也是95线、99线,即95%/99%用户的响应时间
    • 用户访问量大的时候,对网络有任何的抖动都会导致最大响应时间非常大,这时就会忽略此指标
  • 吞吐量
    • Throughput
    • 一般对交易类的系统使用TPS(每秒处理的事务数)来衡量
    • 对于查询搜索类的也可以使用QPS(每秒处理的请求数)来衡量
  • 系统容量

    • Capacity
    • 也叫设计容量,可以理解为硬件配置,成本约束

      常用分类2

  • 业务需求指标

    • 响应时间(RT)
    • 吞吐量(TPS/QPS)
    • 并发数
  • 资源约束指标

    • CPU
    • 内存
    • IO

      JVM基础知识

      编程语言类型

      编程语言分类

      语言分类.png
  • 机器语言

    • 二进制编码指令
    • 执行速度快
  • 汇编语言
    • 指令编程
  • 高级语言

    • 开发人员友好
    • 需要编译或解释执行,转换成汇编码或机器码交给计算机执行

      高级语言分类

  • 虚拟机

    • 有虚拟机 Java / Lua / Ruby等
    • 无虚拟机 C / C++ / C# / Golang等
  • 类型
    • 静态类型 Java / C / C++
    • 动态类型 所有脚本类型的语言
  • 执行环境
    • 编译执行 Java / C / C++(编译产生的是中间类型的代码,不是机器码,Java编译成字节码 Java bytecode)
    • 解释执行 JavaScript / Python 等
  • 特点

    • 面向过程 C 等
    • 面向对象 Java /C++等
    • 函数式编程 Lisp等

      跨平台

  • 一般解释型语言都是跨平台

  • 编译型存在两种级别的跨平台

    • 源码跨平台
      • C++
      • 一次编写,到处编译(到处调试、找依赖、改配置、改版本)
      • C++源码跨平台.png
    • 二进制跨平台
      • Java字节码
      • 一次编写,一次编译
      • Java字节码.png

        运行时与虚拟机

  • JRE就是Java的运行时,包括虚拟机和相关的库

  • JVM启动时需要加载核心库,然后加载业务程序字节码

    内存管理和垃圾回收

  • 内存管理

    • 申请
    • 压缩
    • 回收
  • JVM在代码创建对象时分配新内存,并使用GC算法自动进行垃圾回收
  • 分类

    • C,相信程序员,惯着程序员,内存完全有程序员管理,但内存泄漏、野指针等问题多
    • JAVA,不相信程序员,但惯着程序员,由GC进行管理,但会stop the world
    • Rust,不相信程序员,也不惯着程序员,必须清晰的管理好自己的代码,否则不编译

      Java字节码技术

      简介

  • Java bytecode是JVM的指令集,作为中间代码交给JVM执行

  • 由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode),实际上只用了200左右
  • 操作码(指令)
    • 类型前缀 ( i代表integer )
    • 操作名称 ( iadd代表对整数进行加法运算)
  • 根据指令的性质,主要分为四大类

    • 栈操作指令,包括与局部变量交互的指令
    • 对象操作指令,包括方法调用指令
    • 流程控制指令
    • 算术运算以及类型转换指令
    • 执行专门任务的指令 ,比如同步指令synchronization,抛出异常指令throw 等

      获取字节码清单

  • javap工具获取class文件中的指令清单

  • javap专门用于反编译class文件

    1. Desktop javap -c Hello
    2. Compiled from "Hello.java"
    3. public class Hello {
    4. public Hello();
    5. Code:
    6. 0: aload_0
    7. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
    8. 4: return
    9. public static void main(java.lang.String[]);
    10. Code:
    11. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    12. 3: ldc #3 // String Hello JVM
    13. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    14. 8: return
    15. }

    解读字节码清单

  • 默认的构造函数,是Java编译器生成的,不是在运行时JVM自动生成的

    • 构造函数都会先调用super类的构造函数
    • 这不是JVM自动执行的,也是由指令控制的,所以默认构造函数里也有一些字节码指令
    • 基本上这几条指令就是执行 super()
  • 其中解析的java/lang/Object 就是默认继承的Object类

    查看class文件中的常量池信息

  • 常量池

    • ConstantPool,大多数是指运行时常量池
    • 主要是由class文件中的常量池结构体组成,使用编号的方式把用到的各类常量统一管理起来
    • N就是常量编号,该文件中其他地方可以引用

    • 查看常量池需要 -verbose
    • = 等号就是分隔符 //代表注释 ```shell ➜ Desktop javap -c -verbose Hello Classfile /Users/prief/Desktop/Hello.class Last modified 2020-10-17; size 413 bytes MD5 checksum 84e6dff93470c06b67bbb3cc92a7e7e1 Compiled from “Hello.java” public class Hello minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool:

      1 = Methodref #6.#15 // java/lang/Object.”“:()V

      2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;

      3 = String #18 // Hello JVM

      4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V

      5 = Class #21 // Hello

      6 = Class #22 // java/lang/Object

      7 = Utf8

      8 = Utf8 ()V

      9 = Utf8 Code

      10 = Utf8 LineNumberTable

      11 = Utf8 main

      12 = Utf8 ([Ljava/lang/String;)V

      13 = Utf8 SourceFile

      14 = Utf8 Hello.java

      15 = NameAndType #7:#8 // ““:()V

      16 = Class #23 // java/lang/System

      17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;

      18 = Utf8 Hello JVM

      19 = Class #26 // java/io/PrintStream

      20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V

      21 = Utf8 Hello

      22 = Utf8 java/lang/Object

      23 = Utf8 java/lang/System

      24 = Utf8 out

      25 = Utf8 Ljava/io/PrintStream;

      26 = Utf8 java/io/PrintStream

      27 = Utf8 println

      28 = Utf8 (Ljava/lang/String;)V

      { public Hello(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1
      0: aload_0
      1: invokespecial #1                  // Method java/lang/Object."<init>":()V
      4: return
      
      LineNumberTable: line 1: 0

    public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code:

    stack=2, locals=1, args_size=1
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello JVM
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
    LineNumberTable:
      line 3: 0
      line 4: 8
    

    } SourceFile: “Hello.java”

    <a name="0m8ly"></a>
    ## 查看方法信息
    

    public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V //这里是方法描述 flags: ACC_PUBLIC, ACC_STATIC Code:

    stack=2, locals=1, args_size=1
    

    ```

  • 方法描述

    • 小括号内是入参信息/形参信息
    • 左方括号表示数组
    • L表示对象
    • 后面的java/lang/String就是类名称
    • 小括号后面的V表示这个方法的返回值是void
  • 方法的访问标志
    • flags: ACC_PUBLIC, ACC_STATIC 表示public 和static
  • Code
    • stack 执行该方法时栈深度是2
    • locals 需要局部变量表中保留1个槽位
    • args_size 方法参数的个数
  • 方法的签名包括
    • 方法的修饰符
    • 方法的名称
    • 参数类型清单
    • 返回值类型
  • 默认生成的无参构造函数的参数个数不是0,1表示this

    • 如果是静态方法则没有this引用
    • 如果是非静态方法,this会被分配到局部变量表的0号槽位
    • 对应反射编程的 Method#invoke(Object obj, Object… args)
    • 对应JS中的 fn.apply(obj, args) && fn.call(obj, arg1, arg2…)

      线程栈与字节码执行模型

  • JVM是一台基于栈的计算机器

  • 每个线程都有一个属于自己的线程栈 JVM stack,用于存储栈帧Frame
  • 每一次方法调用,JVM都会自动创建一个栈帧
  • 栈帧组成
    • 操作数栈,是一个LIFO结构的栈,用于压入和弹出值,大小也在编译时确定
    • 局部变量数组,也称局部变量表LocalVariableTable,包括方法的参数和局部变量,编译时已经确定
    • class引用,指向当前方法在常量池中对应的class

栈帧.png

方法体中的字节码解读

 Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello JVM
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
  • 字节码指令前的数字

    • 间隔不等是因为一部分操作码会附带有操作数,也会占用字节码数组中的空间
    • new会占用3个槽位,一个放操作码指令自身,两个放操作数
    • 每个操作码/指令都有对应的HEX字符串表示方式,可以在HEX Editor中查看

      对象初始化指令

      new

  • 当同时看到new,dup,invokespecial指令在一起时,就是在创建类的实例对象

    • new指令只是创建对象,但没有调用构造函数
    • dup指令用于复制栈顶的值,构造函数返回后可以把实例赋值给某个字段
      • astore {N} 或 astore_{N} 赋值给局部变量,N表示位置
      • putfield 赋值给实例字段
      • putstatic 赋值给静态字段
    • invokespecial指令用来调用某些特殊方法,当然这里就是调用构造函数

      init

  • 调用构造函数前执行 init

    clinit

  • 如果尚未初始化,还可能需要类的静态初始化方法,即clinit

  • 但clinit不能被直接调用,而是由这些指令触发

    • new # 创建类的新实例
    • getstatic # 访问静态字段
    • putstatic # 访问静态字段
    • invokestatic # 调用静态方法

      栈内存操作指令

  • swap 指令交换栈顶2个元素的值

  • dup 指令复制栈顶元素的值,并压入栈
  • pop 指令删除栈顶元素的值
  • dup_x1 指令复制栈顶元素的值,并插入最上面2个值的下方

    ..., value2, value1 →  
    ..., [value1,] value2, value1
    
  • dup2_x1 指令复制栈顶1个64位或2个32位的值,并将复制的值按照原始顺序插入原始值下面一个32位值的下方 ```

    情景1: value1, value2, and value3都是分组1的值(32位元素)

    …, value3, value2, value1 → …, [value2, value1,] value3, value2, value1

情景2: value1 是分组2的值(64位,long或double), value2 是分组1的值(32位元素)

…, value2, value1 → …, [value1,] value2, value1 ```

局部变量表

  • LocalVariableTable
  • stack主要用于执行指令,局部变量则用来保存中间结果
  • astore_1 指令将引用地址值 addr 存储 store到编号slot 为1的局部变量表 LocalVariableTable中
  • dstore 4 指令中的d 表示目标变量的类型是 double,保存到slot为4的局部变量表 LocalVariableTable中
  • store类的指令都会删除栈顶值
  • load 类的指令会将值从局部变量表压入栈,但并不删除局部变量表中的值

    流程控制指令

  • 主要用在分支和循环中,异常处理的语句也算流程控制

    • if-then-else
    • for/ while
    • try / catch /throw

      方法调用指令和参数传递

  • invokespecial 指令调用构造函数,也可调用同一个类的private方法以及可见的超类方法

  • invokestatic 指令调用静态方法
  • invokevirtual 指令调用公共,受保护和打包私有方法,针对具体的类型方法表是固定的情况效率优于interface
  • invokeinterface 指令是当要调用的方法属于某个接口时

    invokedynamic

  • JDK7增加的实现“动态类型语言”

  • 也是JDK8支持lambda表达式的基础
  • 不改变字节码的情况下,想调用一个类A的方法m,只有2个方法

    • A a = new A(); a.m()
    • A.class.getMethod.invoke反射调用

      Java类加载器

      类的生命周期和加载过程

      类的生命周期.png
  • 加载

    • 找到class文件
    • 如果找不到二进制表示形式,则会抛出NoClassDefFound错误
  • 校验
    • 确保class文件里的字节流信息符合虚拟机要求,不会危害虚拟机安全
    • 检查classfile的语义,判断常量池中的符号,执行类型检查,判断字节码的合法性
    • 可能抛出VerifyError,ClassFormatError,UnsupportedClassVersionError,ClassCircularityError等
  • 准备
    • 创建静态字段并将其初始化为标准的默认值
    • 分配方法表即在方法区中分配这些变量所使用的内存空间
  • 解析
    • 解析常量池
      • 类或接口的解析
      • 字段解析
      • 类方法解析
      • 接口方法解析
    • 加载一个class时,需要加载所有的super类和super接口
  • 初始化

    • 必须在类的首次主动使用时才能执行类的初始化
    • 类的初始化过程包括
      • 类构造方法
      • static 静态变量赋值语句
      • static 静态代码块
    • java中初始化一个类,必然先初始化过java.lang.Object类

      类加载时机

  • 虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类

  • 子类的初始化会触发父类的初始化

    类加载器机制

  • 系统自带的类加载器

    • 启动类加载器 BootstrapClassLoader
    • 扩展类加载器 ExtClassLoader
    • 应用类加载器 AppClassLoader
  • 类加载机制3个特点
    • 双亲委托,委托自己的父加载器加载
    • 负责依赖,每个类都会加载自己的依赖
    • 缓存加载,避免重复加载

类加载器.png
自定义加载器.png

自定义类加载器示例

  • 对加载数据、加载数据的格式进行自定义处理
  • 只要通过classloader返回一个Class实例即可
  • 比如简单加密的字节码的类加载器,保护字节码文件不被直接破解

    Java内存模型JMM

    JVM内存结构

  • Java内存模型分为两个部分

    • JVM内存结构
    • JMM与线程规范
  • JVM内存结构
    • 线程栈 ThreadStack
      • 保存了调用链上正在执行所有方法的局部变量
      • 每个线程都只能访问自己的线程栈
      • 每个线程都不能访问其他线程的局部变量
      • 所有原生类型的局部变量都存储在线程栈中,对其他线程不可见
      • 如果是对象引用,栈中的局部变量槽位中保留着对象的引用地址,实际对象的内容保存在堆中
      • 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生的局部变量本身
      • 总结:方法中使用的原生数据类型和对象引用地址存在栈上
    • 堆内存 Heap
      • 堆内存中包含了代码中创建的所有对象,不管是哪个线程创建的
      • 不管是创建一个对象并把其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都会保存在堆内存中
      • 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值还是对象引用
      • 类的静态变量和类定义一样都保存在堆中
      • 总结:对象、对象成员与类定义、静态变量都在堆上

JVM.png

栈内存结构

  • 每启动一个线程,JVM就会在栈空间栈分配对应的线程栈,比如1M的空间(-Xss1m)
  • 线程执行过程中,一般会有多个方法组成调用栈StackTrace
  • 每执行一个方法都会创建对应的栈帧Frame

栈内存结构.png
栈帧.png

堆内存结构

堆内存.png

  • S0和S1就是SurvivorSpace存活区,用在GC算法里
  • TLAB是对新生代的优化,ThreadLocalAllocationBuffer,给每个线程一小块空间,创建的对象先在这里分配,满了再换,极大降低并发资源锁定的开销
  • Non-Heap本质也是Heap,只是不归GC管理

    • metaspace以前叫持久代PermanentGeneration,Java8里换了名字叫Metaspace
    • CSS CompressedClassSpace 存放class信息和metaspace有交叉
    • CodeCache 存放JIT编译后的本地机器代码

      CPU指令与乱序执行

  • 计算机支持的指令

    • 精简指令集计算机RISC
      • ARM芯片,功耗低,运算能力较弱
    • 复杂指令集计算机CISC
      • Intel的X86芯片系列,功耗高,性能强劲

        JMM简介

  • 能被多个线程共享使用的内存称为“共享内存”或“堆内存”

  • 所有的对象(包括内部的实例变量),static变量,以及数组,都必须放到堆内存中
  • 局部变量,方法的入参/形参,异常处理语句的入参不允许在线程之间共享
  • 多个线程同时对一个变量访问(读取/写入)时,这时候只要有某个线程执行的是写操作,那么就会发生“冲突”
  • 可以被其他线程影响或感知的操作,称为线程间的交互行为,可分为

    • 读取
    • 写入
    • 同步操作
      • volatile变量的读写
      • monitor的锁定与解锁
      • 线程的起始操作与结尾操作
      • 线程的启动与结束
    • 外部操作
      • 对线程执行环境之外的操作,比如停止其他线程等

        内存屏障简介

  • 控制可见性

    • 读屏障
    • 写屏障
  • 目的用来短暂屏蔽CPU的指令重排功能,和CPU约定,看见这些指令就不要打乱操作顺序
  • 常见的内存屏障
    • LoadLoad 屏障前的Load指令执行完才能执行屏障后的Load指令

    • StoreStore

    • LoadStore 遇到此屏障 CPU短暂屏蔽掉指令重排功能

    • StoreLoad 屏障之前执行所有的Store,屏障后执行Load都能获取到最新值,代价最高