JVM知识体系

123 阅读17分钟

Java虚拟机

内存区域

程序计数器

较小的内存空间 可以理解为当前线程所执行的字节码的行号指示器 如果当前线程执行的是一个Java方法,计数器里面记录的是正在执行的字节码指令的地址;如果当前线程正在执行的是一个Native方法,计数器则为空(Undefined) jvm内存区域中唯一一块不会发生OutOfMemoryError异常的区域

虚拟机栈

描述的是Java方法执行的内存模型 线程私有 生命周期与线程相同 会发生StackOverflowError和OutOfMemoryError异常。当线程请求的栈深度大于虚拟机所允许的栈深度,就会抛出StackOverflowError异常;当需要扩展更多内存但又无法申请到的时候,就会发生OutOfMemoryError异常

  • 栈帧

    每个方法执行时都会创建一个栈帧,一个方法从调用到完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

    • 局部变量表

      • 基本数据类型
      • 对象引用(reference类型)
      • returnAddress类型(字节码指令的地址,很少用)
    • 操作数栈

      举个例子:例如i + j,会将局部变量中的i和j分别压入操作数栈,然后将操作数栈的前两个元素相加,得到计算结果

    • 动态链接(符号地址,例如某个方法在内存中的地址)

    • 方法出口(记录了方法的出口地址,便于返回)

本地方法栈

同虚拟机栈类似,只不过虚拟机栈是为Java方法服务,而本地方法栈是为Native方法服务

Java虚拟机中管理的最大的一块内存 线程共享 当需要更多内存且又无法扩展时,则会发生OutOfMemoryError异常

方法区

线程共享 jdk1.7使用永久代来实现方法区;到jdk1.8之后取消永久代,使用元数据区来实现方法区,并将方法区中的字符串常量池和静态变量移到堆中 元数据区是使用本地内存实现,不受jvm内存区域的限制,和本地内存直接相关

  • 已被虚拟机加载的类信息
  • 常量(字符串的常量池在jdk1.8后被移到堆中)
  • 静态变量(jdk1.8后被移到堆中)
  • 即时编译器编译后的代码

对象内存分配、内存结构以及访问

对象的创建

  • 指针碰撞
  • 空闲列表

对象的内存布局

  • 对象头

    • 对象自身运行时数据(Mark Word)

      • 哈希码(HashCode)
      • GC分代年龄
      • 锁状态标志
      • 线程持有的锁
      • 偏向线程ID
      • 偏向时间戳
      • ...
    • 类型指针(执行方法区存储的类信息)

    • 记录数组长度的数据(只有数组才有)

  • 实例数据

  • 对齐填充(对象的大小必须是8字节的整数倍)

对象的访问定位

  • 句柄访问
  • 指针访问(HotSpot虚拟机采用这种方式)

垃圾收集

对象是否可回收算法

  • 引用计数算法(循环引用有问题)
  • 可达性分析算法(普遍使用,选择GC Roots对象作为起始点,从上到下查找。例如:虚拟机栈中的局部变量表引用的对象,其它对象到该GC Roots对象没有指针连接,这些对象称之为不可达)

引用的强弱分类

  • 强引用(例如:Object obj = new Object()这种)
  • 软引用(使用Softreference类)
  • 弱引用(使用WeakReference类)
  • 虚引用(使用PhantomReference类)

垃圾收集算法

  • 标记 - 清除算法

    原理:算法分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 缺陷:这个算法主要有两个不足。第一:效率问题,标记和清除两个过程的效率都不高,第二:空间问题,会导致出现大量不连续的内存碎片,当需要分配大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收

  • 复制算法

    原理:将可用内存按照容量分为大小相等的两块,每次只使用其中的一块。当这一快的内存用完了,就当存活着的对象复制到另外一块上,然后再把已使用的内存空间一次清理掉 缺陷:可使用内存仅为总内存的一半,造成很大的资源浪费

  • 标记 - 整理算法

    原理:标记的步骤同标记 - 清除算法一样,但是标记 - 整理的算法不是对可回收对象直接进行清理,而是让所有存活的对象都同一方向移动,移动完成后,直接清理掉边界外的内存 缺点:效率较低

  • 分代收集算法

    原理:根据对象存活周期的不同将内存划分,一般是将Java堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代中,大多对象都是朝生夕死,只有少量对象存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中对象存活率比较高,没有额外的担保空间就必须采用标记-清理或者标记整理的算法进行回收

    • 新生代(复制算法)
    • 老年代(标记 - 清除或标记 - 整理算法)
    • 永久代(jdk1.8后被移除,不再有永久代)

垃圾收集器

  • Serial收集器(用于新生代,单线程收集)

  • ParNew收集器(用于新生代,多线程收集)

  • Parallel Scavenge收集器(用于新生代,多线程收集,关注吞吐量)

  • Serial Old收集器(用于老年代,单线程收集,使用标记整理算法)

  • Parallel Old收集器(用于老年代,多线程,使用标记 - 整理算法)

  • CMS收集器

    CMS收集器对CPU资源非常敏感,因为CMS收集器有大量的并发操作 无法处理浮动垃圾,因为并发清除的时候,用户线程也在工作,在此期间会产生浮动垃圾 采用标记 - 清除算法,导致内存碎片不断增多

    • 初始标记(停止所有用户线程)

      标记一下GC Roots能直接关联到的对象,速度很快

    • 并发标记(和用户线程同步进行)

      GC Roots Tracing的过程,从GC Roots对象往下找,找到那些不可达的对象,并进行标记

    • 重新标记(停止所有用户线程)

      因为并发标记时用户线程也在工作,修正那些发生变动的标记

    • 并发清除(和用户线程同步进行)

      因为并发清除的时候,用户线程也在工作,在此过程会产生浮动垃圾

  • G1收集器(jdk1.9默认)

    并行与并发 G1垃圾收集器采用标记 - 整理算法,不会产品内存空间碎片 可预测的停顿,因为垃圾回收器是在用户期望的时间内,回收部分区域的对象 G1内部采用Region的方式来划分堆内存区域,而且G1建立了可预测的停顿时间模型,可以根据用户想要的停顿时间去回收(回收那些价值比较高的Region区域)

    • 初始标记(停止所有用户线程)
    • 并发标记(和用户线程同步进行)
    • 最终标记(停止所有用户线程)
    • 筛选回收(停止用户线程,在用户期望的时间内,回收部分Region相关的对象,回收时间可控)

GC类型

  • Minor GC(新生代GC)
  • Major GC(老年代GC)
  • Full GC(全局GC,包含新生代、老年代)

性能调优

性能指标

  • 停顿时间(垃圾收集器做垃圾回收中断应用执行的时间,G1中使用-XX:MaxGCPauseMillis参数)
  • 吞吐量(垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n) ,G1中使用-XX:GCTimeRatio=n参数)

调优目标

  • 加快Full GC的执行时间(减少停顿时间)
  • 降低Full GC的频率(提升吞吐量)

具体方法

  • 打印GC日志(-XX:+PrintGCDetails  -XX:+PrintGCTimeStamps  -XX:+PrintGCDateStamps  -Xloggc:./gc.log)
  • 分析GC日志得到关键性指标(分析Full GC、Minor GC情况等等)
  • 分析GC原因,调优JVM参数(如:-XX:MetaspaceSize调整元数据空间大小等等jvm参数)
  • 也可以使用gceasy这个网站来分析GC日志,并分析它提供了性能指标报表和调优建议,还可以使用使用gcviewer这个工具来分析GC日志

工具

  • jinfo(查看Java应用程序的扩展参数)

    • 查看jvm参数(jinfo -flags 进程ID)
    • 查看java系统参数(jinfo - sysprops 进程ID)
  • jstat(查看堆内存各部分的使用量,以及加载类的数量)

    • 类加载统计(jstat -class 进程ID)

      • Loaded:加载class的数量
      • Bytes:所占用空间大小
      • Unloaded:未加载数量
      • Bytes:未加载占用空间
      • Time:时间
    • 垃圾回收统计(jstat -gc 进程ID)

      • S0C:第一个幸存区的大小
      • S1C:第二个幸存区的大小
      • S0U:第一个幸存区的使用大小
      • S1U:第二个幸存区的使用大小
      • EC:伊甸园区的大小
      • EU:伊甸园区的使用大小
      • OC:老年代大小
      • OU:老年代使用大小
      • MC:方法区大小(元空间)
      • MU:方法区使用大小
      • CCSC:压缩类空间大小
      • CCSU:压缩类空间使用大小
      • YGC:年轻代垃圾回收次数
      • YGCT:年轻代垃圾回收消耗时间
      • FGC:老年代垃圾回收次数
      • FGCT:老年代垃圾回收消耗时间
      • GCT:垃圾回收消耗总时间
    • 新生代垃圾回收统计(jstat -gcnew 进程ID)

      • S0C:第一个幸存区的大小
      • S1C:第二个幸存区的大小
      • S0U:第一个幸存区的使用大小
      • S1U:第二个幸存区的使用大小
      • TT:对象在新生代存活的次数
      • MTT:对象在新生代存活的最大次数
      • DSS:期望的幸存区大小
      • EC:伊甸园区的大小
      • EU:伊甸园区的使用大小
      • YGC:年轻代垃圾回收次数
      • YGCT:年轻代垃圾回收消耗时间
    • 老年代垃圾回收统计(jstat -gcold 进程ID)

      • MC:方法区大小
      • MU:方法区使用大小
      • CCSC:压缩类空间大小
      • CCSU:压缩类空间使用大小
      • OC:老年代大小
      • OU:老年代使用大小
      • YGC:年轻代垃圾回收次数
      • FGC:老年代垃圾回收次数
      • FGCT:老年代垃圾回收消耗时间
      • GCT:垃圾回收消耗总时间
    • 堆内存统计(jstat -gccapacity 进程ID)

      • NGCMN:新生代最小容量
      • NGCMX:新生代最大容量
      • NGC:当前新生代容量
      • S0C:第一个幸存区大小
      • S1C:第二个幸存区的大小
      • EC:伊甸园区的大小
      • OGCMN:老年代最小容量
      • OGCMX:老年代最大容量
      • OGC:当前老年代大小
      • OC:当前老年代大小
      • MCMN:最小元数据容量
      • MCMX:最大元数据容量
      • MC:当前元数据空间大小
      • CCSMN:最小压缩类空间大小
      • CCSMX:最大压缩类空间大小
      • CCSC:当前压缩类空间大小
      • YGC:年轻代gc次数
      • FGC:老年代GC次数
    • 新生代内存统计(jstat -gcnewcapacity 进程ID)

      • NGCMN:新生代最小容量
      • NGCMX:新生代最大容量
      • NGC:当前新生代容量
      • S0CMX:最大幸存1区大小
      • S0C:当前幸存1区大小
      • S1CMX:最大幸存2区大小
      • S1C:当前幸存2区大小
      • ECMX:最大伊甸园区大小
      • EC:当前伊甸园区大小
      • YGC:年轻代垃圾回收次数
      • FGC:老年代回收次数
    • 老年代内存统计(jstat -gcoldcapacity 进程ID)

      • OGCMN:老年代最小容量
      • OGCMX:老年代最大容量
      • OGC:当前老年代大小
      • OC:老年代大小
      • YGC:年轻代垃圾回收次数
      • FGC:老年代垃圾回收次数
      • FGCT:老年代垃圾回收消耗时间
      • GCT:垃圾回收消耗总时间
    • 元数据空间统计(jstat -gcmetacapacity 进程ID)

      • MCMN:最小元数据容量
      • MCMX:最大元数据容量
      • MC:当前元数据空间大小
      • CCSMN:最小压缩类空间大小
      • CCSMX:最大压缩类空间大小
      • CCSC:当前压缩类空间大小
      • YGC:年轻代垃圾回收次数
      • FGC:老年代垃圾回收次数
      • FGCT:老年代垃圾回收消耗时间
      • GCT:垃圾回收消耗总时间
    • gc通用分析(jstat -gcutil 进程ID)

      • S0:幸存1区当前使用比例
      • S1:幸存2区当前使用比例
      • E:伊甸园区使用比例
      • O:老年代使用比例
      • M:元数据区使用比例
      • CCS:压缩使用比例
      • YGC:年轻代垃圾回收次数
      • FGC:老年代垃圾回收次数
      • FGCT:老年代垃圾回收消耗时间
      • GCT:垃圾回收消耗总时间
  • jmap(查看内存信息)

    • 查看类实例个数以及占用内存大小(jmap -histo 进程ID)
    • 查看堆信息(jmap -heap 进程ID)
    • 堆内存dump(map -dump:format=b,file=文件名 进程ID)。也可以设置内存溢出自动导出dump文件,-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
  • jstack(查看线程,可用于排查死锁,命令:jstack 进程ID)

  • jvisualvm(可视化工具,利用了上面4个命令获取监控信息,可分析dump文件)

  • jconsole(可视化工具,和jvisualvm类似)

  • gceasy(gceasy.io/)gc日志分析

  • gcviewer(gc日志分析)

  • HeapHero(dump文件分析)

类文件结构

魔数(u4,4字节),例如:0xCAFEBABE

次版本号(u2,2字节)

主版本号(u2,2字节)

常量池数量(u2,2字节)

常量池

访问标志(如public、final等等,u2,2字节)

类索引(类信息的索引位置,u2)

父类索引(父类信息的索引位置,u2)

当前类实现接口的数量(u2)

接口索引集合(u2 * 接口数量)

字段数量(u2)

字段

  • 字段访问标识(u2,2字节)
  • 字段名称的索引(名称位于常量池中,u2)
  • 字段描述的索引(描述位于常量池中,u2)
  • 属性数量(u2)
  • 属性信息(用于存储字段的额外信息)

方法数量(u2)

方法

  • 方法访问标识(u2,2字节)
  • 方法名称的索引(名称位于常量池中,u2)
  • 方法描述的索引(描述位于常量池中,u2)
  • 属性数量(u2)
  • 属性信息(用于存储方法的额外信息)

属性数量(u2)

属性

类加载机制

类加载过程

  • 加载

    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证

    • 文件格式的验证

      通过验证后,字节流才会存储到方法区中。后面的几个验证阶段不会再直接操作字节流,而是基于方法区存储的数据

      • 是否以魔数0xCAFEBABE开头
      • 主、次版本号是否在当前虚拟机的处理范围之内
      • 常量池的常量是否有不被支持的常量类型(检查常量tag标志)
      • 指向常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量
      • ...
    • 元数据验证

      对类的元数据信息进行语义检验,以保证不会存在不符合Java语言规范的元数据信息

      • 这个类是否有父类(除了Object之外)
      • 这个类是否继承了不允许被继承的类(例如:被final修饰的类)
      • ...
    • 字节码验证

      对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的安全事件

      • 保证跳转指令不会跳转到方法体以外的字节码指令上
      • 保证方法体中的类型转换是有效的
      • ...
    • 符号引用验证

      对类自身以外(常量池中的各种符合引用)的信息进行匹配性校验

      • 符号引用中通过字符串描述的全限定名是否能找到对应的类
      • 符号引用中的类、字段、方法的访问标识(private、public等)是否可被当前类访问
      • ...
  • 准备

    为类变量分配内存并设置类变量(不包含实例变量,是那些被static修饰的变量)初始值的阶段 静态变量jdk1.7是存储在方法区内,jdk1.8之后将其移除到堆中

  • 解析

    解析阶段是将常量池中的符号引用(定义的符号信息,不是直接的指针)替换为直接引用(地址或者句柄) 转换为直接引用,那么引用的目标一定已经在内存中存在

    • 类或接口的解析
    • 字段解析
    • 类方法解析
    • 接口方法解析
  • 初始化

    初始化阶段是执行类构造器()方法的过程 ()方法:由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的

  • 使用

  • 卸载

类加载器

  • 启动类加载器(Bootstrap ClassLoader)

    使用C++实现,位于虚拟机内部。其它类加载器都是独立于虚拟机外部,使用Java语言来实现的 负责加载<JAVA_HOME>\lib中的类,如:rt.jar等核心jar包

  • 扩展类加载器(Extension ClassLoader,ExtClassLoader类)

    负责加载<JAVA_HOME>\lib\ext中的类

  • 应用程序类加载器(Application ClassLoader,AppClassLoader类)

    如果应用程序没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

类加载器模型

  • 双亲委派模型

    当需要加载一个类的时候,并不会直接使用应用程序类加载器,而是会使用最父级的类加载器加载;如果无法加载,则交给子类加载器进行加载,直到加载成功为止

    • 沙箱安全机制(自己写的java.lang.String类不会被jvm加载,避免核心api类库被篡改)
    • 避免类被重复加载
  • 破坏双亲委派模型

    • 自定义ClassLoader,重写loadClass方法
    • tomcat里面就自定义了类加载器,实现不同webapp之间的隔离

类加载是懒加载,用到什么类加载什么类,而不是一次性加载所有类(使用-verbose:class可以查看加载的类信息)

字节码执行引擎

方法调用

  • 解析

  • 分派

    • 静态分派
    • 动态分派

基于栈的字节码解释执行引擎

早期(编译期)优化

晚期(运行期)优化

Java内存模型

主内存(线程共享)

所有变量都存储在主内存中

工作内存(寄存器、高速缓存,独立于线程)

保存了被该线程使用到的变量的主内存副本拷贝

线程安全和JVM锁优化

线程安全

  • 互斥同步
  • 非阻塞同步

JVM锁

  • 重量级锁

  • 自旋锁和自适应自旋锁

    当某个线程需要访问的资源被占用时,不必将线程挂起,然后过段时间再恢复。可以让当前线程再等一下,继续执行一个空循环 如果锁被占用的时间很短,这种自旋锁的效果非常好;如果锁被占用了很少时间,就会导致等待线程长时间的空循环,浪费了cpu的资源

  • 轻量级锁

  • 偏向锁

锁优化

  • 锁消除

    虚拟机即时编译器在运行时,对一些代码上需要同步,但是被检测到不可能存在共享数据竞争的锁进行清除 例如:字符串拼接时,可能内部代码优化为使用StringBuffer添加字符串。StringBuffer内部加了锁,是线程安全的。但是当前场景不会存在多个线程共享该StringBuffer对象,加锁会浪费性能,所以需要进行锁消除

  • 锁粗化

    某些场景下需要将锁的作用范围扩大,避免频繁的加锁、解锁操作