JVM调优<二>优化案例

186 阅读2分钟

四、优化案例

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

优化参数(调大堆内存)

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"

结果: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

关闭栈上分配测试(会在堆内存中分配对象)

开启栈上分配测试

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.122_100 = 12.2M,如果堆内存设置1个G,那么年轻代大小大约就是333M,那么333M*80%/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"

增加线程数会增大吞吐量(-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参数

响应时间控制在100ms怎么保证?

做压测控制延迟时间

  1. 查看系统状况
  2. 定位问题的线程
  3. 查看问题线程的堆栈
  4. jstat查看进程内存情况
  5. jstack和jmap分析进程堆栈和内存情况
  • 使用jmap命令dump文件,拿到文件后使用mat、jprofler等工具分析:jmap -dump:format=b,file=20230416.dump 6764