JVM

119 阅读10分钟

虚拟机分为四个部分,如图所示。

image.png

Class Loader Subsystem

image.png

  • 加载:在内存中生成java.lang.Class
  • 验证:验证字节码的安全性。虽然语法有错误编译会不通过,此步骤多此一举。但是可以通过非正常手段制造字节码文件。
  • 准备:开辟空间,分配内存,赋予初始值。例如int =1在此阶段先赋值0
  • 解析:将常量池的符号引用替换成直接引用

类加载器

双亲委派机制

parent-delegation model直接翻译其实是"父委托机制"。个人觉得双亲委托比较好,委托了两个亲戚办事,更达意。

sequenceDiagram
appClassLoader->>extClassLoader:亲人!你帮我加载下
extClassLoader->>BootstrapClassLoader:亲人!你帮我加载下
BootstrapClassLoader->>BootstrapClassLoader:'%JAVA_HOME/lib%'中找到了,加载
BootstrapClassLoader-->>extClassLoader:我找不到,你自己看看你的库中有没有
extClassLoader->>extClassLoader: '%JAVA_HOME/jir/lib/ext%'中找到了,加载
extClassLoader-->>appClassLoader:我找不到,你自己看看你的库中有没有
appClassLoader->>ClassPath:加载

设计初衷

我们可以看到,BootstrapClassLoader负责'%JAVA_HOME/lib%'核心库的加载,extClassLoader负责'%JAVA_HOME/jir/lib/ext%'核心库的加载。appClassLoader负责ClassPath下文件的加载。 为什么使用一个classLoader? 假设只有一个classLoader.我们自己写了一个java.lang.System.class,classLoader该加载谁? 同样可以回答为什么要使用双亲委派。

破坏双亲委派

类加载器实践-网络读取二进制流生成对象

Runtime Data areas

image.png

程序计数器

Program Counter Register,也叫pc寄存器,翻译成指令寄存器更加直观。是JVM模拟对cpu寄存器的抽象模拟。顾名思义,指令寄存器是用来存放指令的。如下图所示的指令 image.png

图中是字节码文件的可视化指令。指令前的序号可理解为逻辑地址。程序计数器就是用来保存即将执行的指令地址。

为什么需要指令寄存器

线程频繁切换,再次切换回来时,虚拟机无法得知接下来该执行哪个指令。需要一个记录即将执行的指令的地方。这个地方就是指令寄存器。这样听起来,指令寄存器是每个线程都拥有的,且是私有的。没错,你猜对了。

运行流程

  1. 对象方法入栈
  2. 即将执行的方法栈指令地址,由执行引擎读取到寄存器中。
  3. 解释器从寄存器中读取指令,翻译成机器码交给cpu.
  4. 执行引擎读取下一跳指令到寄存器中。
  5. 我们看到Runtime Data areas组成中有native internal Threads,它是native独享的栈。native方法非字节码,它的运行和字节码无关。执行器会在native执行后,在寄存器中存入下一跳字节码指令。

方法区

方法区是个逻辑概念。1.8以前实现方式是永久代,1.8开始是元空间。 为什么要改成元空间? 官方说法是JRockit 客户不需要配置永久层代(因为 JRockit 没有永久代),所以要移除永久代。 实际上:1.7时,永久代的运行时常量池被转移到堆中;1.8以后永久代这种实现方式直接被抛弃了,改为元空间。元空间和永久代最大的区别是,元空间所占的内存不被虚拟机最大内存限制。猜测这才是官方的真实意图——把减小虚拟机的内存压力,减低GC频率。

运行时常量池

image.png

字节码文件中定义了常量池。字面量和符号引用

字面量:字符串和位数较少的数字 符号引用:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符 运行时,这些符号才有对应的内存地址信息。这些常量就会变成运行时常量池

存放了大部分的对象,是GC的主要部分。逃逸分析技术让一部分对象不在堆上。

逃逸解释
线程逃逸如果一个线程内的对象有可能被其它线程访问到,那么这个对象就行线程逃逸的。
方法逃逸如果一个对象在方法内部创建后,可能被其它方法所引用,例如通过参数传递给其它方法,又或者作为方法返回值返回给其它方法,这就叫方法逃逸
不逃逸如果一个对象在方法内部创建后,不可能被其它方法所引用,也不可能被其它线程所引用,那么它就是从不逃逸的。
标量/聚合量解释示例
标量数据已经无法再分解成更小的数据int,long,String
聚合量数据可以继续分解new User

标量替换:如果对象无方法逃逸,聚合量会被分解成标量。在栈上创建标量

无逃逸逃逸分析结果
无线程逃逸jvm会去掉sync,称为锁擦除;
无方法逃逸对象创建在栈上,标量替换
  • -XX:+DoEscpeAnalysit;开启逃逸分析,1.7后默认开启
  • -XX:+PringEscapeAnalysit查看分析结果
  • -XX:-DoEscapeAnalysis关闭逃逸分析
  • —XX:+EliminateAllocations开启标量替换
  • —XX:+EliminateLocals开启同步擦除

对象的创建

如何分配内存

image.png

标题
指针碰撞法如上图所示,指针作为灵界点。申请内存,就往右边移动,然后将对象放入适用于内存连续无碎片的情况
空闲列表法因为空间不连续,jvm动态维护一个个空闲地址值范围。为对象分配地址时,向此列表申请适用于垃圾回收器会产生空间碎片的情况
TLAB(Thread Local Allocation Buffer)多线程申请内存,每个线程都申请一块空间,并在此空间内维护一个类似指针碰撞法的top指针若对象太大,空间无法满足。则使用CAS去竞争堆空间。
栈与和堆如何关联
指针类型解释
直接指针栈中记录真实地址,指向堆中对象。jvm使用此方式关联。
句柄指针在堆中维护一个对象真实地址的句柄池。栈中记录的是句柄

字节码工具 javasist 强软弱虚

对象长啥样

垃圾回收

识别垃圾

标题解释缺陷
引用计数法被引用一次,引用次数加一部分该回收的对象计数器数量值大于一,被抛弃
可达性分析法/根搜索法垃圾回收器的主流算法!如下图所示,上方存放对象引用的区域叫GC_Roots。从GC_Roots搜索活跃的对象。

image.png

清除垃圾

标题解释缺陷
标记清除(Mark-Sweep)被标记的对象原地删除有碎片
复制存活对象复制到新区,老区对象全部删除
标记整理/压缩(Mark-Compact)将活跃对象整理到一起,形成连续的空间。空间之外的对象回收

分代回收

image.png 分代回收是经典的垃圾回收模型,G1回收器使用的是region模型,虽然也叫分代回收,但区域划分完全不同。这里称为“模型”,是为了和以上几种算法区别开来。新生代使用的是复制算法。它分为eden和survivor两块。其中survivor又被分为两块,survivor-from(s1),survivor-to(s2).

  • 对象初生在伊甸园。
  • 第一次垃圾回收标记了伊甸园,存活对象复制到幸存者区1;
  • 第二次垃圾回收,标记了伊甸园和幸存者区1,存活对象复制到幸存者区2;
  • 第三次垃圾回收,标记了伊甸园和幸存者区2,存活对象复制到幸存者区1;
  • s1,s2总有一块被当做幸存者预留区。反复如此,直到对象被移交到老年代。

垃圾回收器

  1. Serial
  2. SerialOld
  3. ParNew
  4. Parallel Scavenge
  5. ParallelOld
  6. CMS

image.png 以上回收器早已成为过去式。java8开始,一切有了新的变化。 Serial-CMS,ParNew-SerialOld组合在java8开始废弃了。 Parallel Scavenge-SerialOld组合在Java14被废弃了

CMS

虽然CMS在Java14被移除了,但不妨碍它是一个优秀的垃圾回收器。只因它允许GC thread和工作线程同时工作。它适用于20G内存以下的机器使用.

  1. 初始标记:stop-the-world,标记GRoots直接关联的对象;
  2. 并发标记:解除stop-the-world,标记一级活跃对象下的对象。
  3. 重新标记:stop-the-world,继续标记步骤2期间,产生的新对象,并修正错标问题。
  4. 并发清理:解除stop-the-world,回收对象
  5. 并发重置:重置回收器内部数据结构

步骤1、3短时间暂停虚拟机,以达到高效的目的。步骤2、4允许工作线程工作。所以说GMS回收器是一款优秀的回收器。不过并发标记和并发清理比较耗费CPU。

CMS的缺点

步骤1阶段标记的存活对象,在步骤2已经变成垃圾,但是在本次GC中不会被清理。这就是GMS浮动垃圾问题。 因为工作线程和GC线程同时工作,根搜索法即将被标记成存活的对象,被修改引用,导致无法被标记,成为垃圾。这是错标问题。需要在步骤3修正。 如果老年代空间无法存放对象了,CMS将退化成SerialOld来进行垃圾回收。通过-XX:CMSInitiatingOccupancyFraction参数来设置回收阈值。

G1

image.png

G1回收器是java9默认回收器,不再使用经典的分代回收模型,而是region分代模型,推荐内存20G以上使用,否则性能会比较差。

  • 将整个JVM内存分为多个大小相等的region,年轻代和老年代逻辑分区 。
  • 在整体上使用标记整理算法,年轻代复制算法
  • 每个 Region 大小在 1-32M 之间,可以通过-XX:G1HeapRegionSize=n 指定区大小。
  • 总的 Region 个数最大可以存在 2048 个,即heap最大能够达到32M*2048=64G
  • 0.5<obj<1,那么放到old区,old标记成大对象。1<obj<n,连续的n个region,放入H区

垃圾回收过程

  1. 初始标记:同CMS步骤1
  2. 并发标记:同CMS步骤2;
  3. 最终标记:同CMS步骤3;
  4. 筛选回收:无STW,对每个 Region 的回收成本进行排序,根据用户期望的停顿时间来制定收回计划。

cset/rset 三色标记 CMS:写屏障与内存更新 G1:写屏障+SATB

调优

目的:减少相应时间,提高吞吐

常见参数

参数含义
-XX:+UsexxxGC使用某种虚拟机
-Xms,-Xmx堆内存min-max
-XX:NewRatio新生代比例
-XX:SurvivorRatio幸存者区比例
-XX:+UseAdaptiveSizePolicy依据GC过程统计的GC 时间、吞吐量、内存占用量每次GC后重新分配Eden、From和To区
-XX:HeadDumpOnOutOfMemoryErrordump内存溢出信息
-XX:HeadDumpPath内存溢出信息路径
-XX:MaxTenuringThreshold最大居住阈值,控制幸存者区的对象居住次数,一次GC为一次,默认15
-Xloggc:/xxx/logs/xxx-gc-%t.logGC日志路径
-XX:+UseGCLogFileRotationGC日志滚动功能
-XX:NumberOfGCLogFiles主要定义滚动日志文件的个数
-XX:GCLogFileSize滚动日志文件的大小
-XX:+PringGCDetails打印GC日志的详细信息
-XX:PrintGCDateStamps定义日志时间戳

调优方法论 根据业务场景 STW或者高吞吐 选择合理垃圾回收器 计算内存需求 设定年轻代老年代大小 设置日志参数 压测 分析日志 调整参数 常用命令

标题
jinfo查看虚拟机信息,jinfo -flags pid
jstat查看堆状态 jstat -gcutil pid
jps
jstack查看堆栈信息
jmapjmap -histo:live 可查看活动对象信息,jmap -heap pid可查看堆栈信息
jhat开启一个进程,提供访问端口,开启可视化界面
jcmd功能比较全的命令,记不住,help下
jps
Arthas
jconsole可视化工具
jvisualvm

压测工具:ab