1.4.1 说一说JVM由哪些部分组成 , 都有什么作用
JVM运行是数据区主要包括堆
虚拟机栈
方法区
本地方法栈
程序计数器
等五部分构成 , 还包括执行引擎和本地库接口
其中方法区和堆是线程共享区,虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里几乎所有的对象实例都在这里分配内存
Java虚拟机栈描述的是Java方法执行的线程内存模型:方法执行时,JVM会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务
方法区是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
程序计数器也被称为PC寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器
执行引擎就是用于执行在class中定义的指令
本地库接口用于和其他语言进行交互 , 提供和其他编程语言交互的入口
1.4.2 对象创建的过程了解吗
在JVM中对象的创建,从一个new指令开始:
- 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用
- 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程
- 类加载检查通过后,接下来虚拟机将为新生对象分配内存
- 内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值
- 接下来设置对象头,请求头里包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
1.4.3 什么是类加载器,类加载器有哪些?
所谓类加载器就是负责将.class文件(存储的物理文件)加载到内存中的一个工具
主要有四种类加载器:
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它
- 用户自定义类加载器 (user class loader),用户通过继承 java.lang.ClassLoader类的方式自行实现的类加载器。
1.4.4 说一下类装载的执行过程
加载是JVM加载的起点,具体什么时候开始加载,《Java虚拟机规范》中并没有进行强制约束,可以交给虚拟机的具体实现来自由把握。在加载过程,JVM要做三件事情
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了 , 类型数据安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象, 这个对象将作为程序访问方法区中的类型数据的外部接口
1.4.5 有没有了解过双亲委派模型
双亲委派是Java中类的一种加载机制 , 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
1.4.6 JVM为什么采用双亲委派机制
使用双亲委派是为了保证应用程序的稳定有序。保证一个类不会被重复加载 , 并且只会会被加载一次
例如 : 类java.lang.Object,它存放在rt.jar之中,通过双亲委派机制,保证最终都是委派给处于模型最顶端的启动类加载器进行加载,保证Object的一致。反之,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类。
1.4.7 说一说Java的垃圾回收机制
在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中, 有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收 , 这就是垃圾回收
1.4.8 对象什么时候可以被垃圾器回收
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法
引用计数法 : 一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
可达性分析算法 : 现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕 , 判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收
1.4.9 JVM 垃圾回收算法有哪些
JVM 垃圾回收算法有很多 , 主要包括 :
标记清除算法:
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
缺点 :
- 效率较低,标记和清除两个动作都需要遍历所有的对象
- (重要)通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的
标记整理算法 :
标记整理算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题
复制算法 :
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合
优点: 在垃圾对象多的情况下,效率较高 , 清理后,内存无碎片
缺点:分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
分代收集算法 :
在JDK1.8时,堆被分为了两份:新生代和老年代【1:2】,在JDK1.7的时候,还存在一个永久代
新生代主要用来存放新生的对象。一般占据堆空间的1/3。在新生代中,保存着大量的刚刚创建的对象,但是大部分的对象都是朝生夕死,所以在新生代中会频繁的进行MinorGC,进行垃圾回收。新生代又细分为三个区:Eden区、SurvivorFrom(S0)、ServivorTo区(S1),三个区的默认比例为:8:1:1
老年代主要存放应用中生命周期长的内存对象。老年代比较稳定,不会频繁的进行MajorGC。而在MaiorGC之前才会先进行一次MinorGc,使得新生的对象进入老年代而导致空间不够才会触发。当无法找到足够大的连续空间分配给新创建的较大对象也会提前触发一次MajorGC进行垃圾回收腾出空间
永久代指的是永久保存区域。主要存放Class和Meta(元数据)的信息。Classic在被加载的时候被放入永久区域,它和存放的实例的区域不同,在Java8中,永久代已经被移除,取而代之的是一个称之为“元数据区”(元空间)的区域。元空间和永久代类似,都是对JVM中规范中方法的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。
1.4.10 说一下 JVM 有哪些垃圾回收器?
在JVM中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器
串行垃圾收集器(Serial收集器),作用于新生代。是指使用单线程进行垃圾回收,采用复制算法。垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成
并行垃圾收集器(ParallelNew收集器) 在串行垃圾收集器的基础之上做了改进,采用复制算法。将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间 , JDK8默认使用此垃圾回收器
CMS垃圾收集器是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行
G1垃圾收集器 , 是一款同时应用于新生代和老年代、采用标记-整理算法、软实时、低延迟、可设定最大STW停顿时间的垃圾回收器,用于代替CMS,适用于较大的堆(>4~6G),在JDK9之后默认使用G1
1.4.11 强引用、软引用、弱引用、虚引用的区别?
强引用
最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收
User user = new User();
就是强引用
软引用
如果一个对象处于软引用,在内存空间足够的情况下,GC机制并不会回收它,而在内存空间不足时,则会在OOM异常出现之间对其进行回收。但值得注意的是,因为GC线程优先级较低,软引用并不会立即被回收
User user = new User();
SoftReference softReference = new SoftReference(user);
弱引用
在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。同样的,因为GC线程优先级较低,所以弱引用也并不是会被立刻回收。
User user = new User();
WeakReference weakReference = new WeakReference(user);
虚引用
- 例如:
PhantomReference a = new PhantomReference(new A(), referenceQueue);
- 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
1.4.12 有没有遇到过内存溢出 , 如何排查解决
内存溢出就是申请的内存超过了可用内存,内存不够了 , 在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能,重点是堆和栈
Java堆溢出 : Java堆用于储存对象实例,只要不断创建不可被回收的对象,比如静态对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常 OutOfMemoryError
- 误用线程池导致的内存溢出 , 使用了无界队列 , 一直添加任务 或者 线程数设置的比较大 , 创建了大量线程有可能导致堆内存溢出
- 死循环频繁创建大量的对象 , 或者从数据库查询大量的数据 , 有可能导致堆内存溢出
虚拟机栈溢出 : JDK使用的HotSpot虚拟机的栈内存大小是固定的,不断地去创建线程,因为操作系统给每个进程分配的内存是有限的,所以到最后,也会发生OutOfMemoryError异常 , 再比如递归调用没有正常退出, 递归的次数比较多也有可能出现OutOfMemoryError
- 递归调用的时候 , 没有设置退出条件, 递归次数比较多的时候有可能会出现栈溢出
- 在大量循环或者死循环的情况下有可能会出现栈溢出
首先当内存溢出问题发生的时候我们首先要定位问题 , 然后解决问题 , 一般内存溢出会出现的现象主要有 :
- 用户反馈接口响应时间变长, 程序比较卡
- CPU跑的过高,使用率持续飙升
- 内存占用持续升高
- 服务器运行日志中看到大量的服务调用失败
解决方案 :
启用备用服务器, 将流量切换到备用服务器 , 保证服务正常运行, 然后进行排查 , 甚至直接重启服务器
可以使用Nacos注册中心中的服务权重管理切换 , 先启动备用服务器, 然后慢慢降低问题服务器权重
首先要排除掉一些明显的情况 , 例如 :
- 查看服务器监控信息, 查看网络带宽使用是否异常 , 如果异常可能因为大量请求导致 ,需要分析请求来源是正常用户访问还是异常攻击 , 正常访问代表服务器性能不足, 需要进行调优或者加机器 , 异常攻击需要对应处理(黑名单 , IP限流之类的)
- 有没有出现下游服务调用异常(监控工具skyworking) , 看哪一个服务接口响应速度比较慢甚至直接超时
- 有没有出现慢SQL查询(监控工具或者慢查询日志)
- 检查之前稳定版本和目前线上版本的代码差异, 有没有出现大量循环 , 大量数据加载, 线程池设置不合理情况
- 如果实在无法定位可以通过JVM所提供的分析工具进行分析
JVM分析工具
- 通过jmap指定打印JVM的内存快照 dump
jmap只能打印在运行中的程序
有的情况是内存溢出之后程序则会直接中断 , 所以建议通过参数的方式的生成dump文件,
配置如下:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/app/dumps/
指定生成后文件的保存目录
- 使用
JVisualVM
工具去加载并分析dump文件
- 通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
- 找到对应的代码,通过阅读上下文的情况,进行修复即可
1.4.13 线上环境CPU占用100% , 如何排查解决👍
首先要定位问题 , 是哪一个程序导致的CPU使用率飙升 , 排查方式
- 使用top命令查看占用cpu的情况
- 通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:30978
- 查看当前进程中的线程信息
ps H -eo pid,tid,%cpu | grep 30978
- pid: 进程id
- tid 进程中的线程id
- % cpu使用率
- 通过上面分析,在进程(30978)中哪一个线程(30979)占用的cpu较高
- 把线程号转换为16进制 , 记住这个16进制的线程号
printf "%x\n" 30979
, 30979为10进制进程id
- 执行
jstack 30978
可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号
jstack 进程ID
- 找到问题出现在哪个位置 , 去源代码中找到代码, 进行修复即可
1.4.14 有没有遇到过内存泄露问题 , 如何排查解决
内存泄露问题排查 , 同内存溢出, 一般情况下内存泄露会导致内存溢出
1.4.15 有没有做过JVM调优 , 如何调优
要想调优首先就要确定问题 , 目前系统哪些地方出现了问题 , 哪些地方没有达到设计目标 , 需要进行调优
确定了问题之后 , 首先应该思考是否能够从架构方面, 代码优化方面或者数据库优化方面解决问题
从其他方面着手能不能解决问题 ,才需要进行JVM调优应该是Java性能优化的最后手段。
我认为JVM调优应该是系统优化的最后手段 , JVM本身就有自动内存管理机制 , 一般的项目调整一下初始堆内存(-Xms
)大小, 和最大堆内存(-Xmx
)大小也就行了
如果确实要进行JVM调优, 首先要确认系统有没有出现如下的问题 :
- Heap内存(老年代)持续上涨达到设置的最大内存值
- Full GC 次数频繁
- GC 停顿时间过长(超过1秒)
- 应用出现OutOfMemory 等内存异常
- 应用中有使用本地缓存且占用大量内存空间
如果存在上述问题 , 能不能通过架构调整和逻辑代码调整进行解决
- 我们编写的代码中有没有出现内存泄露 , 死循环 , 无限递归这样的问题
- 程序在执行的时候有没有在循环中创建大量的对象, 是否有必要 , 是否可以优化
- 是否存在大量的全局变量和大对象
- 本地缓存中有大量的数据存储, 是否可以切换到外部缓存中间件中
- 数据库SQL执行效率是否达到预期 , 是否还有优化空间 , 有没有查询大量数据到内存 , 是否有必要
- 服务器JVM参数有没有设置到最优
如果确定要进行JVM调优 , 我们可以从以下方面进行着手 :
- 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点
- 确定JVM调优量化目标
- 确定JVM调优参数(根据历史JVM参数来调整)
- 依次调优内存、延迟、吞吐量等指标
- 对比观察调优前后的差异
- 不断的分析和调整,直到找到合适的JVM参数配置
整个过程也不是一下就能完成的 , 要不断的测试和优化
常见的JVM调优参数设置 :
-Xms
:初始化堆内存大小,默认为物理内存的1/64(小于1GB)。
-Xmx
:堆内存最大值。默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
-Xmn
:新生代大小,包括Eden区与2个Survivor区。
-XX:SurvivorRatio=8
:Eden区与一个Survivor区比值 , 默认值Eden:S0:S1为 8:1:1 , 如果是4 那么就是4:1:1
-XX:NewRatio=4
:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-Xss512k
:设置每个线程的堆栈大小。每个线程堆栈大小为1MB , 应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-XX:PermSize=100m
:设置初始化永久代大小为100MB。
-XX:MaxPermSize=256m
:设置持久代大小为256MB。
-XX:MaxDirectMemorySize=1G
:设置直接内存大小。报java.lang.OutOfMemoryError: Direct buffer memory异常可以上调这个值。
-XX:ConcGCThreads=4
:CMS垃圾回收器并行线程线,推荐值为CPU核心数。
-XX:ParallelGCThreads=8
:新生代并行收集器的线程数。
还有很多参数 , 太多了记不住 , 之前整理了一个文档 , 需要用的时候可以查询
一般我们调整参数的比例 :
具体参数要根据情况设置
1.4.16 JVM 调优的参数可以在哪里设置参数值?
- 如果是普通的web项目 , 在Tomcat中部署 , 调整Tomcat中
catalina.sh
文件中的JAVA_OPTS变量即可
- 如果是SpringBoot项目 , 在启动项目的时候
java -jar
命令后面加入JVM参数即可 - 我们的项目是通过Docker容器部署的 , 容器启动的
entrypoint
指令设置的就是java -jar
启动程序的命令, 这个命令可以接收外部传递的ENV
的参数 , 所以可以在构建容器的时候可以通过docker run
指令的-e参数 , 设置JVM参数
1.4.17 JVM 调优的工具
JDK自带了很多性能监控工具,我们可以用这些工具来监测系统和排查内存性能问题
调优和问题排查过程中用的比较多的就是: jps
,jvisualvm
jstat
和jmap
同时也可以使用一些第三方的工具 , 例如 : arthas(阿尔萨斯)
Arthas 是 Alibaba 开源的 Java 诊断工具 , 通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率
链接 : 简介 | arthas