JVM入门笔记
# JVM基础概念
JVM 全称是 Java Virtual Machine 翻译过来是 Java 虚拟机,用于执行 Java 字节码文件,顾名思义它是一个专属于 Java 的虚拟机程序,任何可以安装 JDK 的操作系统都可以执行 Java 程序,这是就是 Java 的跨平台
JVM 主要有四部分组成,它的作用执行字节码文件,所以要先使用类加载器将字节码文件读取并保存到运行时数据区(JVM 的内存),保存到内存后由执行引擎执行 Java 代码,除了这三个之外 JVM 中还包含本地接口,内部封装着 C/C++ 语言编写的代码,Java 的源码中带有 native 关键字的方法调用的就是本地接口中的方法
JIT即时编译
其他编程语言例如 C/C++ 编写的程序会编译为计算机可识别的机器码,也就是直接编译成可执行文件(例如exe),而 Java 编译后的 Class 文件并不是可执行文件,需要 JVM 将它解释为机器码文件才可执行,中间多了一个步骤就导致 Java 程序的执行效率会低一些,JIT 即时编译 就是这个问题的解决方案,通过对程序运行状态进行分析,将高频执行的代码解释为机器码后保存到内存中,节省了反复解释造成的多余的性能开销
# 字节码文件
JVM 的作用是执行字节码文件,字节码并不支持直接用文本编辑器打开浏览,这里介绍三种方案:
- 可用 JDK 自带的
javap -v xxx.class
在控制台上查看,线上环境可通过jar -xvf xxx.jar
解压 jar 包后查看 - 可以使用 jclasslib (opens new window) 图形化工具查看字节码文件,相比控制台查看更方便
- 使用阿里巴巴的 arthas.jar (opens new window) 运维工具,在线上动态查看正在运行的 jar 包,以及动态替换字节码文件等等
- dashboard -i 5000 -n 10,查看程序数据面板,每个 5 秒刷新一次,刷新 10 次后结束
- dump -d /data/java/ site.hanzhe.Application,将 Application 这个字节码文件保存到指定目录下
- jad /data/java/ site.hanzhe.Application,反编译该字节码文件,查看 Java 源代码
- 更多命令参数详查官方文档 (opens new window)
Java 的 .class 字节码文件大致由五部分组成:
- 基础信息:包含魔数、字节码文件对应的 Java 版本号、访问标识符、父类和接口等等信息等等
- 常量池:保存字符串常亮、类或接口名、字段名等等
- 字段:当前类中声明的字段信息
- 方法:当前类中方法的字节码指令信息
- 属性:类源码的文件名、包含哪些内部类等等
Magic 魔数
一个文件的扩展名并不能决定文件的类型,因为扩展名可以随意修改,通常来说程序识别文件类型会根据该文件的前几个字节来判断,例如 PNG 图片开头四个字节为89 50 4E 47
,在 UTF-8 码表中对应字母 PNG
Java 字节码文件中前四个字节为CA FE BA BE
为“咖啡宝贝”的谐音,这是文件头进制数,用于区分文件类型,也成为魔数
主副版本号
在 jclasslib 主界面的一般信息中可以看到主版本号和副版本号,副版本号用来标识小版本这个不重要
主版本号用来标识大版本,JDK1.2 的版本号为46,之后每升级一次版本号就递增,用版本号减去 44 就能得到 JDK 版本
- 46 - 44 = 2,JDK 版本为 1.2
- 52 - 44 = 8,JDK 版本为 1.8
记录一些常见的 JVM 指令,随时补充
指令 | 解释 |
---|---|
iconst_值 | 将值放到操作数栈中 |
istore_下标 | 将操作数栈中的值存放到对应下标的变量中 |
iload_下标 | 将对应下标的值读取到操作数栈中 |
iadd | 操作数栈中的值相加 |
iinc 下标 by 值 | 将指定下标变量的值进行增加,by 后面的值是多少就加多少 |
# 类的生命周期
类的生命周期主要分为五个阶段:加载 → 链接 → 初始化 → 使用 → 卸载
# 加载阶段
类加载器会读取读取字节码文件将它保存到 JVM 的内存中,这里分别用到了 JVM 内存中的方法区和堆内存,类的基础信息会在方法区中保存为一个 InstanceKlass 对象,同时在堆内存中保存一个 class 对象,类中的静态变量就是存放在堆内存中的
Java 安装目录下的 lib 目录中有个sa-jdi.jar
文件,文件中提供了 HSDB 调试工具可以查看内存信息,通过jsp
命令可以获取到运行的 Java 进程 ID,在 Tools/ObjectHistogram 菜单中根据类的权限定名可以看到该类的内存信息
启动 HSDB 工具的命令:java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
# 链接阶段
链接阶段由三部分组成:验证 准备 解析
验证:校验字节码是否符合规范,例如是否为正确的字节码文件,版本是否对应,指令是否正确等等
准备:为当前类中所有的静态变量设置初始值,如果是被 final 修饰的静态变量则直接设置正确的值
解析:将字节码中的符号引用,替换为直引用
# 初始化阶段
# 内存结构
# 程序计数器
程序计数器,英文名是 Program Counter Register,是线程私有内存,基于寄存器实现,主要作用是记录下一个要执行的 JVM 指令,具体体现为:
- 当线程执行过程中 CPU 离开后重新调度时,能够接着上次执行的位置继续向下执行
- 当方法 a 调用方法 b 时,b 方法执行结束后能回到调度的位置,继续向下执行
# 虚拟机栈
栈内存,又被称为虚拟机栈,英文名是 JVM Stacks,为线程工作提供运行空间是线程私有内存
- 栈内存中主要用来存储栈帧,例如方法调用为
a→b→c
每个方法对应一个栈帧,三个栈帧依次入栈,先进后出 - 栈帧中存储方法运行中产生的一些数据,例如局部变量
- 方法嵌套调用过多或者错误的递归调用容易导致
Stack Overflow
栈内存溢出 - 栈内存大小默认为1MB,可以通过 Xss 设置栈内存大小,例如
-Xss2048k
,需要注意的是物理机内存是有容量限制的,栈内存分配的空间越大那么可创建的线程数量就越少,分配内存前须谨慎考虑
CPU高占用检测方案
- 使用
top
命令查看 Java 项目对应的 PID - 使用
ps H -eo pid,tid,%CPU,%MEM | grep PID
查看该进程的所有线程 ID - 将线程ID(TID)转十六进制后,使用
jstack
命令查看线程信息
# 本地方法栈
本地方法栈,英文名是 Native Method Stacks,为本地方法提供运行空间是线程私有内存
本地方法栈的作用与虚拟机栈类似,不同的是本地方法栈为本地方法工作,指的是 Java 源码中常见的使用native
关键字的代码,JVM 通过本地方法实现执行 C/C++ 的代码
# 堆
堆内存,英文名是 Heap,是线程共享内存,通过 new 关键字创建的对象都使用堆内存,有垃圾回收机制不定期清理内存
- 堆内存没有默认大小,可以通过
-Xms
设置堆初始内存大小,-Xmx
设置堆最大内存大小
堆内存诊断工具
jmap -heap PID
jconsole
可视化工具,通过PID连接进程进行监控jvisualvm
可视化工具,通过PID连接进程进行监控
# 方法区
方法区,英文名Method Area
,是线程共享内存,主要存储类的结构信息,例如变量,方法以及构造等信息