- 环境准备
- 常用性能指标
- JVM基础知识
- Java字节码技术
- N就是常量编号,该文件中其他地方可以引用
- 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
- 情景1: value1, value2, and value3都是分组1的值(32位元素)
- 情景2: value1 是分组2的值(64位,long或double), value2 是分组1的值(32位元素)
- Java类加载器
- Java内存模型JMM
- LoadLoad 屏障前的Load指令执行完才能执行屏障后的Load指令
- StoreStore
- LoadStore 遇到此屏障 CPU短暂屏蔽掉指令重排功能
- StoreLoad 屏障之前执行所有的Store,屏障后执行Load都能获取到最新值,代价最高
环境准备
- JDK
- JavaDevelopmentKit
- 开发Java应用程序的开发工具集合
- JRE JavaRuntimeEnvironment
- JVM JavaVirtualMachine虚拟机
- JVM规范要求
- 满足JVM规范要求的一种具体实现(一种计算机程序)
- 一个JVM运行实例
- 核心类
- 支持文件
- JVM JavaVirtualMachine虚拟机
- Java解释器
- Java编译器Javac
- Java归档jar
- Java文档生成器Javadoc
- JRE JavaRuntimeEnvironment
- 分类
- OpenJDK
- OracleJDK
- 关系
- JDK = JRE + 开发工具
- JRE = JVM + 类库
常用性能指标
解决流程
- CPU
- 浪费和过载都不是理想状态
- 内存
- 资源有限
- GC配置不合理容易产生OOM
IO
28原则
- 首先排查基础资源是否是瓶颈
- 如何成本可控,加资源是最快速的解决方案
- 如果资源没到瓶颈,需要进一步分析
- 与JVM相关的资源
- CPU
- 内存
常用手段
延迟
- Latency
- 一般衡量的是响应时间ResponseTime
- 一般要看的也是95线、99线,即95%/99%用户的响应时间
- 用户访问量大的时候,对网络有任何的抖动都会导致最大响应时间非常大,这时就会忽略此指标
- 吞吐量
- Throughput
- 一般对交易类的系统使用TPS(每秒处理的事务数)来衡量
- 对于查询搜索类的也可以使用QPS(每秒处理的请求数)来衡量
系统容量
业务需求指标
- 响应时间(RT)
- 吞吐量(TPS/QPS)
- 并发数
资源约束指标
机器语言
- 二进制编码指令
- 执行速度快
- 汇编语言
- 指令编程
高级语言
虚拟机
- 有虚拟机 Java / Lua / Ruby等
- 无虚拟机 C / C++ / C# / Golang等
- 类型
- 静态类型 Java / C / C++
- 动态类型 所有脚本类型的语言
- 执行环境
- 编译执行 Java / C / C++(编译产生的是中间类型的代码,不是机器码,Java编译成字节码 Java bytecode)
- 解释执行 JavaScript / Python 等
特点
一般解释型语言都是跨平台
编译型存在两种级别的跨平台
JRE就是Java的运行时,包括虚拟机和相关的库
-
内存管理和垃圾回收
内存管理
- 申请
- 压缩
- 回收
- JVM在代码创建对象时分配新内存,并使用GC算法自动进行垃圾回收
分类
Java bytecode是JVM的指令集,作为中间代码交给JVM执行
- 由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode),实际上只用了200左右
- 操作码(指令)
- 类型前缀 ( i代表integer )
- 操作名称 ( iadd代表对整数进行加法运算)
根据指令的性质,主要分为四大类
javap工具获取class文件中的指令清单
javap专门用于反编译class文件
➜ Desktop javap -c HelloCompiled from "Hello.java"public class Hello {public Hello();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Hello JVM5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return}
解读字节码清单
默认的构造函数,是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
LineNumberTable: line 1: 00: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
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
JVM是一台基于栈的计算机器
- 每个线程都有一个属于自己的线程栈 JVM stack,用于存储栈帧Frame
- 每一次方法调用,JVM都会自动创建一个栈帧
- 栈帧组成
- 操作数栈,是一个LIFO结构的栈,用于压入和弹出值,大小也在编译时确定
- 局部变量数组,也称局部变量表LocalVariableTable,包括方法的参数和局部变量,编译时已经确定
- class引用,指向当前方法在常量池中对应的class
方法体中的字节码解读
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,dup,invokespecial指令在一起时,就是在创建类的实例对象
-
clinit
如果尚未初始化,还可能需要类的静态初始化方法,即clinit
但clinit不能被直接调用,而是由这些指令触发
swap 指令交换栈顶2个元素的值
- dup 指令复制栈顶元素的值,并压入栈
- pop 指令删除栈顶元素的值
dup_x1 指令复制栈顶元素的值,并插入最上面2个值的下方
..., value2, value1 → ..., [value1,] value2, value1dup2_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 类的指令会将值从局部变量表压入栈,但并不删除局部变量表中的值
流程控制指令
主要用在分支和循环中,异常处理的语句也算流程控制
invokespecial 指令调用构造函数,也可调用同一个类的private方法以及可见的超类方法
- invokestatic 指令调用静态方法
- invokevirtual 指令调用公共,受保护和打包私有方法,针对具体的类型方法表是固定的情况效率优于interface
invokeinterface 指令是当要调用的方法属于某个接口时
invokedynamic
JDK7增加的实现“动态类型语言”
- 也是JDK8支持lambda表达式的基础
不改变字节码的情况下,想调用一个类A的方法m,只有2个方法
加载
- 找到class文件
- 如果找不到二进制表示形式,则会抛出NoClassDefFound错误
- 校验
- 确保class文件里的字节流信息符合虚拟机要求,不会危害虚拟机安全
- 检查classfile的语义,判断常量池中的符号,执行类型检查,判断字节码的合法性
- 可能抛出VerifyError,ClassFormatError,UnsupportedClassVersionError,ClassCircularityError等
- 准备
- 创建静态字段并将其初始化为标准的默认值
- 分配方法表即在方法区中分配这些变量所使用的内存空间
- 解析
- 解析常量池
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
- 加载一个class时,需要加载所有的super类和super接口
- 解析常量池
初始化
虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类
-
类加载器机制
系统自带的类加载器
- 启动类加载器 BootstrapClassLoader
- 扩展类加载器 ExtClassLoader
- 应用类加载器 AppClassLoader
- 类加载机制3个特点
- 双亲委托,委托自己的父加载器加载
- 负责依赖,每个类都会加载自己的依赖
- 缓存加载,避免重复加载
自定义类加载器示例
- 对加载数据、加载数据的格式进行自定义处理
- 只要通过classloader返回一个Class实例即可
-
Java内存模型JMM
JVM内存结构
Java内存模型分为两个部分
- JVM内存结构
- JMM与线程规范
- JVM内存结构
- 线程栈 ThreadStack
- 保存了调用链上正在执行所有方法的局部变量
- 每个线程都只能访问自己的线程栈
- 每个线程都不能访问其他线程的局部变量
- 所有原生类型的局部变量都存储在线程栈中,对其他线程不可见
- 如果是对象引用,栈中的局部变量槽位中保留着对象的引用地址,实际对象的内容保存在堆中
- 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生的局部变量本身
- 总结:方法中使用的原生数据类型和对象引用地址存在栈上
- 堆内存 Heap
- 堆内存中包含了代码中创建的所有对象,不管是哪个线程创建的
- 不管是创建一个对象并把其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都会保存在堆内存中
- 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值还是对象引用
- 类的静态变量和类定义一样都保存在堆中
- 总结:对象、对象成员与类定义、静态变量都在堆上
- 线程栈 ThreadStack
栈内存结构
- 每启动一个线程,JVM就会在栈空间栈分配对应的线程栈,比如1M的空间(-Xss1m)
- 线程执行过程中,一般会有多个方法组成调用栈StackTrace
- 每执行一个方法都会创建对应的栈帧Frame
堆内存结构

- S0和S1就是SurvivorSpace存活区,用在GC算法里
- TLAB是对新生代的优化,ThreadLocalAllocationBuffer,给每个线程一小块空间,创建的对象先在这里分配,满了再换,极大降低并发资源锁定的开销
Non-Heap本质也是Heap,只是不归GC管理
计算机支持的指令
能被多个线程共享使用的内存称为“共享内存”或“堆内存”
- 所有的对象(包括内部的实例变量),static变量,以及数组,都必须放到堆内存中
- 局部变量,方法的入参/形参,异常处理语句的入参不允许在线程之间共享
- 多个线程同时对一个变量访问(读取/写入)时,这时候只要有某个线程执行的是写操作,那么就会发生“冲突”
可以被其他线程影响或感知的操作,称为线程间的交互行为,可分为
控制可见性
- 读屏障
- 写屏障
- 目的用来短暂屏蔽CPU的指令重排功能,和CPU约定,看见这些指令就不要打乱操作顺序
- 常见的内存屏障






