JVM篇<十>调优

一、调优的概述

1.1调优的目的

  • 防止出现OOM,进行JVM规划和预调优
  • 解决程序运行中各种OOM
  • 减少Full GC出现的频率,解决运行慢、卡顿问题

二、生产环境的问题

2.1、堆溢出

原因:
1、代码中可能存在大对象分配
2、可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法:
1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用MAT等工具分析一下,检查是否存在内存泄漏的问题
3、如果没有找到明显的内存泄漏,使用-Xmx加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

案例:

 	/**
     * 案例1:模拟线上环境OOM
     * 参数设置:
     * -XX:+PrintGCDetails -XX:MetaspaceSize=64m
     * -XX:+HeapDumpOnOutOfNemoryError -XX:HeapDumpPath=heap/heapdump.hprof
     * -XX:+PrintGCDateStamps -Xms30M -Xmx30M -Xloggc:log/gc-oomHeap.log
     *
     */
    @RequestMapping("/oomTest")
    public void addObject(){
        System.err.println("oomTest"+peopleSevice);
        ArrayList<People> people = new ArrayList<>();
        while (true){
            people.add(new People());
        }
    }

2.2、元空间的溢出

原因:
1.运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
2.应用长时间运行,没有重启
3.元空间内存设置过小

解决方法:
1.运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
2.应用长时间运行,没有重启
3.元空间内存设置过小

案例:

	/**
     * 案例2:模拟元空间OOM溢出
     *参数设置:
     * -XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m-XSS512K -XX:+HeapDumponOutOfMemoryErrorl
     * -XX:HeapDumpPath=heap/heapdumpMeta.hprof -xx:SurvivorRatio=8
     * -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps-Xms60M -Xmx60M -Xloggc:log/gc-oomMeta.log
     */
    @RequestMapping("/metaSpaceOom")
    public void metaSpaceOom(){
        ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
        while (true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(People.class);
            enhancer.setUseCache(false);
            //enhancer.setUseCache(true);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
                System.out.println("我是加强类,输出print之前的加强方法");
                return methodProxy.invokeSuper(o,objects);
            });
            People people = (People)enhancer.create();
            people.print();
            System.out.println(people.getClass());
            System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
            System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
            System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
        }
    }

2.3、GC overhead limit exceeded

原因:
这个是DK6新加的错误类型,一般都是堆太小导致的。Sun官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出

解决:
1.检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2.添加参数-XX:-UseGCOverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap
space。
3. dump内存,检查是否存在内存泄漏,如果没有,加大内存。

测试:

	/**
     *
     * 测试 GC overhead limit exceeded
     * 参数设置:
     * -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
     * -XX:HeapDumpPath=heap/dumpExceeded.hprof
     * -XX:+PrintGCDateStamps -Xms10M -Xmx1OM-xloggc:log/gc-oomExceeded.log
     */
    public static void main(String[] args) {
        test1();

//        test2();
    }

    public static void test1() {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while (true) {
                list.add(UUID.randomUUID().toString().intern());
                i++;
            }
        } catch (Throwable e) {
            System.out.println("************i: " + i);
            e.printStackTrace();
            throw e;
        }
    }

	
	//回收效率大于2%所以只会出现堆空间不足
	public static void test2() {
        String str = "";
        Integer i = 1;
        try {
            while (true) {
                i++;
                str += UUID.randomUUID();
            }
        } catch (Throwable e) {
            System.out.println("************i: " + i);
            e.printStackTrace();
            throw e;
        }
    }

2.4 、线程溢出

注意:windos试不出来,超过windos上线会重启

线程创建公式:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Numberof threads
MaxProcessMemory 指的是进程可寻址的最大空间
VMMemory JVM内存
ReservedOsMemory 保留的操作系统内存
ThreadStackSize 线程栈的大小

注意:在32位操作系统下当前公式遵守的 在64位操作系统下MaxProcessMemory (最大寻址空间)这个值接近无限大,所以ThreadStackSize不影响公式的值

Linux查看线程数:
cat /proc/sys/kernel/pid_max 系统最大pid值,在大型系统里可适当调大
cat /proc/sys/kernel/threads-max 系统允许的最大线程数
maxuserprocess (ulimit -u)系统限制某用户下最多可以运行多少进程或线程
cat /proc/sys/vm/max_map_count

JVM篇<十>调优
程序

public class TestNativeOutOfMemoryError {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("i = " + i);
            new Thread(new HoldThread()).start();
        }
    }
}

class HoldThread extends Thread {
    CountDownLatch cdl = new CountDownLatch(1);

    @Override
    public void run() {
        try {
            cdl.await();
        } catch (InterruptedException e) {
        }
    }
}

三、性能优化

3.1性能监控

一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。
监控前,设置好回收器组合,选定CPU(主频越高越好),设置年代比例,设置日志参数(生产环境中通常不会只设置一个日志文件)。比如:
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=20M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGcCause

问题:

  • GC频繁
  • cpu load过高
  • OOM
  • 内存泄漏
  • 死锁
  • 程序响应时间过长

3.2性能分析

  • 打印GC日志,通过GCviewer或者http://gceasy.io来分析日志信息
  • 灵活运用命令行工具,jstack, jmap, jinfo等
  • dump出堆文件,使用内存分析工具分析文件(jconsole/jvisualvm/jprofiler)
  • 使用阿里Arthas,或jconsole,JVisualVM来实时查看JVM状态
  • jstack查看堆栈信息

3.3性能调优

  • 适当增加内存,根据业务背景选择垃圾回收器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池线程数量
  • 使用中间件提高程序效率,比如缓存,消息队列等

四、优化案例

4.1调整堆内存大小提高性能

测试参数设置:
setenv.sh文件中写入(大小根据自己情况修改): setenv.sh内容如下:

export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:SurvivorRatio=8"
export CATALINA_OPTS="$CATALINA_OPTS-Xmx30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"

打印信息

//查看运行进程id
jps
//查看运行信息
jstat -gc 进程id 间隔时间(毫秒)次数
例如 jstat -gc 5397 1000 5

JVM篇<十>调优

优化参数(调大堆内存)

export CATALINA_OPTS="$CATALINA_OPTS -xms120m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:SurvivorRatio=8"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx120m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"

JVM篇<十>调优
结果:FullGC次数大幅降低

4.2JIT编译器的优化

  • 逃逸分析:当前方法内new的对象被当前方法外所使用
  • 栈上分配
    • 将堆分配转化为栈分配。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。可以减少垃圾回收时间和次数。
    • JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

4.2.1栈上分配测试(并没有分配一个个对象,而是用变量替换去体现出来)

/**
 * 栈上分配测试
 * -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 *
 * 只要开启了逃逸分析,就会判断方法中的变量是否发生了逃逸。如果没有发生了逃逸,则会使用栈上分配
 */
public class StackAllocation {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        // 查看执行时间
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " ms");
        // 为了方便查看堆内存中对象个数,线程sleep
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    private static void alloc() {
        User user = new User();//是否发生逃逸? 没有!
    }

    static class User {

    }
}

jdk6之后默认开启栈上分配,测试需要关闭
-XX:-DoEscapeAnalysis

关闭栈上分配测试(会在堆内存中分配对象)
JVM篇<十>调优

开启栈上分配测试
JVM篇<十>调优

4.2.2同步消除

同步消除。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
  • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
public class SynchronizedTest {
    public void f() {
        /*
        * 代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,
        * 并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
        *
        * 问题:字节码文件中会去掉hollis吗?
        * 不会,因为只会由解释器,不会经过JIT编译器
        * */
        Object hollis = new Object();
        synchronized(hollis) {
            System.out.println(hollis);
        }

        /*
        * 优化后;
        * Object hollis = new Object();
        * System.out.println(hollis);
        * */
    }
}

4.2.3标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量

相对的,那些还可以分解的数据叫做聚合量(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

参数设置(默认开启)true
-XX:+EliminateAllocations:

代码体现:

public static void main (string [ ] args){
	alloc ( ) ;
}
private static void alloc (){
	Point point = new Point ( 1,2);
	system.out.println ( "point.x="+point.x+"; point.y="+point.y);
}
class Point {
	private int x;
	private int y;
	以上代码,经过标量替换后,就会变成:private static void alloc() {
	int x = l;I
	int y = 2;
	System.out.println ( "point.x="+x+"; point.y="+y);
}

测试代码

/**
 * 标量替换测试
 *  -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations
 *
 *  结论:Java中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换。
 *
 * @author shkstart  shkstart@126.com
 * @create 2021  12:01
 */
public class ScalarReplace {
    public static class User {
        public int id;
        public String name;
    }

    public static void alloc() {
        User u = new User();//未发生逃逸
        u.id = 5;
        u.name = "www.atguigu.com";
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " ms");

    }
}

逃逸分析总结:

  • 关于逃逸分析的论文在1999年就已经发表了,但直到DK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。
  • 其根本原因就是无法保证非逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
  • 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
  • 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
  • 注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。
  • 目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化, intern字符串的缓存和静态变量
  • 曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

4.3合理分配堆内存

4.3.1参数设置

  • Java整个堆大小设置,Xmx和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
  • 方法区(永久代 PermSize和MaxPermSize或元空间MetaspaceSize和MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
  • 年轻代Xmn的设置为老年代存活对象的1-1.5倍。

4.3.2老年代存活大小的计算

  • JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)

强制触发Full GC的方法
1、jmap -dump:live,format=b,file=heap.bin 将当前的存活对象dump到文件,此时会触发FullGc
2、jmap -histo:live 打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量.此时会触发FullGd
3、在性能测试环境,可以通过Java监控工具来触发FullGC,比如使用VisualVM和3Console,VisualVM集成了JConsole,VisualVM或者JConsole上面有一个触发GC的按钮。

估算GC频率
比如从数据库获取一条数据占用128个字节,需要获取1000条数据,那么一次读取到内存的大小就是128 B/1024 Kb/1024M) * 1000 = 0.122M,那么我们程序可能需要并发读取,比如每秒读取100次,那么内存占用就是0.122100 = 12.2M,如果堆内存设置1个G,那么年轻代大小大约就是333M,那么333M80%/12.2M =21.84s ,也就是说我们的程序几乎每分钟进行两到三次youngGC。

4.4调整ParallelGC比例

ParallelGC默认是6:1:1
调整参数设置

  • -XX:+SusvivorRatio:8
  • -XX:+UseAdaptivesizePolicy(自动调整策略)

注意:对于面向外部的大流量、低延迟系统,不建议启用此参数,建议关闭该参数。

4.5CPU占用很高排查方案

1、ps aux / grep java 查看到当前java进程使用cpu、内存、磁盘的情况获取使用量异常的进程
2、top -Hp 进程pid检查当前使用异常线程的pid
3、把线程pid变为16进制如31695-》 7bcf 然后得到Ox7bcf
4、查看信息(2种方式)

  • 1.jstack+进程的pid l grep -A20 Ox7bcf得到相关进程的代码
  • 2.将信息打印到文件中 jstack pid > 文件名

4.6G1线程的并发执行线程数对性能的影响

测试参数设置

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -xms 30m"
export CATALINA_OPTS="$CATALINA_OPTS -xm×30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc :/opt/tomcat8.5/logs/gc.log"
export CATALINA_OPTS="$CATALINA_OPTS-XX:ConcGCThreads=1"

JVM篇<十>调优

增加线程数会增大吞吐量(-XX:ConcGCThreads设置为2效果和4,8差不多 因为最多为并行垃圾回收的1/4)

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -xms 30m"
export CATALINA_OPTS="$CATALINA_OPTS -xm×30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc :/opt/tomcat8.5/logs/gc.log"
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=2"

4.7调整垃圾回收器对提高服务器的影响

根据服务器的cpu和性能合理使用垃圾回收器

4.8百万级的交易系统如何设置JVM参数

JVM篇<十>调优
响应时间控制在100ms怎么保证?
做压测控制延迟时间

上一篇:LeetCode-113-Path Sum II


下一篇:LeetCode-77-Combinations