| 日期 | 迭代内容 |
|---|---|
| 23-11-24 | 1. 导学指引。确定一种学习和记忆 JVM 相关知识点的逻辑树。 |
| 23-11-26 | 1. 调整整个文档结构,并加以完善。 |
导学指引
人们想要实现一种跨平台的编程语言,于是人们开始设计:
- 常见语言的运行方式主要有解释运行、编译运行两种方式,但是不管如何都要经过高级语言到低级语言的转变过程(如机器语言)。那如何解决跨平台的问题?
- 高级语言不直接转化为机器语言,高级语言转化成一种中间的语言,即字节码文件;
- 加一台虚拟机,区别于物理机。虚拟机运行在物理机之上,不同的物理机设计实现不同的虚拟机程序;
- 这样以来,完美解决跨平台的问题。Java 语言编写 Java 源代码,然后通过编译器生成字节码,字节码再在虚拟机上运行。不同的虚拟机解析字节码文件时,会根据自身实现以及操作系统指令集把同一条命令解析成不同的实现。完美解决跨平台问题。
- 好吧,愿景已经有了,下面就是实现。
- 高级语言得有一种格式吧,得规定 Java 源代码的规范吧,于是有了**《Java 语言规范》**。
- 虚拟机反正是运行字节码,那可不可以解耦?啥意思呢?就是我一定使用 Java 语言生成字节码嘛?有没有一种可能,我使用另外一种高级语言也生成同样的字节码文件?于是有了**《Java 虚拟机规范》**。
- 于是,我们的学习内容有了——Java 虚拟机规范。下面我们开始学习:
- 如何学呢?按照一个顺序,那就是高级语言的执行过程。这个执行过程一定是先要把高级语言转化成低级语言,然后低级语言通过调用底层的操作系统指令集,完成对计算机硬件的调用,最终完成程序的执行。那我们就按照这个过程来学习。
- 由于第二点说了,不一定是 Java 语言,只要能生成被虚拟机识别的字节码文件就行,因此要先学习字节码的相关内容。
- 好了字节码文件有了,下一步就应该是把字节码文件加载到内存中运行吧。
- 怎么加载呢,类加载子系统。
- 运行呢?运行过程要解决这么几个问题:
- 怎么运行呢?总要有一个载体吧,这个载体就是** Java 的线程模型**。
- 运行时总要用到数据的计算吧,那数据怎么分配呢?这个分配过程有个逻辑模型,这个模型就是运行期内存结构。
- 载体和数据都分配完了,那该指令了吧,指令是怎么执行的呢?这就是执行引擎。
- 好了,能够完成基本运行功能的虚拟机已经实现了。但是还有一个重要的问题没有解决,内存的回收怎么处理呢?于是,垃圾回收理论登场。到此为止,基本的 Java 虚拟机算是学完了。
- 上面我们大概了解了《Java 虚拟机规范》,那该找一个具体的虚拟机练练手吧,找哪一个呢?找市场上最流行的。——HotSpot 荣耀登场。
- HotSpot 的内存模型;
- HotSpot 的垃圾回收器;
- HotSpot 有哪些配置参数呢?
- HotSpot 提供了哪些工具包?有哪些好用的第三方的工具包呢?
- 好了,理论部分我们已经学完了,并且我们还找了一个最常用的虚拟机实现进行学习。下个阶段就应该是压榨服务器运算能力了。这个过程我们称为调优。
- 要想压榨运算能力,首先得保证不能出错吧。于是,你得知道如何虚拟机会出现哪些错误信息,你还要知道怎么解决这些错误信息吧。
- 其次才是压榨运算能力,要想压榨,就得保证可用。什么意思呢?如果虚拟机一天总有 30s 的停顿时间,那放到一年中,那得有多少不可用的时间呐,那两年三年呢?于是确定我们压榨的目标——减少停顿。
- 目标有了——减少停顿,这个动作代表一个过程,多少算减少?所以要知道正常的停顿应该是怎样的,也就是说要知道正常指标是怎样的。最后才是在此基础上进行调优。
下面的知识体系也是按照上面的导学指引完成的。
1. 概述
- Java 语言规范与 JVM 规范的区别
- Java 语言的跨平台性
- JDK、JRE、JVM 关系
- 发展历程简述
2. 前端编译阶段及字节码文件
- 前端编译介绍
- 字节码文件解析
- 相关命令
- 特殊应用【如字节码增强技术、热部署等】
3. 类加载
- 类加载子系统
- 类加载过程
- 类加载器分类及各自作用
- 类加载原则
4. 线程模型
- 线程模型的发展历程
- Java 中的线程模型
5. 运行时内存结构
- 运行期内存结构
- 对象探秘
6. 执行引擎
- 执行方式
- 方法的执行过程
7. 垃圾回收理论
垃圾对象的特点、收集的区域 垃圾对象的判断标准 垃圾对象的定位 垃圾对象的收集算法以及衍生问题
8. Hotspot 虚拟机 & 配置 Hotspot
【还是理论部分】 经典垃圾回收器 针对不同垃圾回收器实现的实际的内存划分; 查看有哪些配置参数 各种配置参数讲解 配置参数表现行为等:如配置 printGCDetails,就会打印日志,打印日志是怎样的,如何理解这些日志等
9. 工具包
Hotspot 工具包 第三方工具包 使用案例
10. 调优
方法论
11. 出错
出错类型 排查过程 最佳实践经验 案例
- OOM
- OOM类型
- 服务器异常
- Swap分区
- 异常类型
- Out of swap space
- 本质原因
- Linux系统会把不常用的内存里面的数据放到一个swap分区中,这样再次使用这些数据时,就直接从swap分区中读到内存中就行,即交换分区。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成,遇到这个异常,表示虚拟内存耗尽。
- 常见具体原因
- 地址空间不足
- 物理内存耗尽
- 应用程序的本地内存泄露(native leak),例如不断申请内存空间,却不释放
- 优化手段
- 执行 jmap -histo:live <pid> 命令,强制执行FullGC,如果几次执行后内存明显下降,则基本可以确认为 Direct buffer 问题
- 升级地址空间为64bit
- 升级服务器配置规格或进行资源隔离
- 线程资源
- 异常类型
- Unable to create new native thread
- 本质原因
- JVM创建的用户线程都是由底层操作系统来完成的,而操作系统创建线程需要分配空间,当JVM创建系统线程,而操作系统又没有足够资源分配时,就会抛出异常信息。
- 常见的具体原因
- JVM创建的线程数超过了操作系统的ulimit限制
- 线程数超过了kernel.pid_max
- 本地线程内存空间不足
- 优化手段
- 调整ulimit限制,ulimit -u xxx
- 减小JVM其他资源使用,腾出更多空间给线程使用
- 限制线程栈大小,即调小-Xss大小
- 减小堆空间大小
- 限制线程池大小
- 修复其他应用程序内存泄露问题
- OOM Killer机制
- 异常类型
- Kill process or sacrifice child
- 本质原因
- 默认情况下,Linux允许用户进程申请大于系统可用内存的空间,这样可以『错峰复用』,有利于提高资源利用率。但这也导致了『超卖』问题,因此当线程申请不到资源时,系统会自动激活OOM Killer机制,寻找评分较低的进程来释放资源。在监控工具上的表现是:空闲的内存空间突然大幅度上升
- 常见的具体原因
- 这个异常类型是由操作系统层面出发的,也就是操作系统自动触发的OOM Killer机制导致的异常
- 优化手段
- 可以采用资源隔离或对系统OOM Killer机制进行调优
- Java堆
- 内存泄露或内存溢出
- 异常类型
- Java heap space
- 本质原因
- 堆空间没有足够的空间存放新创建的对象时,就会抛出异常信息
- 常见的具体原因
- 请求创建一个大对象,通常是一个大数组
- 业务流量激增,这种情况需要配合监控工具中的尖峰值时期与OOM时期来判断是否是因为业务流量激增导致的
- 过度使用终结器(Finalizer),该对象没有立即被 GC
- 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收
- 优化手段
- 通常是调高JVM堆内存空间即可,还是不行就采用下面的手段
- 超大对象,就要检查业务代码,判断其合理性
- 如果是业务峰值,就扩大资源,或做限流熔断等
- 如果是内存泄露,需要找到持有的对象,修改代码设计,比如关闭没有释放的资源等
- 多次GC仍无法满足分配资源所需要的空间
- 异常类型
- GC overhead limit exceeded
- 本质原因
- JVM花费了98%的GC时间回收不到2%的内存空间,换句话来说应用程序已经耗尽了所有可用内存,连GC也不会回收
- 优化手段
- 同 Java heap space
- 数组越界
- 异常类型
- Requested array size exceed VM limit
- 本质原因
- 程序请求创建的数组超过最大长度限制抛出异常
- 优化手段
- 检查代码,判断合理性,或改为分批次执行等
- 发生的时机不同,GC overhead limit exceeded 发生时,可能并没有申请内存空间使用,只是多次GC后回收的内存空间过小;而 Java heap space 发生时,一定会有JVM为完成对象的创建而申请内存空间分配
- 方法区
- 方法区已满
- 本质原因
- 方法区已满,通常是加载的class数目太多或体积太大
- 异常类型
- 永久代/老年代
- 永久代空间已满
- Permgen space
- 元空间
- 元空间已满
- Metaspace
- 优化手段
- 根据出现的时机采用不同的办法
- 程序启动时,调整 -XX:MaxPermSize 或 -XX:MaxMetaspaceSize 启动参数,调大方法区的大小
- 应用重新部署时,应用没有重新启动,导致加载了多份class信息,再次重启即可
- 运行时报错,可能是应用程序动态创建了大量的class,而这些class生命周期却很短暂,但是jvm并没有卸载class,可以配置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC 两个参数,允许JVM卸载class
- 如果上面还不能解决,就是用 jmap -dump:format=b,file=dump.hprof <process-id> 命令dump内存对象,然后通过MAT工具分析开销最大的classloader及重复的class
- 直接内存
- JVM允许应用程序通过 Direct byte buffer 直接访问堆外内存,一些应用程序通过 Direct byte buffer 结合内存映射文件(memory mapped file)实现高速IO。Direct byte buffer 默认的大小是64MB,一旦超出这个限制,就会抛出异常。通常情况下,涉及到Nio操作时才会抛错。
- 异常类型
- Direct buffer memory
- 本质原因
- 应用程序使用Direct byte buffer时,超出了限制
- 优化手段
- 通过 -XX:MaxDirectMemorySize 启动参数,调整直接内存的大小
- 检查堆外内存使用代码,确认是否存在内存泄露;或通过反射调用sun.misc.Cleaner的clean()方法,主动释放被 Direct ByteBuffer 持有的内存空间
- Java只能通过ByteBuffer.allocateDirect来操作Direct ByteBuffer,因此可以通过Arthas拦截改方法进行排查
- 检查一下 -XX:+DisableExplicitGC 参数是否启动,如果有就去掉,因为这个参数会让 System.gc() 失效
- StackOverFlow12. 停顿
正常指标: 获取正常指标的过程、正常指标有哪些? 异常指标:如何监控异常指标? 最佳实践经验 案例
- 本质: FullGC频繁
- 表现
- 机器 cpu 负载过高
- 频繁 full gc 告警
- 系统无法请求处理或者过慢, 接口无关的、全面性的无法响应请求或响应过慢
- 原因
- 解决思路: full gc 触发条件是 老年代空间不足, 所以追因的方向就是导致 老年代空间不足的原因:大量对象频繁进入老年代 + 老年代空间释放不掉
- 系统并发高、执行耗时过长,或者数据量过大,导致 young gc频繁,且gc后存活对象太多,但是survivor 区存放不下(太小 或 动态年龄判断) 导致对象快速进入老年代 老年代迅速堆满
- 发程序一次性加载过多对象到内存 (大对象),导致频繁有大对象进入老年代 造成full gc
- 存在内存溢出的情况,老年代驻留了大量释放不掉的对象, 只要有一点点对象进入老年代 就达到 full gc的水位了
- 元数据区加载了太多类 ,满了 也会发生 full gc
- 堆外内存 direct buffer memory 使用不当导致
- 也许, 你看到老年代内存不高 重启也没用 还在频繁发生full gc, 那么可能有人作妖,在代码里搞执行了 System.gc();
- 解决思路
- 如果有监控,那么通过图形能比较直观、快速的了解gc情况;
- 如果没有监控,那么只能看gc日志或jstat来分析 这是基本技能 一定要熟练
- 观察年轻代 gc的情况,多久执行一次、每次gc后存活对象有多少 survivor区多大
- 存活对象比较多 超过survivor区大小或触发动态年龄判断 => 调整内存分配比例
- 观察老年代的内存情况 水位情况,多久执行一次、执行耗时多少、回收掉多少内存
- 如果在持续的上涨,而且full gc后回收效果不好,那么很有可能是内存溢出了 => dump 排查具体是什么玩意
- 如果年轻代和老年代的内存都比较低,而且频率低 那么又可能是元数据区加载太多东西了
- 排查系统是否加载了无用的lib包,对系统所依赖的类库进行精简
- 其实如果是自己负责的系统,可能要看是不是发版改了什么配置、代码log
- 运行期内存结构
1. 线程私有
1. 虚拟机栈: 存放内容、常见配置参数、异常;
2. 本地方法栈: 存放内容、常见配置参数、异常;
3. 程序计数器: 存放内容、常见配置参数、异常;
4. 直接内存
2. 线程共享
1. Java堆: 存放内容、常见配置参数、异常;
2. 方法区: 存放内容、常见配置参数、异常;
1. GC内存模型
1. eden+s0+s1+old+永久代/元空间
2. minorgc、majorgc、FullGC
2. 垃圾回收
1. 存活对象判断标准
1. 可作为GCRoot的对象
2. 引用类型
2. 垃圾回收算法
3. 垃圾回收器
1. 配置参数
2. 回收过程
3. 优缺点分析
3. 对象探秘
1. 对象的内存分布
1. 对象头(哈希值、分代次数、持有的锁、偏向线程id、偏向时间戳)
2. 实例数据(自身的字段数据和从父类继承过来的字段数据)
3. 对齐填充
2. 对象创建过程
1. 逃逸分析(栈上分配、锁消除、标量替换等);
2. 方法区常量池能否定位类型数据;
3. 分配内存;
4. 处理同步问题;
5. 初始化;
6. 执行init,完成其他信息的构造;
3. 对象内存分配过程及访问定位、对象年龄晋升过程;
4. 类的加载
1. 双亲委派模型
2. 类加载器
3. 类加载过程
5. 实操
1. 性能指标
2. 工具(自带的和第三方提供的)
3. 两大类问题
1. 异常信息
2. 优化JVM13. 参考
13.1. 概览
- [ ] 概览及总述:
- [ ] 类加载子系统,JVM是如何把Java语言加载到JVM中的?
- [ ] 运行期内存结构,加载到JVM中的源代码是如何使用内存的?
- [ ] 执行引擎,
- [ ] 本地方法接口及本地方法库
- [ ] 理论层次
- [ ] JVM中的后台进程
- [ ] 字节码文件
- [x] 类加载子系统
- [ ] 【where】类来源途径(本地文件、网络、压缩包、数据库、动态编译生成等)
- [ ] 【who】类加载器(分类、作用、加载内容)
- [ ] 【how】双亲委派(原理、好处)、破坏双亲委派模型的样例
- [x] 运行期内存结构
- [ ] 线程私有区域
- [ ] 程序计数器 (存放内容、异常信息、存在的原因)
- [ ] 虚拟机栈(存放内容(局部变量表、操作数栈、动态链接、方法出口及一些附加信息)、异常信息、配置参数)
- [ ] 本地方法栈 (存放内容、异常信息)
- [ ] 线程共享区域
- [ ] Java堆(存放内容(Java对象及数组)、配置参数、异常类型及解决方案)
- [ ] 方法区(存放内容(Java对象及数组)、配置参数、异常类型及解决方案)
- [ ] 执行引擎
- [ ] Java中的运行方式(解释运行、编译运行),HotSpot的运行方式
- [ ] 解释运行与编译运行的区别
- [ ] 编译器分类(前端编译器、后端编译器、静态提前编译器,各自区别)
- [ ] 本地方法接口与本地库
- [x] 对象探秘
- [ ] 对象实例化方式
- [ ] 对象内存结构
- [ ] JVM为对象分配内存的过程【即在分代回收模型中的内存分配过程】
- [ ] 对象的创建过程(空间分配担保+对象年龄晋升确定所在区域、逃逸分析确定代码优化结果)
- [ ] 对象的访问定位;
- [ ] 内存结构、内存分配过程、访问定位、升代策略
- [ ] 垃圾回收理论
- [ ] 虚拟机中的高效并发
- [ ] 具体实现层面
- [ ] 不同回收方式具有不同的运行期内存结构
- [ ] 经典垃圾回收器(各自垃圾回收过程、所采用的算法、适用场景、配置参数)
- [ ] 组合使用
- [ ] JVM调优
- [ ] 方法论
- [ ] 调优和异常
13.2. 架构图




13.3. 研究方法
- 分别了解上图中的每一部分中涉及到的理论知识;
- 了解Hotspot的具体实现细节
- 针对JVM进行基准测试,了解其基本指标数据;
- 之后进行调优和问题解决;
