JVM基础知识

编程语言

  • 分类
    • 面向过程、面向对象、面向函数
    • 静态类型、动态类型
    • 编译执行、解释执行
    • 有虚拟机、无虚拟机
    • 有GC、无GC(GarbageCollection)
  • 发展过程
    • 机器语言
    • 编程语言
    • 高级语言
      • Java是面向对象、静态类型、编译执行、有VM、有GC的运行时跨平台的高级语言
  • 跨平台
    • 源码跨平台

源码跨平台.png

  • 二进制跨平台

二进制跨平台.png

  • Java、C++、Rust区别

    • C/C++ 相信程序员,并且惯着程序员,让程序员自己管理内存,但很容易造成内存泄漏,导致程序崩溃
    • Java/Golang 完全不相信程序员,但惯着程序员,所有内存都有JVM统一管理,程序员友好
    • Rust语言既不相信程序员,也不惯着程序员,写代码时就要管理好内存,不易理解学习成本高

      字节码、类加载器、虚拟机

      字节码类加载器虚拟机关系.png

      Java字节码技术

      字节码

  • Java bytecode是由单字节的指令组成

  • 理论上支持256个操作码opcode
  • 实际只用了200多个opcode
  • 根据指令的性质,主要分4大类
    • 栈操作指令,包括与局部变量交互的指令
    • 对象操作指令,包括方法调用
    • 程序流程控制指令
    • 算术运算以及类型转换指令

      生成字节码

      ```java package demo.jvm0104;

public class HelloBytecode{ public static void main(String[] args){ HelloBytecode obj = new HelloBytecode(); } }

```shell
# 编译
javac demo/jvm0104/HelloBytecode.java

# 查看字节码
javap -c -verbose demo.jvm0104.HelloBytecode

字节码运行时结构

  • JVM是一台基于栈的计算机器
  • 每个线程都有属于自己的线程栈JVM Stack,用于存储栈帧 Frame
  • 每一次方法调用JVM都会自动产生一个栈帧
  • 栈帧组成
    • 操作数栈
    • 局部变量数组
    • Class引用,指向常量池中对应的Class

字节码运行时结构.png

  • 栈计算

栈计算.png

助记符到二进制

  • 字节码单字节指令对应的语义化的表示为助记符

助记符到二进制.png

数值处理与本地变量表

  • 红框中的store/load _后面的1、2、3对应本地变量表中的槽位slot中的1、2、3
  • 1、2、3都是简写指令,所以直接用_连接,超过3后分开写,如24行的 dstore 4
  • istore/iload 对应 int类型的存储到本地变量表和加载到栈
  • astore/aload 对应 对象类型的存储到本地变量表和加载到栈
  • iconst_1 / iconst_2 表示常量 1 和 2 ,对应代码中的1 和 2
  • dstore 4 表示把 double 类型的变量写到本地变量表中4号 slot 中,对应的变量名为avg 类型是 double
  • i2d 表示 int转换称 double

本地变量表.png

算术操作与类型转换指令

  • Java本身有比较多的类型但在字节码中只有5个类型,大都被int表示
    • int
    • float
    • long
    • double
    • a 表示对象引用
  • 一个int 32位 4个byte
  • 一个float 32位 4个byte
  • 一个long 64位 8个byte
  • 一个double 64位 8个byte

算术操作与类型转换.png

程序流程控制指令

  • if_icmpge 43 判断 int 对比 greater equal ,如果满足则jump到#43,如果不满足则继续向下
  • iinc int increase
  • goto 18 跳转到#18

循环控制.png

方法调用指令

  • invokestatic
    • 调用某个类的静态方法,速度最快
  • invokespecial
    • 调用构造方法,也可以调用同一个类的private方法,以及可见的超类方法
  • invokevirtual
    • 如果是具体类型的目标对象,用于调用public、protected和package级的private方法
    • virtual虚方法,Java中都是虚方法,子类重写了父类的方法后都无法再调用到父类的方法
  • invokeinterface
    • 通过接口引用来调用方法时会编译成此指令
  • invokedynamic

    • JDK7新增的指令,实现动态类型语言支持而进行的升级,同时也是JDK8以后支持的lambda表达式的基础

      相关示例

  • stack 表示运行时需要的栈深度

  • locals 表示局部变量表中的槽位数
  • args_size 表示参数个数
  • 程序计数器PC表示运行时代码执行到第几行(偏移量)

image.png
方法操作例子.png

JVM类加载器

类的生命周期

  1. 加载 Loading :找到class文件
  2. 验证 Verification:验证格式、依赖
  3. 准备 Preparation:静态字段、方法表
  4. 解析 Resolution:符号解析为引用
  5. 初始化 Initialization:构造器、静态变量赋值、静态代码块
  6. 使用 Using
  7. 卸载 Unloading

类的生命周期.png

类的加载时机

  1. 当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类;
  2. 当遇到用以新建目标类实例的new 指令时,初始化new指令的目标类,就是new一个类的时候要初始化;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化;
  6. 如果一个接口定义了default方法,那么直接实现或间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射API对某个类进行反射调用时,初始化这个类,跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
  8. 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类;

    不会初始化(可能会加载)

  9. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化;

  10. 定义对象数组,不会触发该类的初始化,数组中只是存在对象指针;
  11. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类;
  12. 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化;
  13. 通过Class.forName加载指定类时,如果指定参数initialize为false时,不会初始化;
  14. 通过ClassLoader默认的loadClass方法,不会触发初始化动作,只加载不初始化;

    三类加载器

  • 启动类加载器 BootstrapClassLoader
    • 加载JVM依赖的核心系统类
    • java.lang / java.util
  • 扩展类加载器 ExtClassLoader
    • JDK配置的扩展类路径里的jar或class
  • 应用类加载器 AppClassLoader
    • 应用的jar或class

类加载器.png

自定义加载器

  • 自定义加载器可以对同一个类产生不同的作用
  • 实现同一个类的模块化、版本化管理
  • 扩展加载器和应用加载器在Java9之前都是通过URLClassLoader加载的,URLClassLoader是通过ClassLoader加载的
  • URLClassLoader.png
  • 可以显示当前ClassLoader都加载了哪些Jar
  • 文件夹内所有的class文件和资源文件用zip方式压缩到一起就是jar包
  • 显示classLoader加载了Jar.png
  • 自定义classLoader
  • 自定义classLoader.png

    加载器特点

  • 双亲委托(向上委托)

  • 负责依赖
  • 缓存加载

    添加引用类的几种方式

  • 放到JDK的lib/ext目录下,或者 -Djava.ext.dirs

  • java -cp/classpath 或者class文件放到当前路径
  • 自定义classLoader加载
  • 拿到当前执行类的ClassLoader,反射调用addUrl方法添加Jar或路径(JDK9之后无效,但有更简单的Class.forName(className,initFlag,classLoader)方法)

addUrl.png

JVM内存模型

内存堆栈结构

  • 每个线程都只能访问自己都线程栈
  • 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的
  • 线程可以将一个原生类型的变量值的副本传给另一个线程,但不能共享原生类型的局部变量
  • 堆内存中包含了Java代码中创建的所有对象(包括包装类型Byte/Long等),不管是哪个线程创建的
  • 不管是创建一个对象并将其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都保存在堆内存中

JVM内存结构.png

  • 如果是原生类型的局部变量,那么他的内容就全部保存在线程栈上
  • 如果是对象引用,栈中局部变量槽位中保存的对象的引用地址,对象的实际内容保存在堆上
  • 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生类型还是引用类型
  • 类的静态变量则和类定义一样都保存在堆中

JMM.png

  • 总结

    • 方法中使用的原生类型和对象引用地址在栈上存储
    • 对象、对象的成员、类、类的静态变量在堆上存储
    • 堆内存又称为“共享堆”,堆中所有的对象都可以被所有线程访问
    • 如果一个线程可以访问某个对象,就可以访问对象的成员变量
    • 如果两个线程同时调用某个对象的同一个方法,则他们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的

      内存整体结构

  • 每启动一个线程,JVM就会在栈空间分配对应的线程栈,比如1MB的线程栈空间(-Xss1m)

    • JVM常见的三个参数
      • Xmx 最大堆内存(第二个Heap的内存通常设置总内存70%,栈/非堆/堆外/JVM自身/系统占30%)
      • Xms 最小堆内存(第二个Heap的内存,JVM启动起来就分配的内存,通常和Xmx一致)
      • Xss 栈空间大小
  • 线程栈也叫方法栈,如果使用了JNI方法,则会分配一个单独的本地方法栈(NativeStack)
  • 线程执行过程中,一般有多个方法组成调用栈StackTrace,每执行到一个方法,就会创建对应的栈帧Frame

内存整体结构.png

栈内存结构

  • 栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本能确定
    • 返回值需要空间存放
    • 局部变量需要空间
    • 操作数栈
    • class指针,标识这个栈帧对应的是哪个类的方法,指向非堆的Class对象

栈内存结构.png

堆内存结构

  • 堆内存是所有线程共用的内存空间
  • JVM将Heap内存分为两部分
    • 年轻代 Young generation
      • 伊甸区新生儿Eden space
      • 存活区Survivor space(S0S1总有一个是空,YoungGC时就是清空一个,把留存的整理到另一个)
        • S0
        • S1
    • 老年代 Old generation也叫Tenured
      • 年轻代里的对象被几次GC后仍然存活的转移到老年代
      • 超大对象的分配直接分配到老年代
  • Non-Heap本质上还是Heap,只是一般不归GC管理,里面划分了3个内存池
    • Metaspace
      • 之前叫持久代Permanent generation
      • java8后换了名字 Metaspace
      • 对象的结构,常量池
    • CSS CompressedClassSpace,存放class信息的,和Metasapce有交叉
    • CodeCache,存放JIT编译后的本地机器码

堆内存结构.png

CPU与内存行为

  • CPU乱序执行(多核并发乱序执行片段)
  • volatile关键字
  • 原子性操作
  • 内存屏障

cpu与内存行为.png

总结

  • JMM JavaMemoryModel
  • JMM规范明确定义了不同的线程之间,通过哪些方式,在什么时候可以看见其他线程保存到共享变量中的值,以及如何在必要的时候对共享变量的访问进行同步,屏蔽各种硬件和操作系统之间的内存访问差异,实现Java并发程序真正的跨平台
  • JMM规范的是线程间的交互操作,不管线程内局部变量的操作
  • 所有的对象、对象的成员、static变量,数组都必须保存在堆内存中
  • 局部变量、方法形参、入参、异常处理语句的入参不允许在线程间共享,不受内存模型的影响
  • 多个线程同时对一个变量访问时,只要有一个线程执行的是写操作,就会发生“冲突”
  • 可以被其他线程影响或感知的操作,称为线程间的交互行为,主要有

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

        JVM启动参数

        命令行分类

  • VM Options

    • java命令启动时传入的选项options
    • -Dfile.encoding=UTF-8 -Da=1
  • Program Arguments

    • java命令启动时传入的参数args
    • 对应main方法里的args参数
      # java [options] classname [args]
      # java [options] -jar filename [args]
      # jps -v
      # jps -m
      

      形式分类

  • 标准参数

    • 所有JVM都要实现,并且向后兼容
    • -D设置系统属性
  • 非标准参数
    • 默认JVM实现这些参数的功能,但不保证所有的JVM都完全实现,也不保证向后兼容
    • -X开头的基本都是传给JVM的
    • java -X查看当前JVM支持的非标准参数
  • 非稳定参数

    • 专门用于控制JVM的行为,跟具体的JVM实现有关,随时可能被取消
    • -XX: [+-]Flags形式,+-是对布尔值进行开关
    • -XX: key=value形式,指定某个选项的值

      作用分类

  • 系统属性参数

    • -D
    • 命令行:A=100 java …
    • System.getProperty(“A”)/System.setProperty(“A”,”100”)
    • 设置环境变量
  • 运行模式参数
    • -server
      • 启动速度慢
      • 运行时性能和内存管理效率很高
      • 适用于生产环境
      • 64位机器上默认此模式
    • -client
      • 启动速度快
      • 运行时性能和内存管理效率不高
      • 适用于调试环境
    • -Xint
      • 解释模式运行
      • JVM解释执行所有的字节码
      • 降低运行速度
    • -Xcomp
      • 编译模式执行
      • JVM第一次使用时会把所有的字节码编译成本地代码
      • 提高运行效率,但要注意预热
    • -Xmixed
      • 解释模式和编译模式混合
      • JVM默认模式,也是推荐模式
      • java -version可以看到此模式信息
  • 堆内存设置参数
    • -Xmx
      • 指定最大堆内存,比如-Xmx4g
      • 限制了Heap部分最大值为4g,这个不包括栈内存和堆外内存
    • -Xms
      • 指定初始堆内存大小,比如-Xms4g
      • 并不是操作系统立刻分配初始值,而是GC先规划好,用到时才分配
      • 服务器上需要-Xms和-Xmx一致,否则应用刚启动可能就有很多FullGC,堆内存扩容时也会性能抖动
    • -Xss
      • 设置每个线程栈的大小
      • 如-Xss1m就是指定线程栈为1MB
      • 与-XX:ThreadStackSize=1m等价
    • -Xmn
      • 等价于-XX:NewSize
      • 使用G1垃圾回收器不应该设置此选项
      • 官方建议设置为-Xmx的1/2-1/4
    • -XX:MaxPermSize=size
      • JDK1.7之前使用的,对应PermanentGeneration大小
      • Java8默认允许的Meta空间无限大,此参数无效
    • -XX:MaxMetaspaceSize=size
      • Java8默认不限制Meta空间,一般不允许设置此选项
    • -XX:MaxDirectMemorySize=size
      • 系统可以使用的最大堆外内存
      • 这个参数和-Dsun.nio.MaxDirectMemorySize效果相同
  • GC设置参数
    • -XX:+UseSerialGC 使用串行垃圾回收器
    • -XX:+UseParallelGC 使用并行垃圾回收器
    • -XX:+UseG1GC 使用G1垃圾回收器
    • -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
    • -XX:+UnlockExperimentalVMOptions -XX:+UseZGC // Java11
    • -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC // Java12+
  • 分析诊断参数
    • -XX:+HeapDumpOnOutOfMemoryError OOM时自动Dump堆内存
    • -XX:HeapDumpPath=PATH 与上面的开关配合使用,指定Dump路径,默认为java启动目录
    • -XX:OnError
      • 发生致命错误FatalError时执行的脚本,例如执行一些命令或curl一个请求
      • java -XX:OnError=”gdb -%p” MyApp # %p表示PID
    • -XX:OnOutOfMemoryError 发生OOM时执行的脚本
    • -XX:ErrorFile=filename 指定致命错误的日志文件名
  • JavaAgent参数
    • Agent是JVM中一项黑科技,可以通过无侵入方式做很多事情
      • 注入AOP代码
      • 执行统计
    • -agentlib:libname[=options] 启用native方式的agent,参考LD_LIBRARY_PATH路径
    • -agentpath:pathname[=options] 启用native方式的agent
    • -javaagent:jarpath[=options] 启用外部的agent库
    • -Xnoagent 禁用所有agent
    • JAVA_OPTS=”-agentlib:hprof=cpu=samples,file=cpu.samples.log” 开启CPU使用时间抽样分析