1、JVM学习建议
1.1、为什么学习 JVM?
- 1、可以知道设备是怎么识别我们编写的Java程序的,规避它在使用中的 Bug;
- 2、Java 虚拟机提供了许多配置参数,用于满足不同应用场景下,对程序性能的需求,你可以针对自己的应用,最优化匹配运行参数
- 3、在面试中重要程度上:在当前 Java 后端面试中,JVM所有框架/中间件中被问到频率是最高的
1.2、学习的建议
- 1、阅读源码。读源码其实也是一种实战锻炼,可以帮助你从代码逻辑中彻底理解 JVM 系统的实际运行机制。当遇到问题时,可以直接从代码层面进行定位、分析和解决问题
- 2、亲自动手实践
- 在开发中,使用并体会其特性
- 3、学习的路线
- JVM全景知识图
- 4、重点学习内容:
- 1、需了解内存模型各部分作用,保存哪些数据;
- 2、类加载双亲委派加载机制,常用加载器分别加载哪种类型的类;
- 3、GC分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景;
- 4、性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法;
- 下图主要表示的逻辑关系,用来将所有知识点放到一张图里,帮助你理解。
- JVM整体流程:编译器将Java代码编译为字节码 --》类加载器将字节码加载到内存(加载、连接、初始化) --》放到方法区 --》字节码是一套规范,需要特定的解析器翻译成操作系统底层系统指令,然后让CPU执行 --》这个过程中也需要调用其他语言的本地库接口实现整个功能
1.3、JVM 学习资料
参考的书籍 (帮助解决别人的JVM问题是学习JVM的最好方法)
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》 周志明
- 《深入理解JVM.2nd》笔记
- 极客时间课程:《深入拆解Java虚拟机》郑雨迪
1.4、项目中用到JVM的地方 (面试常见题目梳理)
1、运行时数据区?
场景1:方法区、堆内存以及栈内存
- 方法区:存放类信息、常量池、静态变量
- 堆内存:存放实例对象、数组
- 栈内存:虚拟机栈本地方法栈
- 程序计数器:为了线程切换后能恢复到正确的执行位置
场景2:内存分配策略**,**对象的创建/布局/访问
- 栈: 基础数据类型,方法形参,对堆内存数据的引用
- 堆:引用数据类型数组
- 对象的创建
- 对象头信息(对对象进行必要的设置)
- 对象的访问定位:通过栈上的引用数据来操作堆中具体对象
场景3:JVM指令重排以及内存屏蔽
- 定义了一套程序中变量的访问规则
- 提供对原子性,可见性,有序性保证
场景4:内存溢出,内存泄露
- 什么是内存溢出?
- 堆 场景1:对象创建太多超出了最大堆容量限制
- **虚拟机栈和本地方法栈 ** 场景1:递归太深、死循环导致栈帧创建过多
- 方法区和运行时常量池 场景1:方法区被填满
- 项目中的内存溢出
- 场景1、内存中加载的数据量过于庞大 持久层没有limit限制,导致大批量数据被查询
- 场景2、代码中存在**死循环 **
- dubbo 泛化调用时,使用姿势不对,导致了循环调用;
- json序列化使用姿势不对,也会导致循环调用
- 什么是内存泄漏?
- 一个对象已经不需要再使用,本该被回收,然而另外一个正在使用的对象持有它的引用从而导致它不能被回收
- 项目中的内存泄漏?
- 场景1:对象连接资源未关闭 网络资源、IO资源
- 场景2:ThreadLocal 使用完毕没有调用remove方法
- 场景3:guava cache 的weakKey 导致内存泄露
2、服务器使用的什么垃圾收集器?CMS 垃圾收集的原理?G1 垃圾收集的特点,为什么低延迟?有哪些垃圾回收算法,优缺点?
- 有哪些垃圾回收算法,优缺点?
- 复制算法 (新生代) 缺点:堆内存使用率低,只有原来的一半
- 标记-清除算法 直接回收不存活的(老年代)
- 缺点:分配效率较低
- 空间问题:标记清除后会产生大量不连续的内存碎片
- 标记 - 整理算法(老年代)
- 优点:1、解决大量内存碎片问题;2、当对象存活率较高时,效率也很好
缺点:压缩算法的性能开销大
- 服务器使用的什么垃圾收集器? CMS(老年代)PraNew(新生代) G1
- CMS 垃圾收集的原理?使用标记-清除算法(底层为三色标记法)
- 初始标记 对应三色标记:gc-roots直接关联对象置为灰色;
- 并发标记 对应三色标记:没有子节点,将本节点变为黑色。有子节点:则当前节点变为黑色,子节点变为灰色;
- 最终标记 对应三色标记:此时黑色对象就是存活的对象,白色对象就是已消亡可回收的对象;
- 并发清除 清理未使用的对象并回收它们占用的空间
- G1 垃圾收集的特点,为什么低延迟?
- 使用G1收集器时,将堆划分为相等的region,并且能和整个堆中任意的对象发生引用关系,优先回收价值大的region。每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
- 过程
- 1、初始标记(STW) 标记GC roots对象,不会立即执行,而是等到下次young gc时执行,尽量减少STW
- 2、并发标记
- 进行GC Roots Tracing的过程,扫描整个堆,进行可达性分析
- 耗时较长,没有停顿
- 3、最终标记(STW)
- 找出并发标记阶段,未被标记的存活对象
- 4、筛选回收(STW)
- 根据停顿时间来进行价值最大化的回收(默认200ms)
- RSet梳理
- 对old regions进行对象存活率排序,并放入 Cset
- 识别空闲分区,即无存活对象的分区,立即回收
- G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色
- 1、并发标记时间
- 由于CMS需要并发标记整个old区,所以极端情况下(大堆),标记时间会很长,
- 而G1会选择性的选择GC region。G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间,所以G1时间更可控。
- 该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
- G1更优
- 2、内存碎片
- CMS使用标记-清除法,会产生内存碎片,若触发内存碎片整理,也会加长GC时间。
- G1采用复制算法,不存在很多内存碎片情况(未考虑大对象场景)。
- G1更优
- 3、浮动垃圾
- CMS最后一步: 并发清除会产生浮动垃圾
- CMS更优
- 相对而言G1只会选择性的选择region,故G1浮动垃圾更严重。
- 4、垃圾收集时的内存占用
- G1无论是为了垃圾收集产生的内存占用还是程序运行时额外执行负载都要比CMS高。
- 优劣势的平衡点:6GB~8GB间
3、哪些对象可以作为 GC Roots?
- 堆外指向堆内的引用
- 1、虚拟机栈中引用的对象(堆中);
- Java堆从GC的角度还可以细分为:新生代(Eden 区、From Survivor 区和To Survivor 区)和老年代。
- 2、本地方法栈 native方法引用的对象;
- 3、方法区中 类静态属性 引用的对象;
- 4、方法区中常量引用的对象。
4、有哪些类加载器?双亲委派模式,哪些场景是打破双亲委派模式?
- 类加载
- 作用:类加载器将字节码加载到内存,并对数据进行校验,解析和初始化,最终形成可被虚拟机直接使用的 java 类型。
- 过程:加载、链接、初始化
- 四种类加载器:启动类加载器扩展类加载器系统类加载器用户自定义类加载器
- 目前没有使用场景
- 双亲委派模型 先让最上层去加载
- 哪些场景是打破双亲委派模式?重点
- 我知道有两种方式来破坏双亲委派模型
- 第一种,继承ClassLoader抽象类,重写loadClass方法,在这个方法可以自定义要加载的类使用的类加载器。
- 第二种,使用线程上下文加载器,可以通过java.lang.Thread类的setContextClassLoader()方法来设置当前类使用的类加载器类型。
- java.jdbc.Driver(SPI) Tomcat容器,也存在破坏双亲委派的情况,来实现不同应用之间的资源隔离。
- tomcat自定义了类加载器,重写loadClass方法使其优先加载自己目录下的class文件,来达到class私有的效果
5、线上服务器出现频繁 Full GC,怎么排查?介绍下 JVM 调优的过程?
GC与调优经验 消息推送出现了数据洪峰 即10W条数据在短时间内(10分钟)推送
- 场景1:哪些内存需要回收? 方法区和堆
- 场景2:什么时候回收?在堆内存不足时触发,使用可达性算法判断对象是否存活
- 场景3:如何回收死亡的对象?(使用分代收集思想)
- 新生代:Serial、PraNew(配合CMS使用)、Parallel Scavenge
- 复制算法 新生代空间使用
- 老年代:Parallel Old、CMS
- **标记-清除算法 ** CMS 目标:最短回收停顿时间
- 如何解决STW:将GC过程细化,区分出必须STW场景和非必须STW场景。从业务场景出发解决问题
- 缺点1:标记-清除算法无法清理浮动垃圾
- 缺点2:并发清理可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生
- 整堆:G1
- 标记 - 整理算法 G1 不会产生内存空间碎片
- 目标:实现指定的GC暂停时间,同时还能保持较高的吞吐量
- 重点概念:卡表:标记老年代是否存在对新生代的引用
- G1适用场景
- 追求RT:对外提供服务的服务器
- GC耗时严重:大堆jvm
- 多核服务器(并发GC)
- 使用场景1:新生代使用 parNew ,老年代使用 CMS
- 使用场景2:整堆使用G1 目前来说G1属最佳实践
- 为什么有两个幸存空间? 解决碎片问题
- 新生代 ( Young )和老年代( Old ) 的比例:默认比例1:2
- 遇到什么场景,需要调整这个比例?
- 场景4:什么时候触发young GC? 新生代
- ①eden区满;②新创建的对象大小 > Eden所剩空间
- 经验值:YGC耗时在100ms以内,50ms以内尤佳
- 场景5:什么时候触发F GC? 老年代
- ①内存分配速度大于 YGC速度;②空间分配担保失败;③大对象分配失败;④设置了参数
-XX:CMSInitiatingOccupancyFaction
,老年代可用内存大于历次年轻代GC后进入老年代的对象的平均大小,但是老年代已使用内存超过该参数指定的比例,自动触发Full GC - 经验值:FGC最多几小时1次,1天不到1次尤佳,耗时在1s以内,500ms以内尤佳
- 场景6:调优经验,主要是做过一些什么样的调优?
- 1、为啥调优:如果使用合理的JVM参数设置,在大多数情况下应该是不需要调优的,少量场景需要调优,配置监控告警工具:使用Arthas dashboard
- 查看当前 JVM 堆内存参数配置是否合理
- 查看堆中对象的统计信息
- 查看堆存储快照,分析内存的占用情况
- 查看堆各区域的内存增长是否正常
- 查看是哪个区域导致的GC
- 查看GC后能否正常回收到内存
- 我们的场景:消息推送出现了数据洪峰即10W条数据在短时间内(10分钟)推送
- 限流方案是:推送平台通过消费MQ,执行职责链进行数据推送,根据业务类型在当前推送队列中的数量进行限流;
- 原因:网络请求跟不上上游MQ的投递速度,导致内存逐渐被打满了;
- 瓶颈:在于JVM 堆内存参数配置不合理
- 我们的目标:尽可能降低GC频率和耗时
- 方案:
- 1、JVM方面,调大年轻代大小:由1G调整为 1.5G eden与serviver比例:10:1:1修改为8:1:1,避免年轻代过多进入老年代
- 2、代码方面:修改限流器,设定一个jvm堆内存的使用率,当超过这个阈值后对当前的消费线程进行阻塞,直到使用率低于阈值后再进行放行,并且设置一个最大阻塞时间,超过该时间后放行该消费者线程
- 结果:性能提升了一倍
- 优化前 推送花了35分钟,fullGC 312次 单次耗时 991ms
- 优化后 推送花了18分钟,fullGC 104次 单次耗时 436ms
6、定位问题常用哪些命令?
- 使用 jstack 分析cpu问题;
- 使用
jstat -gc pid 1000
命令来对gc分代变化情况进行观察; - 使用
dashboard -i 10000 -n 3
命令来对gc分代变化情况进行观察; iostat -d -k -x
来进行分析磁盘问题;- 使用JMAP定位代码内存泄漏
jmap -dump:format=b,file=filename pid
来导出dump文件; - 通过pmap来查看下进程占用的内存情况
pmap -x pid | sort -rn -k3 | head -30
。
2、相关文章
2.1、JVM相关 - 知识体系
首先按照上述学习思路
,理解总体知识点在全局上与知识体系之间的对应关系。
2.2、JVM相关 - 类加载
理解类字节码和类的加载机制。
- 源代码通过编译器编译为字节码,再通过类加载子系统进行加载到JVM中运行
- 在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析。
- 这篇文章将带你深入理解Java 类加载机制
2.3、JVM相关 - 内存结构
因为类字节码是加载到JVM内存结构中的,所以紧接着理解JVM内存结构。
- 本文主要对JVM 内存结构进行讲解,注意不要和Java内存模型混淆了
2.4、 JVM相关 - JMM
通过理解JVM与硬件之间的联系,理解Java 通过其内存模型保证数据线程安全等,这是JVM在并发上底层的支持。
- 很多人都无法区分Java内存模型和JVM内存结构,以及Java内存模型与物理内存之间的关系。本文从堆栈角度引入JMM,然后介绍JMM和物理内存之间的关系, 为后面
JMM详解
,JVM 内存结构详解
,Java 对象模型详解
等铺垫。
- 本文主要转载自 Info 上深入理解Java内存模型, 作者程晓明。这篇文章对JMM讲的很清楚了,大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现;java 内存模型的设计,及其与处理器内存模型和顺序一致性内存模型的关系
2.5、JVM相关 - GC
理解Java GC机制,如何回收内存等。
- 垃圾收集主要是针对堆和方法区进行;程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
- 本文讲解讲解常见的垃圾收集算法,首先思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收? 然后讲解内存回收的具体实现-垃圾收集器 ,最后对Java中 对象引用类型及具体使用场景 做了探讨,jvm的自动垃圾回收策略使得程序员摆脱了编程中繁杂的内存管理,可以把精力专注于系统业务。
- G1垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方在ZGC还没有出现时也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
- ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器, 是JDK 11+ 最为重要的更新之一,适用于大内存低延迟服务的内存管理和回收。在梳理相关知识点时,发现美团技术团队分享的文章 新一代垃圾回收器ZGC的探索与实践比较完善(包含G1收集器停顿时间瓶颈,原理,优化等), 这里分享给你,帮你构建ZGC相关的知识体系。
- 本文整理自美团技术团队, 这篇文章将可以帮助你构建CMS GC相关问题解决的知识体系,分享给你。
2.6、JVM相关 - 排错调优
最后围绕着调试和排错,分析理解JVM调优参数,动态字节码技术及动态在线调试的原理;学会使用常用的调试工具和在线动态调试工具等。
- 本文对JVM涉及的常见的调优参数和垃圾回收参数进行阐述
- 本文以两个简单的例子(
堆内存溢出
和MetaSpace (元数据) 内存溢出
)解释Java 内存溢出的分析过程
- Java 堆外内存分析相对来说是复杂的,美团技术团队的Spring Boot引起的“堆外内存泄漏”排查及经验总结可以为很多Native Code内存泄漏/占用提供方向性指引。
- Thread Dump是非常有用的诊断Java应用问题的工具。
- Java 在线问题排查之通过linux常用命令排查。
- Java 在线问题排查之通过java调试/排查工具进行问题定位。
- 本文主要梳理常见的JVM可视化的分析工具,主要包括JConsole, Visual VM, Vusial GC, JProfile 和 MAT等。
- 本文主要介绍Alibaba开源的Java诊断工具,开源到现在已经1.7万个点赞了,深受开发者喜爱。具体解决在线问题,比如:
- 这个类从哪个 jar 包加载的? 为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到? 难道是我没 commit? 分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到JVM的实时运行状态?
- Debug用来追踪代码的运行流程,通常在程序运行过程中出现异常,启用Debug模式可以分析定位异常发生的位置,以及在运行过程中参数的变化;并且在实际的排错过程中,还会用到Remote Debug。IDEA 相比 Eclipse/STS效率更高,本文主要介绍基于IDEA的Debug和Remote Debug的技巧。
- 本文转载自 美团技术团队胡健的Java 动态调试技术原理及实践, 通过学习java agent方式进行动态调试了解目前很多大厂开源的一些基于此的调试工具
- 线上环境 FGC 频繁,如何解决?重点:得靠经验来解决
- 纵横数据如何应对洪峰推送。纵横系统最初定位为数据推送平台,用于将公司内部系统的各项业务数据(如:合同、公共、采购计划等)推送至对接的外部第三方平台。后续随着业务的扩张,逐渐出现了需要主动获取第三方系统的数据这一类的场景,纵横慢慢从一个数据推送平台演变成了公司的业务数据出口网关。
- web-agg在7月底容器化后,于8月13日内存泄漏被oom killer,之后频繁触发pod内存使用率超过90%告警。