JVM调优的原则?
- 不要为了调优而调优,首先确定的应该是项目的架构与代码已经没有优化的空间了,再考虑进行JVM的调优工作,不能指望通过JVM调优来使得性能有一个质的飞越
- 从三个属性中(吞吐量,延迟,内存)中选择两个进行JVM调优,称之为调优3选2,在处理吞吐量和延迟问题时,GC能使用的内存越大,应用运行也就越流畅,这也叫GC内存最大化原则
什么情况下需要进行调优?
- Full GC数量频繁,GC停顿时间超长(超过1s)
- 应用出现OOM异常或者吞吐量下降,响应性能不高,
- 应用CPU占用过高,内存占用过高
吞吐量和低延迟?
吞吐量:代码时间/(代码时间+垃圾回收时间),吞吐量越高,算法越好
低延迟:STW越短,响应时间越好,算法越好,目的在于缩短或者完全消除因垃圾收集器引起的停顿
一个GC算法只可能针对于以上两个目标之一或者尝试找到一个折中方案,吞吐量优先的垃圾回收器有Parallel的年轻代和老年代版本,响应时间优先的垃圾回收器有CMS和G1
常用性能调优工具和常用参数
常用工具:jvisualvm,jconsole,MAT(提示可能内存泄露的点)
常用参数解释:
- -Xms:JVM启动时申请的初始Heap大小
- -Xmx:JVM运行时可申请的最大Heap值,为了避免每次GC后JVM重新分配内存,一般设置为同一个值
- -Xmn:设置Heap中新生代的大小,通过Xms-Xmn可以得到老年代的大小
- -Xss:设置每个线程可使用的栈大小
- -XX:TraceClassLoading/TraceClassUnLoading可以在日志中追踪类加载和卸载的情况
CPU占用过高的排查流程?
- top命令查看出cpu最高的进程pid(这里也可以用jps -l命令)
- [ps -mp 进程号 -o THREAD,tid,time]定位具体的线程(这里也可以使用top -Hp pid)
- 使用[printf "%x\n" 线程id]将需要的线程转换为16进制格式
- 使用[jstack 进程ID|grep 16进制线程id -A60]打印出前60行信息
ps常用参数:
-A 显示所有进程(等价于-e)(utility)
-a 显示一个终端的所有进程,除了会话引线
-N 忽略选择。
-d 显示所有进程,但省略所有的会话引线(utility)
-x 显示没有控制终端的进程,同时显示各个命令的具体路径。dx不可合用。(utility)
-p pid 进程使用cpu的时间
-u uid or username 选择有效的用户id或者是用户名
-g gid or groupname 显示组的所有进程。
-f 全部列出,通常和其他选项联用。如:ps -fa or ps -fx and so on.
-l 长格式(有F,wchan,C 等字段)
-j 作业格式
-o 用户自定义格式
-m 显示所有的线程
-H 显示进程的层次(和其它的命令合用,如:ps -Ha)(utility)
-a 显示同一终端下的所有程序
-A 列出所有的进程
-w 显示加宽可以显示较多的资讯
-au 显示较详细的资讯
-aux 显示所有包含其他使用者的进程
-e 显示所有进程,环境变量
-f 全格式
-h 不显示标题
-l 长格式
-w 宽输出
内存占用过高排查流程?
- 查找进程Id: top查看内存占用过高的进程pid
- [jmap -heap pid]:查看JVM堆内存的分配情况
- [jmap -histo:live pid|head -n 100]:查看占用内存比较多的存活对象
什么情况会抛出OOM?OOM之前会发生什么
满足以下两个条件中的任意一个会产生OOM
- JVM的98%时间都用于内存回收
- 每次回收的内存小于2%
OOM之前的现象:
- 每次垃圾回收的时间越来越长,且full gc的次数越来越多
- 老年代的内存越来越大,每次full gc后,只有少量的内存被释放掉
线上死锁排查?
- [jps -l]查找到可能有问题的进程id
- 执行[jstack -F 进程id]命令
- 如果说可以远程连接到JVM,可以使用jconsole或者jvisualvm,以图形化界面检测是否死锁
jstack命令:
-l:打印关于锁的信息
-F:当-l没有响应的时候,强制打印栈信息
-m:答应jaba和native c/c++框架所有栈信息
JVM的主要组成部分及其作用?
JVM包含两个子系统和两个组件,子系统为:类装载和执行引擎,组件为运行时数据区与本地接口
- 类装载:根据给定的类全限定类名,装载class文件到运行时数据区的方法区
- 执行引擎:将字节码翻译成底层系统指令,交由cpu去执行
- 本地接口:与本地方法库交互,是其他编程语言交互的接口
- 运行时数据区域:jvm的内存
运行时数据区:java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区
- 程序计数器:当前线程所执行的字节码的行号指示器,利用它可以完成分支,跳转,线程恢复,循环等工作
- 虚拟机栈:存储局部变量表,操作数栈,动态链接,方法出口等信息
- 本地方法栈:虚拟机栈服务java方法,本地方法栈服务native方法
- 堆:被线程共享的一块区域,几乎所有的对象实例都在这里分配
- 方法区:用于存储已经被虚拟机加载的类信息,常量,静态常量,即时编译后的代码等数据
堆和栈的区别?
堆的物理地址分配对象是不连续的,因此性能会慢一些,栈使用的是数据结构中的栈,先进后出,物理地址分配是连续的所以性能快
堆所分配的内存是在运行期间确认的,大小不固定,一般远远大于栈,栈分配的内存在编译时就要确定
对存放对象的实例和数组,栈存放的是局部变量,操作数栈,返回结果
堆对于整个程序是可见的,栈对于线程可见,生命周期与线程相同
Java是否会存在内存泄露问题?
有,当长生命周期的对象持有短生命周期对象的引用就会发生,经过短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致无法回收
垃圾收集器?
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代和老年代),而前六种收集器回收的范围仅限于新生代或老年代。
标记清除:从GC roots中找到存活对象,然后清除其他没有被标记的对象
标记整理:在标记清除的基础上,将存活对象压缩到内存的一端,避免了内存碎片问题
复制算法:把内存划分为两个区域,每次只使用其中的一个区域,垃圾收集时,把存活对象复制到另一个区域,清空当前块的内存
分代收集:根据对象的存活周期划分内存,一般包括年轻代,老年代和永久代
CMS垃圾收集器?
牺牲了吞吐量来获得最短回收停顿时间的垃圾回收器,对于要求服务响应速度的应用,这种垃圾回收器非常合适,使用-XX:+UseConcMarkSweepGC来使用
CMS的步骤分为:
- 初始标记:会标记GC Roots直接关联的对象,这个过程会发生stw,因为只标记一层所以速度很快,该过程是单线程
- 并发标记:该过程不会停止用户线程,做的是从GC Roots向下追溯标记可达对象的工作
- 重新标记:该过程中保证的是可达对象一定被标记了,需要stw
- 并发清理:不会stw,并发的回收不可达的对象,这个过程用户线程会不断的产生垃圾,变为浮动垃圾,留给下一次GC处理
优点:
- 将stw的时间降到最低,给电商网站的用户带来最好的体验
缺点:
- 内存碎片问题
- 浮动垃圾问题
- 有时候会占用大量的cpu时间,当cms运行过程中预留的空间不够用,会使用serial old来进行老年代垃圾回收导致停顿时间变长
G1垃圾收集器?
相较于CMS,G1最突出的改进有以下两点:
- 基于标记整理算法实现,不会产生内存碎片
- 可以精确控制停顿时间,在不牺牲吞吐量的前提之下,实现低停顿垃圾回收
流程如下:
前三个流程基本和CMS一致
筛选回收:排序各个Region的回收价值和成本,根据用户期望的GC停顿时间来指定回收计划,最后按照极坏回收一些价值高的region中的垃圾对象,需要stw
简述分代垃圾回收器是怎么工作的?
分代有两个分区:老年代(2/3)和新生代(1/3)
新生代使用的复制算法,Eden,To Survivor,From Survivor.默认占比为8:1:1,执行流程如下:
- Eden+From->To,并且使对象的年龄+1,大于15就被丢进老年代,大对象也会直接进入老年代
- 清空Eden与From
- From和To交换身份
内存分配策略:
- 对象优先在Eden区分配,当Eden没有足够空间,发起一次Minor GC
- 大对象直接进入老年代,参数-XX:PretenureSizeThreshold
- 长期存活的对象进入老年代
- 空间担保机制:在Minor GC发生之前,JVM要预估老年代是否能容纳Minor GC后新生代晋升到老年代的存活对象,以确定是否需要提前触发GC回收老年代空间
- 动态年龄判断:新生代对象年龄没有达到阈值,但是新生代存活对象的年龄达到某个值且这些对象大小的总和大于survivor空间的一半或者说survivor空间不足以容纳这些对象,对象直接进入老年代
类装载的过程?
加载:根据查找路径找到相应class文件然后导入,在方法区生成类信息,在堆中添加class对象
验证:检查加载的class文件的正确性
准备:给类中的静态变量,或者是final修饰的变量分配内存空间
解析:将符号引用转为直接引用
初始化:对静态变量和静态代码块执行初始化
对象创建过程?
- java在new一个对象的时候,会先查看所属类有没有被加载到内存中,如果没有会先进行类的加载过程
- 申请对象内存:指针碰撞或者空闲列表
- 成员变量赋默认值
- 设置对象头(哈希码,GC分代年龄,偏向锁id,指向类的元数据的指针)
- 调用构造方法init
方法区信息的演变
方法区是一个JVM的规范,永久代与元空间都是实现方式,JDK1.8之前,方法区的实现是永久代,之后开始使用元空间来替代,以前永久代的静态变量和常量池移动到堆内存中,其他的内容移动到了元空间,元空间直接在本地内存分配
元空间不会发送垃圾回收,永久代会发生垃圾回收
为什么要使用元空间代替永久代呢?
永久代所使用的物理内存与堆内存是连续的,由于类和方法信息难以确定大小,所以指定永久代的大小困难,容易出现永久代溢出,太大则容易导致老年代移溢出,并且永久代会为GC带来不必要的复杂度,并且回收效率偏低,且字符串存在于永久代中,容易出现性能问题和内存溢出
方法区变成元空间后,物理内存不再与堆内存连续,而是存在于本地内存中,理论上机器内存多大,元空间就有多大