JVM 性能调优实战(3)

实战 - CPU过高问题定位

top + jstack

首先使用 top 命令

PID    COMMAND      %CPU  TIME     #TH   #WQ  #PORTS MEM    PURG   CMPRS  PGRP
61603  idea         174.5 03:07.44 64/2  2    471    1662M+ 5732K  207M-  61603
62085  java         67.8  00:07.23 19/1  1    79     50M    0B     0B     61603
133    WindowServer 21.6  85:32.91 16    7    2494+  647M+  2688K- 88M-   133
541    WeChat       21.1  11:14.79 34    11   11555+ 266M+  4036K+ 167M-  541

使用命令:top -Hp 61603

显示 61603 这个进程中线程的情况

PID   USER PR NI   VIRT    RES    SHR  S %CPU %MEM  T IME+ COMMAND
36044 root 20 0 2848360   32136  13144 R 99. 9 3.2 0:39.78 Thread-0

使用命令:printf %x 36044

把 36044 转化成 十进制

8ccc

使用命令: jstack 61603 > xxx.txt

把结果输出到 xxx.txt

在 xxx.txt 中往后搜索 30 行 8ccc 线程相关的信息

cat xxx.txt | grep -A 8ccc

输出:

"Thread-Ø" #12 prio=5 OS_ prio=o cpu= 198582. 74ms elapsed=738.21s tid=0x00007 f14d4149000 nid=0x8ccc
runnable [Øx000Ø7 f 14bca64000] 
java.lang.Thread.State: RUNNABLE
at java.io.File0utputStream.writeBytes( java. base@11.0.7/Native Method )
......
at actualCombat.CPU100.main(CPU100.java:5) 

这样就找到了CPU 过高的代码

JMC

直接在终端运行 : jmc

全称:java mission control

直接就可以看到哪个线程占有率过高

可能导致CPU占用率过高的场景与解决方案

  • 无限while循环
    ● 尽量无限循环
    ● 让循环执行得慢一点

  • 频繁 GC
    ● 尽量降低 GC 频率

  • 频繁创建对象
    ● 合理使用单例

  • 序列化和反序列化
    ● 使用合理的 API 实现功能

  • 正则表达式
    ● 减少字符匹配期间执行的回溯

  • 频繁的线程上下文切换
    ● 降低切换的频率

实战 - 内存溢出

堆内存溢出

演示堆内存溢出代码,并定位问题
总结堆内存溢出的场景与解决方案
分析项目中可能存在堆内存溢出的代码并解决

public class HeapOOMTest {
    private List<String> oomList = new ArrayList<>();

    public static void main(String[] args) {
        HeapOOMTest oomTest = new HeapOOMTest();
        while (true){
            oomTest.oomList.add("1");
        }
    }
}

项目中可能出现的问题:
分页查询的时候没有控制分页的大小,查询多少页,控制一下就行了

栈内存溢出

Java虚拟机规范

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将拋 StackOverflowError
  • 如果虚拟机的栈内存允许动态扩展,当无法申请到足够内存时,将抛出 OutOfMemoryError

Hotspot虚拟机

  • 栈内存不可扩展
  • 统一用Xss设置栈的大小
    有的虚拟机可以用Xss设置虚拟机栈,Xoss设置本地方法栈

案例一:

package actualCombat;

public class StackOOMTest {
    private int stackLength = 1;

    private void stackLeak() {
        stackLength++;
        this.stackLeak();
    }

    public static void main(String[] args) {
        StackOOMTest oom = new StackOOMTest();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length: " + oom.stackLength);
            throw e;
        }

    }
}
stack length: 17189
Exception in thread "main" java.lang.StackOverflowError
	at actualCombat.StackOOMTest.stackLeak(StackOOMTest.java:8)

设置 VM option 之后 : -Xss150k

输出:

The stack size specified is too small, Specify at least 160k

重新设置 VM option : -Xss200k

输出:

stack length: 1239
Exception in thread "main" java.lang.StackOverflowError
	at actualCombat.StackOOMTest.stackLeak(StackOOMTest.java:7)

每调用一次方法都会创建一个栈帧,栈的大小越小,整个栈里面能容纳的栈帧也越小,所以能够递归的次数也越小

案例二:

package actualCombat;

import java.util.concurrent.atomic.AtomicInteger;

public class StackOOMTest2 {
    private AtomicInteger integer = new AtomicInteger();

    private void dontStop(){
        while (true){

        }
    }
    public void newThread(){
        while (true){
            Thread thread = new Thread(()->{
               dontStop();
            });
            thread.start();
            System.out.println("线程创建成功,threadName = " + thread.getName());
            integer.incrementAndGet();
        }
    }

    public static void main(String[] args) {
        StackOOMTest2 stackOOMTest2 = new StackOOMTest2();
        try {
            stackOOMTest2.newThread();
        }catch (Throwable e){
            System.out.println("创建的线程数: " + stackOOMTest2.integer);
            System.out.println("异常发生: " + e);
        }
    }
}

如何让上面的代码运行更多的线程?

  • 减少Xss配置
  • 栈能分配的内存=
    机器总内存-操作系统内存-堆内存-方法区内存-程序计数器内存-直接内存
  • 尽量杀死其他程序
  • 操系统对线程数目的限制

cat /proc/sys/kernel/thread-max

  • 作用:系统支持的最大线程数,表示物理内存决定的理论进程数上限,一般会很大
  • 修改:sysctl -w kernel.threads-max=7762

cat /proc/sys/kernel/pid_max

  • 作用:查看系统限制某用户下最多可以运行多少进程或线程
  • 修改:sysctl -w kernel.pid_max=65535

cat /proc/sys/vm/max_map_count

  • 作用:限制一耳光进程可以拥有的VMA(虚拟机内存区域)的数量,虚拟内存区域是一个连续的虚拟,在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间
  • 修改:sysctl -w vm.max_map_count=262144

ulimit -u

  • 作用:查看用户最多可启动的进程数目
  • 修改:ulimit -u 65535

方法区溢出

线程共享,用来存储被虚拟机加载的类型信息、常量、静态变量等

package actualCombat;

import java.util.HashSet;
import java.util.Set;

/**
 * @description: 方法区溢出测试
 * @author: xwy
 * @create: 下午1:28 2021/3/2
 **/

public class MwthodAreaOOMTest {
    public static void main(String[] args) {
        // 使用 set 保持着常量池的引用, 避免 Full GC回收常量池行为
        Set<String> set = new HashSet<>();
        int i = 0;
        while (true) {
            // intern() : native 方法
            // 如果字符串常量池里面已经包含了等于字符串 X 的字符串,就返回常量池中找过字符串的引用
            // 如果常量池中不存在,那么就会把当前字符串添加到常量池,并返回这个字符串的引用
            /*
            JDK 6: -XX:PermSize=6m -XX:MaxPermSize=6m
            报永久代溢出 (java.lang.OutOfMemoryError:PermGen space)
            ==========
            JDK 7: -XX:PermSize=6m -XX:MaxPermSize=6m
            不报错,原因: JDK7 把字符串常量池放到堆了,设置 -Xmx6m 会报堆内存溢出
            ==========
            JDK 8+:同JDK 7
             */
            set.add(String.valueOf(i++));
        }
    }
}

方法区总结

  • 不同JDK版本,方法区存放结构不同,相同的代码报错也可能不同
  • 方法区溢出的场景
    常量池里对象太大
    加载的类的“种类”太多
    ● 动态代理的操作库生成了大量的动态类
    ● JSP项目(jsp是在第一次访问的时候编译成 java 类 ,在极端场景下,应用在启动的时候是没有问题的,但是喉咙访问了一些页面,对应的 jsp编译成 java 类了,结果竟然把方法区给打满了)
    ● 脚本语言动态类加载

如何避免方法区溢出

防止类加载过多导致的溢出

  • <=JDK 7 :设大PermSize、 MaxPermSize
  • JDK>8 :留空元空间相关的配置,或设置合理大小的元空间
属性作用默认值
-XX:MetaspaceSize元空间的初始值,元空间占用达到该值就会触发垃圾收集,进行类型卸载,同时,收集器会自动调整该值。如果能够释放空;间,就会自动降低该值;如果释放空间很少,那么在不超过-XX:MaxMetaspaceSize的情况下,可适当提高该值。21810376字节
-XX:MaxMetaspaceSize元空间最大值受限于本地内存大小
-XX:MinMetaspaceFreeRatio垃圾收集后,计算当前元空间的空闲百分比,如果小于该值,就增加元空间的大小.40%
-XX:MaxMetaspaceFreeRatio垃圾收集后,计算当前元空间的空闲百分比,如果大于该值,就减少元空间的大小70%
-XX:MinMetaspaceExpansion元空间增长时的最小幅度340784 字节
-XK:MaxMetaspaceExpansion元空间增长时的最大幅度5452592 字节

直接内存与直接内存溢出

直接内存

  • 《Java虚拟机规范》并没有定义这块区域
  • 不属于语虚拟机运行时数据区

什么是直接内存?

  • 直接内存是一块由操作系统直接管理的内存,也叫堆外内存
  • 可以使用Unsafe或ByteBuffer分配直接内存
  • 可用XX:MaxDirectMemorySize控制,默认是0,表示不限制

为什么要有直接内存?

直接内存使用场景

  • 有很大的数据需要存储,生命周期很长
  • 频繁的IO操作,比如并发网络通信

直接分配内存的示例:

  • Unsafe.allocateMemory(size)
  • ByteBuffer.allocateDirect(size);

直接分配内存溢出

package actualCombat;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @description: 直接内存溢出
 * @author: xwy
 * @create: 下午2:13 2021/3/2
 **/

public class DirectMemoryOutTest {

    private static final int GB_1 = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        // 通过反射获取 Unsafe 类并通过其分配直接内存
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);

        int i = 0;
        while (true) {
            unsafe.allocateMemory(GB_1);
            System.out.println(++i);
        }
    }
}

输出:

131055
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at actualCombat.DirectMemoryOutTest.main(DirectMemoryOutTest.java:25)

和前面报的OutOfMemoryError不一样,前面的有小尾巴,这个直接就报了OutOfMemoryError

package actualCombat;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @description: 直接内存溢出
 * @author: xwy
 * @create: 下午2:13 2021/3/2
 **/

加上 VM options: `-XX:MaxDirectMenorySize`

/*
1. Unsafe 类导致直接内存报错没有小尾巴
2. -XX:MaxDirectMenorySize 对 Unsafe类无效
 */
public class DirectMemoryOutTest {

    private static final int GB_1 = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        // 通过反射获取 Unsafe 类并通过其分配直接内存
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);

        int i = 0;
        while (true) {
            unsafe.allocateMemory(GB_1);
            System.out.println(++i);
        }
    }
}
package actualCombat;

import java.nio.ByteBuffer;

/**
 * @description:
 * @author: xwy
 * @create: 下午2:18 2021/3/2
 **/
/*
1. ByteBuffer 直接内存溢出,报错 java.lang.OutOfMemoryError Direct buffer memory
2. -XX:MaxDirectMenorySize 对 ByteBuffer 有效
 */
public class DirectMemoryOutTest2 {
    private static final int GB_1 = 1024 * 1024 * 1024;

    public static void main(String[] args) {
        int i = 0;
        while (true) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(GB_1);
            System.out.println(++i);
        }
    }
}

有关直接内存的经验之谈

  • 堆Dump文件看不出问题或者比较小,可考虑直接内存溢出问题
  • 配置内存时,应给直接内存预留足够的空间

直接内存总结

  • 直接内存也叫堆外内存, IO效率较高
  • 可以用Unsafe类或ByteBuffer来分配

Unsafe

  • 溢出时报 : java.lang.OutOfMemoryError
  • -XX:MaxDirectMemorySize 不起作用

ByteBuffer
溢出时报: java.lang.OutOfMemoryError: Direct buffer memory
-XX:MaxDirectMemorySize 起作用
ByteBuffer底层也是用Unsafe去分配内存的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值