JVM篇

121 阅读12分钟

JVM篇

JVM整体结构

第02章_JVM架构-简图[1].jpg JVM内存模型整体结构分为类加载子系统、运行时数据区、本地方法区

1.类加载器子系统

image.png 类加载子系统分为加载阶段、链接阶段(验证、准备、解析)、初始化阶段

类加载器的分类

  1. 引导类加载器[Bootstrap Class Loader]
  2. 自定义类加载器(扩展类加载器[Extension Class Loader]、系统类加载器[System Class Loader]):将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

加载

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

链接

  • 验证
    • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全
    • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
  • 准备
    • 为类变量分配内存并且设置该类变量的默认初始值,即零值
    • 这里不包含用final修饰的static,因为final在编译的时候就会分配,准备阶段会显式初始化
    • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
  • 解析
    • 将常量池内的符号引用转换为直接引用的过程
    • 解析操作往往会伴随着JVM在执行完初始化之后再执行
    • 解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型等

初始化

  • 初始化阶段就是执行类构造器方法<Clinit~>()的过程
  • 此方法不需要定义,是Javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
  • 构造器方法中指令按语句在源文件中出现的顺序执行
  • 若该类具有父类,JVM会保证子类的<Clinit~>()执行前,父类的<Clinit~>()已经执行完毕
  • 虚拟机必须保证一个类的<Clinit~>()方法在多线程下被同步加锁

双亲委派机制
原理:

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
  3. 如果父类加载器可以完成类加载任务,就返回成功,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

优势:

  1. 避免类的重复加载
  2. 保护程序安全,防止核心API被随意篡改(沙箱安全机制 )

2.运行时数据区

image.png

3.垃圾回收

image.png

什么是垃圾

垃圾是指在运行程序中没有任何指针指向的对象。这个对象就是需要被回收的垃圾

为什么需要GC

  1. 不GC内存会被消耗完
  2. JVM可以将整理出的内存分配给新的对象
  3. 没有GC就不能保证应用程序正常运行

垃圾回收相关算法

  1. 标记阶段:引用计数算法
  2. 标记阶段:可达性分析算法
  3. 对象的finalization机制
  4. MAT与JProfiler的GC Roots溯源
  5. 清除阶段:标记-清除算法
  6. 清除阶段:复制算法
  7. 清除阶段:标记-压缩算法
  8. 分代收集算法
  9. 增量收集算法、分区算法

如何标记一个死亡对象?

当一个对象已经不再被任何的存活对象继续引用时,就可以宣判已经死亡;常用的两种算法:引用计数算法/可达性分析算法

引用计数算法

对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况;有任何一个对象引用计数器加1;当引用失效减1

优点

  1. 实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟性

缺点

  1. 需要单独字段存储计算器,增加了空间的开销
  2. 每次赋值都需要更新计算器,增加了时间的开销
  3. 无法处理循环引用的情况,所以垃圾回收器中没有使用这类算法

可达性分析算法

以根对象集合(GC roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达

GC roots可以是哪些元素

采用栈方式存放变量和指针,所以如果一个指针,他保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root

  1. 虚拟机栈中引用的对象
  2. 本地方法栈
  3. 方法区中类静态属性引用的对象(引用类型静态变量)
  4. 方法区中常量引用的对象(字符串常量池里的引用)
  5. 所有被同步锁synchronized持有的对象
  6. Java虚拟机内部的引用

对象的finalization机制

当垃圾回收器发现没有任何引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法

标记清除算法

  1. 标记:从引用根节点开始遍历,标记出所有被引用对象。一般是在对象的Header中记录可达对象
  2. 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

缺点:

  1. 效率不高
  2. 在进行GC的时候,需要停止整个应用程序。导致用户体验差
  3. 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表

复制算法

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收)适合存活对象少、垃圾对象多

优点:

  1. 没有标记和清除过程,实现简单 ,运行高效
  2. 复制过去以后保证空间的连续性,不会出现碎片问题

缺点:

  1. 需要两倍的内存空间

标记-整理压缩算法

第一阶段从根节点出发遍历可达对象,第二阶段将所有存活对象压缩到内存的一端,顺序排放,之后清理边界所有的空间

优点:

  1. 消除了标记清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址既可
  2. 消除了复制算法中,内存减半的高额代价

缺点:

  1. 效率低于复制算法
  2. 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  3. 移动过程中,需要全程暂停用户应用程序,即使:STW

分代收集算法

不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收效率

新生代

区域相对老年代较小,对象生命周期短、存活率低、回收频繁

串行GC(UseSerialGC)

一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有工作线程直到它收集结束
开启后:新生代UseSerialGC,老年代UseSerialOldGC的收集器组合
表示:新生代使用复制算法,老年代使用标记整理算法

并发GC(UseParNewGC)

使用多线程进行垃圾回收,在垃圾收集时,会stop-the-word暂停其他所有的工作线程直到它收集结束
开启后:新生代UseParNewGC,老年代UseSerialOldGC的收集器组合
表示:新生代使用复制算法,老年代使用标记整理算法

并行回收(UseParalleIGC)

串行收集器在新生代、老年代的并行化
开启后:新生代UseParalleIGC,老年代UseParallelOldGC的收集器组合
表示:新生代使用复制算法,老年代使用标记整理算法

老年代

区域较大,对象生命周期长、存活率高、回收不及年轻代频繁

串行GC(UseSerialOldGC)

已废弃

并行GC(UseParallelOldGC)

可与UseParalleIGC互相激活
开启后:新生代UseParalleIGC,老年代UseParallelOldGC的收集器组合
表示:新生代使用复制算法,老年代使用标记整理算法

并发标记清除(UseConcMarkSweepGC)

并发收集低停顿,并发指的是与用户线程一起执行
开启后:新生代UseParNewGC,老年代UseConcMarkSweepGC和UseSerialOldGC的收集器组合
表示:UseSerialOldGC将作为CMS出错的后备收集器

优点:

  1. 并发收集低停顿

缺点:

  1. 并发执行,对CPU资源压力大
  2. 采用并发标记清除算法会导致大量内存碎片

如何选择合适的垃圾收集器

参数新生代垃圾收集器新生代算法老年代垃圾收集器老年代算法
-XX:+UseSerialGCSerialGC复制算法SerialOldGC标记整理算法
-XX:+UseParNewGCParNewGC复制算法SerialOldGC标记整理算法
-XX:+UseParalleIGC/-XX:+UseParallelOldGCParallel[Scavenge]复制算法ParallelOld标记整理算法
-XX:+UseConcMarkSweepGCParNewGC复制算法CMS+SerialOld的收集器组合SerialOldGC将作为CMS出错的后备收集器标记清除算法
-XX:+UseG1GCG1整体上采用标记-整理算法局部是通过复制算法,不会产生内存碎片

如何选择:

单CPU或内存小,单机程序:-XX:+UseSerialGC
多CPU,需要最大吞吐量,如后台计算型应用:-XX:+UseParalleIGC/-XX:+UseParallelOldGC
多CPU,追求低停顿时间,需要快速响应,如互联网应用:-XX:+UseConcMarkSweepGC

GC的几种垃圾收集器

  1. -XX:+UseSerialGC: 在新生代和老年代使用串行收集器
  2. -XX:+UseSerialOldGC: 老年代使用串行收集器(废弃)
  3. -XX:+UseParNewGC: 在新生代使用并行收集器
  4. -XX:+UseParalleIGC :新生代使用并行回收收集器,更加关注吞吐量
  5. -XX:+UseParallelOldGC: 老年代使用并行回收收集器
  6. -XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用CMS+串行收集器
  7. -XX:+UseG1GC: 启用G1垃圾回收器

G1垃圾收集器

特点:

  1. G1充分利用多CPU、多核环境硬件优势,尽量缩短STW(stop the word)
  2. G1整体上采用标记整理压缩算法,局部是复制算法,不会产生内存碎片
  3. 宏观上看G1之中不再区分年轻代和老年代。把内存划分多个独立的子区域(Region),可以近似理解为一个棋盘
  4. G1收集器里面将整个的内存都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但他们不再是物理隔离的,而是一部分Region的集合混合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域
  5. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换

JVM调优

jvm调优涉及到两个很重要的概念:吞吐量和响应时间。jvm调优主要是针对他们进行调整优化,达到一个理想的目标,根据业务确定目标是吞吐量优先还是响应时间优先。

  1. 吞吐量:用户代码执行时间/(用户代码执行时间+GC执行时间)。
  2. 响应时间:整个接口的响应时间(用户代码执行时间+GC执行时间),stw时间越短,响应时间越短。

参数

第一种查看jvm参数默认值

  • jpa + jinfo -flag 具体参数名称 进程号
  • jpa + jinfo -flags 进程号

第二种查看所有参数默认值

  • Java -XX:+PrintFlagsInitial

第三种查看所有修改过的参数值

  • Java -XX:+PrintFlagsFinal -version
  • Java -XX:+PrintCommandLineFlags -version

JVM的参数类型

  1. 标配参数
    • java -version
    • java -help
    • java -showversion
  2. x参数
    • java -Xint -version 解释执行
    • java -Xcomp -version 第一次使用久编译成本地代码
    • java -Xmixed -version 混合模式
  3. xx参数
    • -Xms 初始堆内存大小 默认物理内存的1/64 -XX:InitialHeapSize
    • -Xmx 最大堆内存大小 默认物理内存的1/4 -XX:MaxHeapSize
    • -Xss 设置单个线程栈的大小 一般默认512k-1024k -XX:ThreadStackSize
    • -Xmn 设置新生区年轻代大小
    • -XX:MetaspaceSize 设置元空间大小
    • -XX:+PrintGCDetails 打印GC情况
    • -XX:SurvivorRatio 设置伊甸园区和幸存者区的比例 默认-XX:SurvivorRatio=8 伊甸园区占8;两个幸存者区各占1
    • -XX:NewRatio 设置年轻代和老年代比例 默认-XX:NewRatio=2 新生代占1;老年代占2,年轻代占整个堆的1/3
    • -XX:MaxTenuringThreshold 设置垃圾最大年龄