虚拟机分为四个部分,如图所示。
Class Loader Subsystem
- 加载:在内存中生成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
程序计数器
Program Counter Register,也叫pc寄存器,翻译成指令寄存器更加直观。是JVM模拟对cpu寄存器的抽象模拟。顾名思义,指令寄存器是用来存放指令的。如下图所示的指令
图中是字节码文件的可视化指令。指令前的序号可理解为逻辑地址。程序计数器就是用来保存即将执行的指令地址。
为什么需要指令寄存器
线程频繁切换,再次切换回来时,虚拟机无法得知接下来该执行哪个指令。需要一个记录即将执行的指令的地方。这个地方就是指令寄存器。这样听起来,指令寄存器是每个线程都拥有的,且是私有的。没错,你猜对了。
运行流程
- 对象方法入栈
- 即将执行的方法栈指令地址,由执行引擎读取到寄存器中。
- 解释器从寄存器中读取指令,翻译成机器码交给cpu.
- 执行引擎读取下一跳指令到寄存器中。
- 我们看到Runtime Data areas组成中有native internal Threads,它是native独享的栈。native方法非字节码,它的运行和字节码无关。执行器会在native执行后,在寄存器中存入下一跳字节码指令。
方法区
方法区是个逻辑概念。1.8以前实现方式是永久代,1.8开始是元空间。 为什么要改成元空间? 官方说法是JRockit 客户不需要配置永久层代(因为 JRockit 没有永久代),所以要移除永久代。 实际上:1.7时,永久代的运行时常量池被转移到堆中;1.8以后永久代这种实现方式直接被抛弃了,改为元空间。元空间和永久代最大的区别是,元空间所占的内存不被虚拟机最大内存限制。猜测这才是官方的真实意图——把减小虚拟机的内存压力,减低GC频率。
运行时常量池
字节码文件中定义了常量池。字面量和符号引用
字面量:字符串和位数较少的数字 符号引用:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符 运行时,这些符号才有对应的内存地址信息。这些常量就会变成运行时常量池。
堆
存放了大部分的对象,是GC的主要部分。逃逸分析技术让一部分对象不在堆上。
| 逃逸 | 解释 |
|---|---|
| 线程逃逸 | 如果一个线程内的对象有可能被其它线程访问到,那么这个对象就行线程逃逸的。 |
| 方法逃逸 | 如果一个对象在方法内部创建后,可能被其它方法所引用,例如通过参数传递给其它方法,又或者作为方法返回值返回给其它方法,这就叫方法逃逸 |
| 不逃逸 | 如果一个对象在方法内部创建后,不可能被其它方法所引用,也不可能被其它线程所引用,那么它就是从不逃逸的。 |
| 标量/聚合量 | 解释 | 示例 |
|---|---|---|
| 标量 | 数据已经无法再分解成更小的数据 | int,long,String |
| 聚合量 | 数据可以继续分解 | new User |
标量替换:如果对象无方法逃逸,聚合量会被分解成标量。在栈上创建标量
| 无逃逸 | 逃逸分析结果 |
|---|---|
| 无线程逃逸 | jvm会去掉sync,称为锁擦除; |
| 无方法逃逸 | 对象创建在栈上,标量替换 |
- -XX:+DoEscpeAnalysit;开启逃逸分析,1.7后默认开启
- -XX:+PringEscapeAnalysit查看分析结果
- -XX:-DoEscapeAnalysis关闭逃逸分析
- —XX:+EliminateAllocations开启标量替换
- —XX:+EliminateLocals开启同步擦除
对象的创建
如何分配内存
| 标题 | ||
|---|---|---|
| 指针碰撞法 | 如上图所示,指针作为灵界点。申请内存,就往右边移动,然后将对象放入 | 适用于内存连续无碎片的情况 |
| 空闲列表法 | 因为空间不连续,jvm动态维护一个个空闲地址值范围。为对象分配地址时,向此列表申请 | 适用于垃圾回收器会产生空间碎片的情况 |
| TLAB(Thread Local Allocation Buffer) | 多线程申请内存,每个线程都申请一块空间,并在此空间内维护一个类似指针碰撞法的top指针 | 若对象太大,空间无法满足。则使用CAS去竞争堆空间。 |
栈与和堆如何关联
| 指针类型 | 解释 |
|---|---|
| 直接指针 | 栈中记录真实地址,指向堆中对象。jvm使用此方式关联。 |
| 句柄指针 | 在堆中维护一个对象真实地址的句柄池。栈中记录的是句柄 |
字节码工具 javasist 强软弱虚
对象长啥样
垃圾回收
识别垃圾
| 标题 | 解释 | 缺陷 |
|---|---|---|
| 引用计数法 | 被引用一次,引用次数加一 | 部分该回收的对象计数器数量值大于一,被抛弃 |
| 可达性分析法/根搜索法 | 垃圾回收器的主流算法!如下图所示,上方存放对象引用的区域叫GC_Roots。从GC_Roots搜索活跃的对象。 | 赞 |
清除垃圾
| 标题 | 解释 | 缺陷 |
|---|---|---|
| 标记清除(Mark-Sweep) | 被标记的对象原地删除 | 有碎片 |
| 复制 | 存活对象复制到新区,老区对象全部删除 | |
| 标记整理/压缩(Mark-Compact) | 将活跃对象整理到一起,形成连续的空间。空间之外的对象回收 |
分代回收
分代回收是经典的垃圾回收模型,G1回收器使用的是region模型,虽然也叫分代回收,但区域划分完全不同。这里称为“模型”,是为了和以上几种算法区别开来。新生代使用的是复制算法。它分为eden和survivor两块。其中survivor又被分为两块,survivor-from(s1),survivor-to(s2).
- 对象初生在伊甸园。
- 第一次垃圾回收标记了伊甸园,存活对象复制到幸存者区1;
- 第二次垃圾回收,标记了伊甸园和幸存者区1,存活对象复制到幸存者区2;
- 第三次垃圾回收,标记了伊甸园和幸存者区2,存活对象复制到幸存者区1;
- s1,s2总有一块被当做幸存者预留区。反复如此,直到对象被移交到老年代。
垃圾回收器
- Serial
- SerialOld
- ParNew
- Parallel Scavenge
- ParallelOld
- CMS
以上回收器早已成为过去式。java8开始,一切有了新的变化。
Serial-CMS,ParNew-SerialOld组合在java8开始废弃了。
Parallel Scavenge-SerialOld组合在Java14被废弃了
CMS
虽然CMS在Java14被移除了,但不妨碍它是一个优秀的垃圾回收器。只因它允许GC thread和工作线程同时工作。它适用于20G内存以下的机器使用.
- 初始标记:stop-the-world,标记GRoots直接关联的对象;
- 并发标记:解除stop-the-world,标记一级活跃对象下的对象。
- 重新标记:stop-the-world,继续标记步骤2期间,产生的新对象,并修正错标问题。
- 并发清理:解除stop-the-world,回收对象
- 并发重置:重置回收器内部数据结构
步骤1、3短时间暂停虚拟机,以达到高效的目的。步骤2、4允许工作线程工作。所以说GMS回收器是一款优秀的回收器。不过并发标记和并发清理比较耗费CPU。
CMS的缺点
步骤1阶段标记的存活对象,在步骤2已经变成垃圾,但是在本次GC中不会被清理。这就是GMS浮动垃圾问题。 因为工作线程和GC线程同时工作,根搜索法即将被标记成存活的对象,被修改引用,导致无法被标记,成为垃圾。这是错标问题。需要在步骤3修正。 如果老年代空间无法存放对象了,CMS将退化成SerialOld来进行垃圾回收。通过-XX:CMSInitiatingOccupancyFraction参数来设置回收阈值。
G1
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区
垃圾回收过程
- 初始标记:同CMS步骤1
- 并发标记:同CMS步骤2;
- 最终标记:同CMS步骤3;
- 筛选回收:无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:HeadDumpOnOutOfMemoryError | dump内存溢出信息 |
| -XX:HeadDumpPath | 内存溢出信息路径 |
| -XX:MaxTenuringThreshold | 最大居住阈值,控制幸存者区的对象居住次数,一次GC为一次,默认15 |
| -Xloggc:/xxx/logs/xxx-gc-%t.log | GC日志路径 |
| -XX:+UseGCLogFileRotation | GC日志滚动功能 |
| -XX:NumberOfGCLogFiles | 主要定义滚动日志文件的个数 |
| -XX:GCLogFileSize | 滚动日志文件的大小 |
| -XX:+PringGCDetails | 打印GC日志的详细信息 |
| -XX:PrintGCDateStamps | 定义日志时间戳 |
调优方法论 根据业务场景 STW或者高吞吐 选择合理垃圾回收器 计算内存需求 设定年轻代老年代大小 设置日志参数 压测 分析日志 调整参数 常用命令
| 标题 | |
|---|---|
| jinfo | 查看虚拟机信息,jinfo -flags pid |
| jstat | 查看堆状态 jstat -gcutil pid |
| jps | |
| jstack | 查看堆栈信息 |
| jmap | jmap -histo:live 可查看活动对象信息,jmap -heap pid可查看堆栈信息 |
| jhat | 开启一个进程,提供访问端口,开启可视化界面 |
| jcmd | 功能比较全的命令,记不住,help下 |
| jps | |
| Arthas | |
| jconsole | 可视化工具 |
| jvisualvm |
压测工具:ab