第28章:性能优化案例

1、性能优化案例1:调整堆大小提高服务吞吐量

1.1、修改tomcat JVM配置

生产环境下, Tomcat并不建议直接在catalina.sh里配置变量,而是写在与 catalina同级目录(bin目录)下的setenv.sh里。

1.2、初始配置

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:/mydata/tomcat8.5/logs/gc.log"

启动

[root@shuidi150 bin]# ./startup.sh 

查看日志:

1.3、优化配置

export CATALINA_OPTS="$CATALINA_OPTS -Xms120m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx120m"

优化后不需要full GC,吞吐量由原先的 6194.5 增加到 8360.5

2、性能优化案例2:JVM优化之JIT优化

2.1、堆,是分配对象的唯一选择吗?

在《深入理解Java虚拟机中》关于Java内存有这样一段描述:

随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

2.2、编译的开销

时间开销

解释器的执行,抽象的看是这样的:

输入的代码 -> 【解释器 解释执行】 -> 执行结果

JIT编译然后再执行的话,抽象的看则是:

输入的代码 -> 【编译器 编译】-> 编译后的代码 -> 【执行】 ->执行结果

注意:

说JIT比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释”这个动作快。JIT编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。所以,对“只执行一次”的代码而言,解释执行其实总是比JIT编译执行要快。

空间开销

对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10+是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致代码爆炸。这也就解释了为什么有

些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。

2.3、即时编译对代码的优化

2.3.1、逃逸分析

  • 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
  • 逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
  • 通过逃逸分析, Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。
  • 逃逸分析的基本行为就是分析对象动态作用域:

①当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

②当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

public void my_method(){
    V v = new V();
    // use v
    // ......
    v=null;
}

没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。

  • 逃逸分析包括:

全局变量赋值逃逸

方法返回值逃逸

实例引用发生逃逸

线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量

代码举例

/**
 * 逃逸分析
 *
 *  如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。
 *
 * @author shkstart
 * @create 2021 下午 4:00
 */
public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /*
    方法返回EscapeAnalysis对象,发生逃逸
     */
    public EscapeAnalysis getInstance(){
        return obj == null? new EscapeAnalysis() : obj;
    }
    /*
    为成员属性赋值,发生逃逸
     */
    public void setObj(){
        this.obj = new EscapeAnalysis();
    }
    //思考:如果当前的obj引用声明为static的,会发生逃逸吗?会!

    /*
    对象的作用域仅在当前方法中有效,没有发生逃逸
     */
    public void useEscapeAnalysis(){
        EscapeAnalysis e = new EscapeAnalysis();
    }
    /*
    引用成员变量的值,发生逃逸
     */
    public void useEscapeAnalysis1(){
        EscapeAnalysis e = getInstance();
        //getInstance().xxx()同样会发生逃逸
    }
    /*
    * 也发生了逃逸
    * */
    public void operate(EscapeAnalysis e){
        // e
    }
}

参数设置

  • 在JDK6u23版本之后, HotSpot中默认就已经开启了逃逸分析。
  • 如果使用的是较早的版本,开发人员则可以通过:

通过选项 -XX:+DoEscapeAnalysis 显式开启逃逸分析

通过选项 -XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果。

结论:

开发中能使用局部变量的,就不要使用在方法外定义。

2.3.2、代码优化一:栈上分配

使用逃逸分析,编译器可以对代码做如下优化:

栈上分配。将堆分配转化为栈分配。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。可以减少垃圾回收时间和次数。

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

代码举例

/**
 * 栈上分配测试
 * -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 *
 * 只要开启了逃逸分析,就会判断方法中的变量是否发生了逃逸。如果没有发生了逃逸,则会使用栈上分配
 *
 * @author shkstart  [email protected]
 * @create 2021  10:31
 */
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 {

    }
}

关闭逃逸分析

开启逃逸分析

2.3.3、代码优化二:同步省略(消除)

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

  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
  • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么IT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

代码举例

/**
 * 同步省略说明
 * @author shkstart  [email protected]
 * @create 2021  11:07
 */
public class SynchronizedTest {
    public void f() {
        /*
        * 代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,
        * 并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
        *
        * 问题:字节码文件中会去掉hollis吗?
        * */
        Object hollis = new Object();
        synchronized(hollis) {
            System.out.println(hollis);
        }

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

2.3.4、代码优化三:标量替换

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

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

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

代码举例

/**
 * 标量替换测试
 *  -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations
 *
 *  结论:Java中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换。
 *
 * @author shkstart  [email protected]
 * @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");

    }
}

/*
class Customer{
    String name;
    int id;
    Account acct;

}

class Account{
    double balance;
}


 */

参数设置

-XX:+EliminateAllocations 开启了标量替换(默认开启),允许将对象打散分配在栈上。

当配置参数 -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations 时 花费的时间为: 54 ms

当配置参数 -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations 时 花费的时间为: 4 ms

结论:Java中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换。默认逃逸分析,标量替换 均开启,两者同时开启才有效果,单独开启逃逸分析无效。

2.3.5、逃逸分析小结

逃逸分析小结:逃逸分析并不成熟

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

3、性能优化案例3:合理配置堆内存

3.1、推荐配置

在案例1中我们讲到了增加内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生FullGC的时候,GC时间会相对比较长,如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?

分析:

依据的原则是根据 Java Performance里面的推荐公式来进行设置。

Java整个堆大小设置,Xmx和Xms设置为老年代存活对象的3-4倍,即 Fullgc之后的老年代内存占用的3-4倍。

方法区(永久代 PermSize和MaxPermSize和或元空间 MetaspaceSize和MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。

年轻代Xmn的设置为老年代存活对象的1-1.5倍。

老年代的内存大小设置为老年代存活对象的2-3倍。

但是,上面的说法也不是绝对的,也就是说这给的是一个参考值,根据多种调优之后得出的一个结论,大家可以根据这个值来设置一下我们的初始化内存,在保证程序正常运行的情况下,我们还要去查看GC的回收率,GC停顿耗时,内存里的实际数据来判断, Full GC是基本上不能有的,如果有就要做内存Dump分析,然后再去做一个合理的内存分配。

我们还注意到一点就是,上面说的老年代存活对象怎么去判定。

3.2、如何计算老年代存活对象

3.2.1、方式1:查看日志

推荐/比较稳妥!

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

3.2.2、方式2:强制触发FullGC

会影响线上服务,慎用!

  • 方式1的方式比较可行,但需要更改JVM参数,并分析日志。同时,在使用CMS回收器的时候,有可能不能触发 FullGC,所以日志中并没有记录 FullGC的日志。在分析的时候就比较难处理。所以,有时候需要强制触发一次 FullGC,来观察 FullGC之后的老年代存活对象大小。

  • 注:强制触发FullGC,会造成线上服务停顿(STW),要谨慎!建议的操作方式为,在强制 FullGC前先把服务节点摘除, FullGC之后再将服务挂回可用节点,对外提供服务,在不同时间段触发 FullGC,根据多次 FullGC之后的老年代内存情况来预估 FullGC之后的老年代存活对象大小

  • 如何强制触发Full GC?

1、jmap -dump:live, format=b,file=heap.bin <pid>将当前的存活对象dump到文件,此时会触发 FullGC

2、jmap -histo:live <pid>打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量.此时会触发FullGC

3、在性能测试环境,可以通过Java监控工具FullGC来触发,比如使用 VisualVM 和Jconsole, VisualVM集成JConsole,VisualVM或JConsole或者上面有一个触发GC的按钮

3.3、案例演示

JVM配置参数

现在我们通过idea启 springboot工程,我们将内存初始化为1024M。我们这里就从1024M的内存开始分析我们的GC日志,根据我们上面的一些知识来进行一个合理的内存设置。

-XX:+PrintGCDetails  -XX:MetaspaceSize=64m -Xss512K -XX:+HeapDumpOnOutMemoryError
-XX:HeapDumpPath=heap/heapdump3.hprof -XX:SurvivorRatio=8 -XX:+PrintGCDateStamps
-Xmx1024M -Xmx1024M -Xloggc:log/gc-oom3.log

代码演示

/**
 * 性能优化案例3:合理配置堆内存
 */
@RequestMapping("/getData")
public List<People> getProduct(){
    List<People> peopleList = peopleSevice.getPeopleList();
    return peopleList;
}

FullGC后用 jmap -heap <pid> 指令可以查看老年代存活大小

3.4、数据分析

按照理论将 -Xmx1024M -Xmx1024M 减小后,YoungGC次数增多,但依然没有发生FullGC,压测吞吐量未发生大幅度变化

3.5、结论

在内存相对紧张的情况下,可以按照上述的方式来进行内存的调优,找到一个在GC频率和GC耗时上都可接受的一个内存设置,可以用较小的内存满足当前的服务需要。

但当内存相对宽裕的时候,可以相对给服务多增加一点内存,可以减少GC的频率,GC的耗时相应会增加一些。一般要求低延时的可以考虑多设置一点内存,对延时要求不高的,可以按照上述方式设置较小内存。

如果在垃圾回收日志中观察到 OutOfMemoryError,尝试把Java堆的大小扩大到物理内存的80%~90%。尤其需要注意的是堆空间导致的 OutOfMemoryError以及一定要增加空间

  • 比如说,增加-Xms和-Xmx的值来解决old代 的OutOfMemoryError
  • 增加-XX: PermSize和-XX: MaxPermSize来解决permanent代引起的OutOfMemoryError(jdk7之前);增加-XX:MetaspaceSize和-XX:MaxMetaspaceSize来解决metaspace引起的OutOfMemoryError(jdk8之后)

记住一点Java堆能够使用的容量受限于硬件以及是否使用64位的JVM。在扩大了Java堆的大小之后,再检查垃圾回收日志,直到没有OutOfMemoryError为止。如果应用运行在稳定状态下没有OutOfMemoryError就可以进入下一步了,计算活动对象的大小。

3.6、你会估算GC频率吗?

正常情况我们应该根据我们的系统来进行一个内存的估算,这个我们可以在测试环境进行测试,最开始可以将内存设置的大一些,比如4G这样,当然这也可以根据业务系统估算来的。

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

youngGC。这样可以让我们对系统有一个大致的估算。

4、特殊问题:新生代与老年代的比例

4.1、参数设置

JVM 参数设置

打印日志详情 打印日志打印日期 初始化内存300M 最大内存300M 日志路径

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms300M -Xmx300M -Xloggc:log/gc.log

新生代(Young) 与 老年代(Old) 的比例为1:2 ,所以,内存分配应该是新生代100M,老年代200M

我们可以先用命令查看一下堆内存分配是怎么样的:

# 查看进程ID jps -l # 查看对应的进程ID的堆内存分配 jmap -heap 3725

结果可以看到:我们的SurvivorRatio=8,但是内存分配却不是8:1:1,这是为什么呢?

4.2、参数AdaptiveSizePolicy

这是因为JDK1.8默认使用 UseParallelGC垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy,会根据GC的情况自动计算计算Eden、From和to区的大小;所以这是由于JDK1.8的自适应大小策略导致的,除此之外,我们下面观察GC日志发现有很 多类似这样的FULLGC( Ergonomicss),也是一样的原因。

我们可以在jvm参数中配置开启和关闭该配置:

#开启
-XX:+UseAdaptiveSizePolicy
#关闭
-XX:-UseAdaptiveSizePolicy

注意事项:

1、在JDK 1.8 中,如果使用CMS,无论 UseAdaptiveSizePolicy如何设置,都会将UseAdaptiveSizePolicy设置为 false;不过不同版本的JDK存在差异;

2、 UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效;

3、由于 UseAdaptiveSizePolicy会动态调整Eden、 Survivor的大小,有些情况存在 Survivor被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉Eden区后,还存活的对象进入 Survivor装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发 FULL GC,如果一次 FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。

4、使用ParallelGC的情况下,不管是否开启了UseAdaptiveSizePolicy参数,默认Eden与Survivor的比例都为:6:1:1

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

如果不想动态调整内存大小,以下是解决方案:

1、保持使用UseParallelGC,显式设置-XX:SurvivorRatio=8 -XX:-UseAdaptiveSizePolicy

2、使用CMS垃圾回收器。CMS默认关闭UseAdaptiveSizePolicy。配置参数 -XX:+UseConcMarkSweepGC

5、性能优化案例4:CPU占用很高排查方案

5.1、案例

/**
 * <pre>
 *    @author  : shkstart
 *    desc    : jstack 死锁案例
 *    version : v1.0
 * </pre>
 */
public class JstackDeadLockDemo {
    /**
     * 必须有两个可以被加锁的对象才能产生死锁,只有一个不会产生死锁问题
     */
    private final Object obj1 = new Object();
    private final Object obj2 = new Object();

    public static void main(String[] args) {
        new JstackDeadLockDemo().testDeadlock();
    }

    private void testDeadlock() {
        Thread t1 = new Thread(() -> calLock_Obj1_First());
        Thread t2 = new Thread(() -> calLock_Obj2_First());
        t1.start();
        t2.start();
    }

    /**
     * 先synchronized  obj1,再synchronized  obj2
     */
    private void calLock_Obj1_First() {
        synchronized (obj1) {
            sleep();
            System.out.println("已经拿到obj1的对象锁,接下来等待obj2的对象锁");
            synchronized (obj2) {
                sleep();
            }
        }
    }

    /**
     * 先synchronized  obj2,再synchronized  obj1
     */
    private void calLock_Obj2_First() {
        synchronized (obj2) {
            sleep();
            System.out.println("已经拿到obj2的对象锁,接下来等待obj1的对象锁");
            synchronized (obj1) {
                sleep();
            }
        }
    }

    /**
     * 为了便于让两个线程分别锁住其中一个对象,
     * 一个线程锁住obj1,然后一直等待obj2,
     * 另一个线程锁住obj2,然后一直等待obj1,
     * 然后就是一直等待,死锁产生
     */
    private void sleep() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5.2、问题呈现

cpu大量占用

5.3、问题分析

1、ps aux |grep java 查看到当前java进程使用cpu,内存,磁盘的情况获取使用量异常的进程

[root@shuidi150 ~]# ps aux |grep java
root       1140  0.2  1.8 3497792 70348 ?       Sl   11月22   2:08 /usr/local/jdk1.8.0_191/bin/java -Dzookeeper.log.dir=. -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/local/zookeeper-3.4.11/bin/../build/classes:/usr/local/zookeeper-3.4.11/bin/../build/lib/*.jar:/usr/local/zookeeper-3.4.11/bin/../lib/slf4j-log4j12-1.6.1.jar:/usr/local/zookeeper-3.4.11/bin/../lib/slf4j-api-1.6.1.jar:/usr/local/zookeeper-3.4.11/bin/../lib/netty-3.10.5.Final.jar:/usr/local/zookeeper-3.4.11/bin/../lib/log4j-1.2.16.jar:/usr/local/zookeeper-3.4.11/bin/../lib/jline-0.9.94.jar:/usr/local/zookeeper-3.4.11/bin/../lib/audience-annotations-0.5.0.jar:/usr/local/zookeeper-3.4.11/bin/../zookeeper-3.4.11.jar:/usr/local/zookeeper-3.4.11/bin/../src/java/lib/*.jar:/usr/local/zookeeper-3.4.11/bin/../conf: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/local/zookeeper-3.4.11/bin/../conf/zoo.cfg
root      36289  0.2  4.8 2670284 186980 pts/0  Sl   11月22   2:52 /usr/local/jdk1.8.0_191/jre/bin/java -Djava.util.logging.config.file=/mydata/apache-tomcat-8.5.73/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Xms120m -XX:SurvivorRatio=8 -Xmx120m -XX:+UseParallelGC -XX:+PrintGCDetails -XX:MetaspaceSize=64m -XX:+PrintGCDateStamps -Xloggc:/mydata/tomcat8.5/logs/gc.log -Dignore.endorsed.dirs= -classpath /mydata/apache-tomcat-8.5.73/bin/bootstrap.jar:/mydata/apache-tomcat-8.5.73/bin/tomcat-juli.jar -Dcatalina.base=/mydata/apache-tomcat-8.5.73 -Dcatalina.home=/mydata/apache-tomcat-8.5.73 -Djava.io.tmpdir=/mydata/apache-tomcat-8.5.73/temp org.apache.catalina.startup.Bootstrap start
root      45239  0.4  0.7 3331040 28184 pts/0   Sl+  11:26   0:00 java JstackDeadLockDemo
root      46972  0.0  0.0 112828   980 pts/2    S+   11:26   0:00 grep --color=auto java

找到当前的进程id是 45239

2、top -Hp 进程id 检查当前使用异常线程的pid

top -Hp 45239

可以看到 线程id 45250 占用cpu比较高

3、把线程pid变为16进制如 45250->b0c2 ,然后得到 0xb0c2

4、jstack 进程的pid |grep -A20 0xb0c2 得到相关进程的代码 (-A 表示20行后)

生产环境可以使用 李鼎 开源的show-busy-java-threads 脚本

5.4、解决方案

(1)调整锁的顺序,保持一致

(2)或者采用定时锁,一段时间后,如果还不能获取到锁就释放自身持有的所有锁。

6、性能优化案例5:G1并发执行的线程数对性能的影响

6.1、配置信息

硬件配置:4核linux

JVM参数设置

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"
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:/mydata/tomcat8.5/logs/gc.log"
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=1"

说明:最后一个参数可以在使用G1GC测试初始并发GCThreads之后再加上

初始化内存和最大内存调整小一些,目的发生FullGC,关注GC时间

关注点是:GC次数,GC时间,以及Jmeter的平均响应时间

6.2、初始的状态

GC时间/YoungGC次数

(10556-8427)/(3601-2836)=2129/765=2.783

6.3、优化之后

export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=2"

GC时间/YoungGC次数

(2485-1328)/(783-391)=1157/392=2.95

6.4、总结

吞吐量由原先的 6717 提升到 7609

7、性能优化案例6:调整垃圾回收器提高服务的吞吐量

7.1、初始配置

系统配置是单核,我们看到日志,显示 DefNew,说明我们用的是串行收集器, SerialGC

7.2、优化配置1

使用ParallelGC

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"
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:/mydata/tomcat8.5/logs/gc6.log"

查看日志

由于服务器依旧是单核,吞吐量并没有发生改变

7.3、优化配置2

将服务器修改为4核

查看日志

吞吐量大幅度提升,甚至翻了一倍,这说明我们在多核机器上面采用并行收集器对于系统的吞吐量有一个显著的效果。

7.4、优化配置3

修改为G1收集器

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"
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:/mydata/tomcat8.5/logs/gc6.log"

查看日志

吞吐量明显提升

8、性能优化案例7:日均百万级订单交易系统如何设置JVM参数

一天百万级订单这个绝对是现在顶尖电商公司交易量级,百万订单一般在4个小时左右产生,我们计算一下每秒产生多少订单,

3000000/3600/4=208.3单/s,我们大概按照每秒300单来计算。

问如何进行服务器配置只是第一个层面的问题!

第二个层面问题:如果要求响应时间控制在100ms如何实现?

压测,调优!

9、面试小结

9.1、part1

12306遭遇春节大规模抢票如何支撑?

12306号称是国内并发量最大的秒杀网站,并发量达到百万级别。

普通电商订单-->下单-->订单系统(IO)减库存>等待用户付款

12306一种可能的模型:下单-->减库存和订单(redis、 kafka)同时异步进行-->等付款

但减库存最后还会把压力压到一台服务器上。如何?

分布式本地库存+单独服务器做库存均衡!

9.2、part2

问题一:有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器是32位的,1.5G的堆,用户反馈网站比较缓慢。因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了!

1.为什么原网站慢?

频繁的GC,STW时间比较长,响应时间慢!

2.为什么会更卡顿?

内存空间越大,FullGC时间更长,延迟时间更长

3.咋办?

>垃圾回收器:parallel GC ; ParNew+CMS; G1

>配置GC参数:-XX:MaxGCPauseMillis 、-XX:ConcGCThreads

>根据log日志、dump文件分析,优化内存空间比例

jstat jinfo jstack jmap

问题二:系统CPU经常100%,如何调优?(面试高频)

CPU100%的话,一定是有线程占用系统资源。具体步骤前面已经讲过。

注意:工作中有时候是工作线程100%占用了CPU,还有可能是垃圾回收线程占用了100%

问题三:系统内存飙高,如何查找问题?(面试高频)

一方面:jmap -heap;jstat;gc日志情况

另一方面:dump文件分析

问题四:如何监控JVM

>命令行工具

>图形化界面

猜你喜欢

转载自blog.csdn.net/dyangel2013/article/details/121533943