JVM(java虚拟机)性能分析
一、jvm性能调优的目标---降低垃圾回收的频率和时间
JAVA 程序运行时,jvm 自动进行内存的回收和释放,将死亡的对象从内存里面移除,以释放更多的内存空间供新生的对象使用。这个过程就是 JVM 的垃圾回收,又称之为 GC。新时代垃圾回收,称之为 MinorGC,老年代垃圾回收称之为 MajorGC。GC 的时候所有线程都会暂停等待 GC 完成,频繁 GC 会极大的影响应用性能。Jvm 调优的一个主要目标就是降低 GC 频率和时间
二、Jvm 的内存空间
1.Jvm 的内存空间包括上以下个部分
堆区: 堆区分为 年轻代(分为 eden 和 存活区,存活区是 S0 S1) ,老年代,用来存放不同类型的对象。 例如 朝生暮死的小对象,长数组的大对象,长生不死
堆空间最大是物理内存的1/4,最小是物理内存的1/64,没有设置固定式,就是最大最小活动。
年轻代:年轻代有三个区域,一个 n eden 区和 2 2 个 r survivor 区。 新的对象都是在 n eden 创建的,然后在 r survivor 区域内多次 gc ,杀不死的对象或者过大的对象直接进去老年代
老年代 :老年代主要存放超龄对象( ( 超过年龄阈值) ) 和大对象( ( 超过尺寸阈值) ) 。老年代空间不足的时候,直接触发 fullgc
方法区: 存储被虚拟机加载的类信息、常量、字符串常量、类静态变量、编译后的代码数据等
栈区xss: java 线程在运行时需要申请线程栈,虚拟机栈中存放每个方法在执行的同时创建的栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。当一个方法被调用,栈帧入栈;当这个方法返回,栈帧出栈
应用可提交内存 = 最大堆内存+xss*线程数
程序计数器 : 程序计数器记录着当前线程所执行的字节码的行号指示器 ,即当前运行位置。它和 U CPU 的寄存器并称为 进程切换 的上下文
2.Jmap -heap {PID} 可以查看 JVM 内存空间分配
三、GC过程以及原理
新的内存对象是在eden中存活,如果eden满了做一次YGC,清空eden。残留的垃圾对象会扔到S0,eden又满了,做第二次YGC,清空eden和S0,把残留的垃圾对象会扔到S1,周而复始
有部分数据始终无法GC,会被打上存活标签【每次GC年龄加1】,超出年龄上线 (默认 16 岁 【-XX:MaxTenuringThreshold 年轻代GC最大年龄】) ,垃圾数据进去老年代。
老年代空间满了,进行一次FullGC,返回是所有堆内存空间,FGC的时间会很长,超过200ms就问题很大了。FGC的时候CPU是暂停的,长时间暂停会导致业务数据丢失
如果年轻代的对象尺寸超出老年代的剩余大小,也会FullGC
年轻代 GC: 新创建的对象都会被分配到 Eden 区( ( 没有超出尺寸阈值的对象 ), 这些对象经过第一次 Minor GC 后如果 还活着 , 就 被复制到两块 Survivor 区 轮转( 交替复制) ,在 Survivor 区中每熬过一次 GC ,年龄就会增加 1 岁,当年龄增加到一定程度时( (默认 16 岁) ,就会被 扔 到 老年 代中。 因为这里 存放的基本都是朝生暮死的小对象 ,所以 采用复制算法 进行 GC 。即 将内存分为两块,每次只用一块 。 当这一块内存用完,将没死 的对象复制到另外一块 ,优点是不会产生内存碎片 。
老年代 GC : 超出尺寸阈值和年龄阈值的对象都会进入老年代。随着 Minor GC 的持续进行,老年代对象也会持续增长,最终老年代的空间也不够 了 ,就会执行 MajorGC 。 老年代 使用标记清除算法 。 首先从对象的集合中进行遍历,如果发现对象 依然引用, 就 打上存活的标记 ,接着再去 对象集合进行二次遍历, 把那些 没有被打标记的对象清除掉。 因为需要 2 次扫描对象,所以老年代的 GC 时间会很长
查看进程的GC情况:【jstat -gcutil {pid} 1000】
四、参数配置
五、内存溢出
Kill process or sacrifice child 【强杀进程 OOMkiller】--内核层面
Out of swap space 【swap 满了 --改空间或者关掉swap 】--内核层面
memory cgroup out of memory --内核层面【3.x内核层面的问题,开启keem的方法,申请的内存不会被释放 查看内核版本 uname -r】
ls -al /sys/kernel/slab/| grep inode_cache |more 查看slab缓存-
cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo 查看内存泄露,不报错就没有问题
dmesg | grep -i memory 查看内存日志
如果有以上三个问题--解决方案
1:内核参数文件 /boot/grub2/grub.cfg 中添加 cgroup.memory=nokmem 让系统禁用 cgroup 的 kmem
2:内核参数文件 /boog/grub2/grub.cfg 中添加 cgroup_disable=memory关掉整个 cgroup memory
3:升级内核版本到 4.x
StackOverflow【栈内存溢出】 --应用层面
无限递归循环,超出栈帧深度,栈空间耗尽
执行了大量方法,导致线程栈空间耗尽
方法内声明了海量的局部变量
解决方案:
最常用的是修改启动参数 -Xss 增加栈内存空间,但是在某些异常场景下,这种做法毫无用处
通过程序抛出的异常堆栈,修复引发无限递归调用的异常代码
Java heap space【堆内存溢出】--应用层面
请求创建了一个超大对象(数组),超出了 heap 区域内存空间
内存持续泄漏,大量对象没用被 GC,JVM 无法对其自动回收
解决方案:
通常只要将 -Xmx 参数调高即可
如果是超大对象,需要检查合理性
如果是内存泄漏,需要找到持有的对象,修改代码设计
GC overhead limit exceeded--应用层面
GC花费98%的时间只回收了2%的内存,持续5次
-XX:-UseGCOverheadLimit
解决方案:
添加 JVM 参数-XX:-UseGCOverheadLimit
检查代码的死循环,优化它
检查是否存在内存泄露,优化它
Unable to create new native thread --应用层面
Java 无法直接访问到操作系统底层(例如上图所说的 allocateDirect 本地内存),为此 Java 使用 native 方法来扩展 Java 线程的功能。当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会返回这个错误
原因:
线程数超过操作系统最大线程数(thread_max)
操作系统的内存耗尽,拒绝本次 native 内存分配
Direct buffer memory Java 应用通过 Direct ByteBuffer 直接访问堆外内存。堆外内存不足,返回此错误
NIO 模式下需要使用 ByteBuffer 来读写数据,它使用 Native 函数库直接分配堆外内存。应用程序通过 Direct ByteBuffer 直接访问堆外内存,实现高速 IO。但是 ByteBuffer.allocateDirect 分配的是本地内存,不归 GC 管。正式因为不需要内存拷贝所以速度相对较快。不过由此带来一个问题,因为分配的都是本地内存,所以堆内存很少使用,JVM 就不执行 GC,DirectByteBuffer 对象就无法被回收。此时虽然堆内存充足,但本地内存却不够了,就会出现 java.lang.OutOfMemoryError: Directbuffer memory
解决方案:
调整 启动参数 - - XX:MaxDirectMemorySize 设置 Direct ByteBuffer 的上限值
检查 JVM 参数是否有 - - XX:+DisableExplicitGC 选项,如果有就去掉,该参数会使 System.gc() 失效
物理内存不足,升级配置
六、常见的线程问题
CPU 使用率不高但是响应时间很长
对进行线程 dump,检查是不是有线程卡在了 IO、数据库这些地方
检查是否有“ Waiting on condition”,如果有则说明线程在等待某种条件来唤醒。比如 sleep 时间,网络 IO,磁盘 IO
检查是否有线程死锁( 线程死锁指的是两个线程互相持有对方的锁,无法释放)
CPU 的 的 s us 使用率高,负载很高,响应很长
多次堆栈,检查是否有某个线程一直在执行同一个方法
七、堆栈日志分析
常见线程堆栈状态
waiting on condition:线程等待某个条件的发生;线程进入了 sleep;线程在等待网络的读写
Waiting for Monitor Entry and in Object.wait():等待获取线程 monitor
"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f6e4813e800
nid=0x3803 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
这一段线程堆栈的意思是,线程进入 waiting on condition 状态,等待” C2
CompilerThread”编译代码,当前状态是 runnable