前言
记录java虚拟机常见的问题场景。
提示:以下是本篇文章正文内容,下面案例可供参考
一、JAVA内存结构体系
1.整体结构
2.堆结构
这块区域是JVM中最大的,用于存储应用的对象和数组,也是GC主要的回收区,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
堆内存分为三部分:新生代、老年代。
引申:新生代s0,s1区域作用?
背景:如果没有s区,我们的对象直接到达
目的:降低老年代的内存分配压力,通过设置两个s区来对年轻对象进行拦截,降低fullGc的次数
引申:为什么是8:1:1
新生代98%的对象都是朝生夕灭的,所以我们只需要预留10%的空间放存活的对象。
引申:为什么分代?
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
3、栈结构
栈帧空间在什么销毁:
a.当我们方法执行结束之后,栈帧空间也会销毁
b.方法抛出异常。
4.程序计数器
程序计数器记录我们当前线程执行的行号。(只有在多线程中才有
作用,线程切换的时候通过程序计数器知道在哪行继续执行。)
程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
5.本地方法栈
本地方法栈和JVM栈的差异
他们都是线程私有的,区别是JVM栈为JVM执行Java方法(也就是字节码)服务,而本地方法栈为JVM使用到的Native方法服务
二、双亲委派机制
https://www.jianshu.com/p/538c49e4c132
三、JAVA虚拟机调优场景
1.CPU占用过高
场景:死循环
问题分析:
1)业务量瞬间飙升->正常现象,可考虑扩容
2)程序出现死循环->以下步骤进一步分析问题
技术手段定位:
1)top命令查看cpu占用情况
这样就可以定位出cpu过高的进程。在linux下,top命令获得的进程号和jps工具获得的vmid是相同的:
2)用top -Hp命令查看线程的情况
可以看出7287一直在占用线程cpu
3)把线程号转换为16进制
[root@localhost ~]# printf “%x” 7287
1c77
4)用jstack工具查看线程栈情况
通过jstack工具输出现在的线程栈,再通过grep命令结合上一步拿到的线程16进制的id定位到这个线程的运行情况,其中jstack后面的7268是第(1)步定位到的进程号,grep后面的是(2)、(3)步定位到的线程号。
从输出结果可以看到这个线程处于运行状态,在执行com.spareyaya.jvm.service.EndlessLoopService.service这个方法,代码行号是19行,这样就可以去到代码的19行,找到其所在的代码块,看看是不是处于循环中,这样就定位到了问题。
2.死锁
死锁并没有第一种场景那么明显,web应用肯定是多线程的程序,它服务于多个请求,程序发生死锁后,死锁的线程处于等待状态(WAITING或TIMED_WAITING),等待状态的线程不占用cpu,消耗的内存也很有限,而表现上可能是请求没法进行,最后超时了。在死锁情况不多的时候,这种情况不容易被发现。
可以使用jstack工具来查看
(1)jps查看java进程
[root@localhost ~]# jps -l
8737 sun.tools.jps.Jps
8682 jvm-0.0.1-SNAPSHOT.jar
(2)jstack查看死锁问题
由于web应用往往会有很多工作线程,特别是在高并发的情况下线程数更多,于是这个命令的输出内容会十分多。jstack最大的好处就是会把产生死锁的信息(包含是什么线程产生的)输出到最后,所以我们只需要看最后的内容就行了
3.内存泄漏
出现泄漏的场景:
使用静态集合类,或者监听器没关,创建的连接等导致的对象没有被回收;
程序发生内存泄漏后,进程的可用内存会慢慢变少,最后的结果就是抛出OOM错误。发生OOM错误后可能会想到是内存不够大,于是把-Xmx参数调大,然后重启应用。这么做的结果就是,过了一段时间后,OOM依然会出现。最后无法再调大最大堆内存了,结果就是只能每隔一段时间重启一下应用。
内存泄漏的另一个可能的表现是请求的响应时间变长了。这是因为频繁发生的GC会暂停其它所有线程(Stop The World)造成的。
为了模拟这个场景,使用了以下的程序
代码如下(示例):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
Main main = new Main();
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
main.run();
}
}
private void run() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
// do something...
});
}
}
}
运行参数是-Xms20m -Xmx20m -XX:+PrintGC,把可用内存调小一点,并且在发生gc时输出信息,运行结果如下
可以看到虽然一直在gc,占用的内存却越来越多,说明程序有的对象无法被回收。但是上面的程序对象都是定义在方法内的,属于局部变量,局部变量在方法运行结果后,所引用的对象在gc时应该被回收啊,但是这里明显没有。
为了找出到底是哪些对象没能被回收,我们加上运行参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.bin,意思是发生OOM时把堆内存信息dump出来。运行程序直至异常,于是得到heap.dump文件【这种方法dump出来的快照会比较大】
可以看到居然有21260个Thread对象,3386个ThreadPoolExecutor对象,如果你去看一下java.util.concurrent.ThreadPoolExecutor的源码,可以发现线程池为了复用线程,会不断地等待新的任务,线程也不会回收,需要调用其shutdown()方法才能让线程池执行完任务后停止。
其实线程池定义成局部变量,好的做法是设置成单例
拓展,另一种方式获取dump文件
在线上的应用,内存往往会设置得很大,这样发生OOM再把内存快照dump出来的文件就会很大,可能大到在本地的电脑中已经无法分析了(因为内存不足够打开这个dump文件)。这里介绍另一种处理办法:
(1)用jps定位到进程号
因为已经知道了是哪个应用发生了OOM,这样可以直接用jps找到进程号135988
(2)用jstat分析gc活动情况
jstat是一个统计java进程内存使用情况和gc活动的工具,参数可以有很多,可以通过jstat -help查看所有参数以及含义
上面是命令意思是输出gc的情况,输出时间,每8行输出一个行头信息,统计的进程号是24836,每1000毫秒输出一次信息。
输出信息是Timestamp是距离jvm启动的时间,S0、S1、E是新生代的两个Survivor和Eden,O是老年代区,M是Metaspace,CCS使用压缩比例,YGC和YGCT分别是新生代gc的次数和时间,FGC和FGCT分别是老年代gc的次数和时间,GCT是gc的总时间。虽然发生了gc,但是老年代内存占用率根本没下降,说明有的对象没法被回收(当然也不排除这些对象真的是有用)。
(3)用jmap工具dump出内存快照
jmap可以把指定java进程的内存快照dump出来,效果和第一种处理办法一样,不同的是它不用等OOM就可以做到,而且dump出来的快照也会小很多。
jmap -dump:live,format=b,file=heap.bin 24836
这时会得到heap.bin的内存快照文件,然后就可以用eclipse来分析了。
另外一个例子:https://www.jianshu.com/p/15637724ef16
参考内存溢出和内存泄漏的例子:https://blog.youkuaiyun.com/crazymakercircle/article/details/114421142
4.元数据空间溢出
元数据区:元数据区的概念出现在Java8以后,在Java8以前成为方法区,元数据区也是一块线程共享的内存区域,主要用来保存被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。
参考:
https://segmentfault.com/a/1190000023289961
大量的反射会
总结:
以上三种严格地说还算不上jvm的调优,只是用了jvm工具把代码中存在的问题找了出来。我们进行jvm的主要目的是尽量减少停顿时间,提高系统的吞吐量。
但是如果我们没有对系统进行分析就盲目去设置其中的参数,可能会得到更坏的结果,jvm发展到今天,各种默认的参数可能是实验室的人经过多次的测试来做平衡的,适用大多数的应用场景。
如果你认为你的jvm确实有调优的必要,也务必要取样分析,最后还得慢慢多次调节,才有可能得到更优的效果。
性能调优归纳
1. jps
jps 是 JDK 提供的Java进程查看⼯具,ps 是 Process Status 的缩写, 这个⽤法也很简单。
直接输⼊ jps 命令,就能列出当前系统下,所有正在运⾏的 Java 进程以及进程对应的 PID 。
$ jps
17680 Jps
109 Main
2、jstat
jstat 的全称是 statistics monitoring tool ,顾名思义,统计数据监控⼯具,就是⽤来对 Java程序的资源
和性能进⾏实时监控的。
它提供的功能还挺多的,但是我们最常⽤的,还是它的 GC 实时监控功能,可以说,这个⼯具是线上监
控 GC 情况最好的⼯具之⼀。
它的使⽤⽅式也很简单:
jstat -gcutil [-t] 3s 1000
-gcutil :GC 相关区域的使⽤率(utilization)统计
-t :可选,⽤于打印时间戳,即 JVM 启动到现在的秒数
3s :采样频率,默认单位为ms,可以使⽤ s 或 ms 结尾,如果使⽤ s,则频率就是秒。
1000:采样总次数
让我简单给⼤家解释⼀下,每⼀列的意思
3、jmap
如果 JVM 参数设置没问题,但却出现频繁 GC 且内存未被回收的情况的话,那就要考虑⼀下是否存在内
存泄漏的情况了。这时候就需要祭出我们的 B超,jmap。
它常⽤的功能有3个:
-heap pid :打印 堆内存/内存池 的配置和使⽤信息。
-histo :看哪些类占⽤的空间最多。
live :也可以只显⽰存活对象,会触发⼀次 Full GC。
-dump:live,format=b,file=xxx.hprof pid :dump 当前的堆内存。
live :只 dump 存活对象,如果不指定这个参数,则默认会 dump 堆中的所有对象。
live参数实际上是产⽣⼀次Full GC来保证只看还存活的对象。
format=b :⼆进制的格式
file=xxxx :将 dump 的内容转储到什么⽂件中。
4、调优思路总结
1)JVM性能调优的评估指标及调优示例
参考:https://www.jianshu.com/p/32358bea9c9e
2)线上问题排查思路
观察问题现象
借助jvm命令工具:jps,jstat,jstack,jmap–>打印堆文件
使用笛第三方工具进行分析:
三、垃圾回收器
1、垃圾回收算法
1)标记-清除
标记要回收的对象,然后进行清除
缺点:
效率问题
空间碎片
2) 标记-复制算法
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
缺点:只有一半内存可以用
3)标记-整理算法
把存活的对象移向一端,然后进行清理;
4)分代收集算法(这也是为什么要分少年区和老年区的问题)
新生区:使用标记-复制,付出复制成本就可以完成垃圾收集
老年代:标记-清除或标记-整理
2、垃圾收集器
1)serial收集器
它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。【新生代采用标记-复制算法,老年代采用标记-整理算法。】
2)ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。【新生代采用标记-复制算法,老年代采用标记-整理算法。】
拓展:除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
3)Parallel Scavenge 收集器
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU),并行的。使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
【新生代采用标记-复制算法,老年代采用标记-整理算法】
这是 JDK1.8 默认收集器
4)CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
CMS优点:并发收集、低停顿。
CMS缺点:
1、对CPU资源敏感,用户线程和垃圾收集线程会争抢资源,会对垃圾收集的吞吐量造成影响。
2、无法处理浮动垃圾,在并发清理的过程中从非垃圾对象编程垃圾对象的对象就是浮动垃圾,这种浮动垃圾会在下一次gc的时候被清理掉。
3、因为使用的是标记–清除算法,所以会产生大量的内存碎片,但是我们可以通过设置-XX:+UseCMSCompactAtFullCollection参数来让每次做完CMS之后做一次标记–整理来消除内存碎片。
4、concurrentmode failure(并发失败),在CMS执行并发的过程中,由于用户线程还在执行,万一丢进来的大对象老年代放不下,这个时候并发过程进行不下去了,这个时候就产生了并发失败,接下来不会OOM,而是会STW,用serial old垃圾收集器来回收(因为只有Serial和ParNew可以和CMS配合使用),然后就相当慢,给用户的感觉就是网页卡住了,解决方案是配置-XX:CMSInitiatingOccupancyFraction参数,详情请看下面辅助知识的CMS核心参数。
5)G1 收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
四、八股文面试题
1、新生代会stw吗
copy算法会STW,但是因为对象比较少,所以也没有多大的影响。
2、类加载过程
加载:硬盘上查找通过io读入字节码文件,使用类时进行加载。例如调用main方法,new对象等等
验证:校验字节码文件的正确性
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用
初始化:对类的静态变量初始化为代码中指定的值,执行静态代码块
3、为什么pc寄存器被设定为私有?
背景:多线程在一个时间段才会执行一个线程的方法,cpu不停的切换任务,导致经常中断和恢复。那我们怎么才能避免恢复后线程继续执行代码的位置?
解决:给每一个线程分配一个寄存器,保证每一个线程之间不受影响
4、对象创建五个步骤?
5、对象分配过程