jvm 优化和诊断

内存溢出场景

  1. Java heap space,堆内存不足
  2. metaspace 或 Permgen space,方法区不足
  3. GC overhead limit exceeded,超过 98% 时间在做 GC,但是回收内存不到 2%
    1. 默认开启 UseGCOverheadLimit,当超过 98% 时间在做 GC,但是回收内存不到 2%时报
    2. 可以关闭这个检查 -XX: -UseGCOverheadLimit,就会报 OOM java heap space 堆空间不足
    3. 可能的问题:内存泄露,检查流是否关闭,是否有大对象,是否有死循环等
  4. Unableto createnewnativethread:内存不足不能创建线程了。把 -Xss 设置小点,意思每个线程内存使用少点,理论上就能多创建线程了,但实际不是这样的,具体能创建多少线程数量的公式入如下:
    1. (MaxProcessMemory - JVMMemory - ReservedOsMemory) / ThreadStackSize
    2. ThreadStackSize 就对应 -Xss 的值,这只是分母,不同操作系统和位数都不一样,记住结论就好了
    3. 结论: 32 位操作系统,还是比较严格遵循这个公式的,64 位的操作系统不遵循,因为操作系统有自己的保护机制(64位的 MaxProcessMemory 是一个超大的数,理论上可以常见无限的线程,这回触发才做系统的保护机制

JIT 优化

后端编译器的优化,默认开启,两个优化项:栈上分配锁消除。栈上分配涉及内存逃逸和标量替换知识

1 栈上分配对于没有发生内存逃逸的对象,该对象直接分配在栈上,不会分配在堆上,用完就出栈,不涉及 GC

  • 内存逃逸:方法里的变量,既不是入参传进来的,也不会 return 出去,这个对象就没有发生内存逃逸

    public User getUser(){
    	return new User(1, "张三"); // 返回了对象,其他地方可能会用到这个这个对象,不是当前方法独自使用,存在内存逃逸
    }
    
    public void initUser(User user){
    	user.setAge(18); // 虽然没返回,但是 user 是入参传进来的,这里设置后,别的地方也可能使用,也不是独自使用,存在内存逃逸
    }
    
    public void test(){ // 没有返回,也不是参数传进来,对象只在当前方法使用,不存在内存逃逸
        User user = new User();
        user...
    }
    
  • 标量替换:可以使用局部变量代替对象

    // User 假设有这三个属性,如果没有发生内存逃逸,会用局部变量的形式替换对象
    public void test(){
    	int id = xxx;
        int age = xxx;
        String name = xxx;
    }
    
  • jvm 参数配置

    -XX:+DoEscapeAnalysis # 开启栈上分配(当对象内存不逃逸时有用)
    -XX:+EliminateAllocations # 开启标量替换(两者必须同时开启,因为 hotpot 虚拟机的栈上分配是标量替换来体现的)
    

2 锁消除synchronized 没必要存在的时候,会自动消除

public class TestClass{
    public void test(){
        User user = new User();
        synchronized(user) { // test 的同步代码块的锁对象是方法内部创建的,所以每个线程调用这个方法都会创建一个对象
            ....
        }
    }
}

优化1:内存调整

增大堆内存会减少 FULL GC、提升吞吐量。但是也有弊端,当真 FULL GC 的时候要回收的垃圾也会比较多,STW 时间会增加,不能两全其美

java 官方有一些建议如下,这些都是基于 FULL GC 后,以老年代已使用空间作为参照(可以多次 FULL GC 求平均值):

  1. 整个堆:老年代已使用空间的 3-4 倍
  2. 方法区:老年代已使用空间的 1.2 - 1.5 倍
  3. 年轻代:老年代已使用空间 1 - 1.5 倍
  4. 老年代:老年代已使用空间的 2-3 倍

如何触发 FULL GC?1,生成 dump 文件;2,VisualVM 或 JConsole 工具点击【回收垃圾】功能

优化2:堆大小自适应调整策略

-XX:+UseAdaptiveSizePolicy 会自动调整堆大小,具体是伊甸园和幸存区的大小比例

默认伊甸园和幸存区的比例是 8:1:1,如果 UseAdaptiveSizePolicy 生效,伊甸园和幸存区的比例就是 6:1:1,保留了 20% 的空间来动态自适应

  • 对于 Parallel GC 无论设置开启还是关闭,UseAdaptiveSizePolicy 都会生效,如果要使 UseAdaptiveSizePolicy 失效,需要设置 SurvivorRatio 参数(SurvivorRatio 是设置伊甸园和幸存区的比例的参数)
  • 对于 CMS GC,默认开启 UseAdaptiveSizePolicy,可以设置 -XX:-UseAdaptiveSizePolicy 来关闭

优化3:CPU 占用高

  1. 先确认哪个进程占用高,多种方式

    • top 显示所有进程,带有 cpu、内存 等使用情况,然后按 P ,根据 cpu 使用率从高到底排序
    • ps aux --sort=-%cpu | head -n 10 这条命令会列出 CPU 使用率最高的前 10 个进程
    • top -p $(pgrep -d',' -f 'java') 这个命令只显示 java 进程的 cpu 使用情况
    • htopatop 也可以,但是要安装,有的 linux 默认没有这俩命令
  2. 有了进程后,就要分析该进程的线程了

    top -Hp 12323 看12323 这个进程下所有线程使用资源情况,可以看出哪个线程使用 cpu 过高

  3. 把 线程id 转成 16 进制,因为 jstack 命令要跟 16 进制的线程id

  4. jstack 进程id | grep -A20 16进制线程id 看某个进程下某个线程信息(-A20 后 20 行)

注意有可能是 GC 线程占用高,这个一半 不用做处理,要处理的话,如果是 CMS 和 G1 可以试试降低 STW 时间

优化4:内存占用高

这个没啥好说的,用命令或分析 dump,看哪个线程使用内存多了,根据 VisualVM 或 JConsole 分析 dump 文件也能看到哪个线程、哪种类型的对象使用内存高,从而定位到代码

内存泄露(是垃圾但是没有被回收):因为 java 使用的是可达性分析,这种情况的原因多半是可达性能达到,但是某个环节不适用了还引用着,比如流没关闭等情况。可以使用 jstat 命令,隔一段时间观察一下老年代使用情况,如果一直递增,说明有可能内存泄露了

优化5:诊断命令

  • jps:查看正在运行的 java 进程信息

    命令 作用
    jps 显示进程 ID 和主类名
    jps-q 显示进程ID
    jps -l 显示进程 ID、主类的全类名(包括全路径)
    jps -v 显示进程 ID、主类名以及 JVM 启动参数
    jps -m 显示进程 ID、主类名以及传递给主类的参数
    jps -lvm 显示进程 ID、主类名、JVM 参数及主类参数
    jps -v | grep 进程id 指定指定进程的 进程 ID、主类名以及 JVM 启动参数
    • q 独立,lvm 可以组合
    • v 和 m 一个虚拟机的启动参数,一个是主方法的入参
    • 如果 jvm 启动参数有 -XX:-UsePerData 就不能使用 jps 命令了
  • jstat:显示正在运行的 java 进程的虚拟机信息

    命令 作用 描述
    jstat -class 进程id 查看进程的类加载信息 当前进程对应的 jvm 加载类的个数、时间和卸载类的个数和时间
    jstat -gc 进程id 查看进程 jvm 内存信息,单位是字节 S0C、S1C:幸存区容量
    S0U、S1U:幸存区使用容量
    EC、EU:伊甸园区容量和已使用容量
    OC、OU:老年代容量和已使用容量
    MC、MU:方法区容量和已使用容量
    YGC、YGCT:轻 GC 次数、轻 GC 时间
    FGC、FGCT:重 GC 次数、种 GC 时间
    GCT:总 GC 时间
    jstat -gc 进程id 1000 10 每隔1秒打印一次,总共打印10次
    jstat -gcutil 进程id 查看进程 jvm 内存使用信息 gc 打印容量和使用容量,gcutil 打印使用容量
    jstat -gcutil 进程id 1000 50 每个1秒答应一次,总共打印50次

    jstat -gc 进程id -间隔时间 -次数

  • jinfo:查看 jvm 参数(虽然也能修改,但是修改的项很有限,主要还是查看 jvm 参数)

    命令 作用
    jinfo -sysprops 进程id 可以查看由 System.getProperties() 方法取得的参数
    jinfo -flags 进程id 查看 jvm 参数。比如堆空间大小,使用的哪个 GC 收集器等
  • jmap:导出堆内存此时的 dum 文件或查看此时堆内存使用情况。还有别的用法,别的用的不多

    命令 作用
    jmap -dump:format=b,file=/root/a.hprof 进程id 导出此时的 dump 文件
    jmap -dump:live,format=b,file=/root/a.hprof 进程id 功能同上,只关注存活对象
    jmap -heap 进程id 显示内存情况,容量、已使用、已使用百分比
    jmap -heap 进程id >/root/a.txt 功能同上,输出到一个文件中
    jmap -histo 进程id 此时占用内存从大到小排列的对象,[B byte 类型数组,[C char 类型数组
    jmap -histo 进程id >/root/b.txt 功能同上,输出到文件中
  • jstack:查看线程信息

    命令 作用
    jstack 进程id 打印当前进程所有线程信息
    jstack 进程id | grep -A30 16进制线程id 打印进程指定的线程信息,注意线程id是16进制的

    需要关注的状态

    状态 含义
    Deadlock 死锁
    Waiting on condition 等待资源
    Waiting on monitor entry 等待获取监视器
    Blocked 阻塞

优化6:诊断工具

有很多 jconsole、visual VM、eclipse MAT、jprofileer(收费),jconsole 是官方提供的,jdk5 开始有的,visual VM 是 jdk6 之后官方提供的,相比于 jconsle,visual VM 更强大

优化7:arthas

posted @ 2023-04-14 16:51  CyrusHuang  阅读(12)  评论(0编辑  收藏  举报