java虚拟机13

JVM性能调优

JVM调优分类

1.JVM预调优
  • 根据业务场景预估,预估需要使用的服务器资源容量、JVM堆内存的配置
2.优化JVM运行环境(慢、卡顿等)
2.优化JVM运行环境(慢、卡顿等)
  • 常见的手段就是重启,但是不可取
  • 这种问题是不明显的,像慢和卡是需要有数据指标的
3.解决JVM中的问题(OOM等)
  • 内存溢出是最明显的问题

调优规划及怎么进行预调优

调优规划

①业务场景设定
  • 场景1:注重吞吐量,让系统的tps更高,parallel scavenge的吞吐量是最大的,单一时间做单一事
  • 场景2:注重用户的使用体验,让用户的响应时间尽可能短,也就是要减少stop the world的时间,而parallel scavenge就是不行的,因为这种垃圾回收器不是并发处理的,当正好进行垃圾回收时,用户线程是阻塞的,所以用cms和g1更好
②无监控不优化
  • 监控就是压力测试,ab或者jmeter,就是需要有监控数据的结果,这种量化的指标
    • 吞吐量
    • 响应时间
    • 服务器资源
    • 网络资源
③处理步骤
  • 1.计算内存需求
    • 内存不一定越大越好,内存小gc频繁,但是回收速度比较快
    • 资源如果比较紧缺,虚拟机栈默认1M,把它改小改成256K
    • jdk1.8的元空间默认大小是没有限制的,但是服务器一旦跑起来,元空间大小就是固定的,比方设定为1G, 假如一个8g的机器,设置堆的初始大小是1g,堆最大是4g,会发现机器在运行的过程中,虚拟机栈+元空间的大小合起来可能超过了6个g,堆的空间达不到4个g
    • 具体的这些堆设置参数都是可以通过跑压测测试出来的
  • 2.选定CPU
    • CPU没办法算,理论上性能越高越好,性能比内存影响更大,但是成本更高,理论上根据预算来算
    • 对于云服务器,不能光看虚拟机化后的参数指标,更应该看物理机的实际情况
      • 8台虚拟机,先给4台,再给4台,业务量有千万,尤其是人脸识别,刚开始的4台压测是没问题的,后面4台的性能指标是一样的,但是把8台去做负载均衡,就有反馈说卡,就很矛盾,单机测试后4台发现是有问题的,性能只有前面4台性能的四分之一,最后怎么解决?找到机房,后面4台是旧机器虚拟化产生的,并且挂了其他的项目,并且并发比较高
  • 3.选择合适垃圾回收器
    • 对于吞吐量优先的场景,就只有一种选择,就是使用 PS 组合(Parallel Scavenge+Parallel Old )
    • 对于响应时间优先的场景,在 JDK1.8 的话优先 G1,其次是 CMS 垃圾回收器。
  • 4.设定新生代大小、分代年龄
    • 在吞吐量优先的应用,设置很大的新生代,比较小的老年代,因为默认情况下新生代只占整个堆空间的三分之一,而在吞吐量优先的项目中,可以设置到三分之二,甚至更大,这样做的目的是让绝大多数对象在新生代回收,而新生代采用复制算法,对于朝生夕死的对象回收效率是比较高的,而老年代是采用标记清除和标记整理,这种算法效率相比复制算法效率要低
  • 5.设定日志参数
    • -XX:+PrintGC 输出 GC 日志
    • -XX:+PrintGCDetails 输出 GC 的详细日志
    • -XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
    • -XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
    • -XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
    • -Xloggc:…/logs/gc.log 日志文件的输出路径
    • 注意:一般记录日志的是,如果只有一个日志文件肯定不行,有时候一个高并发项目一天产生的日志文件就上 T,其实记录日志这个事情,应该是运维干的事情。日志文件帮助我们分析问题。

亿级流量电商系统JVM调优

亿级流量系统

  • 1.每日点击在亿级
  • 2.每个用户一次浏览点击在20~40之间
  • 3.日活用户在500万左右
  • 4.其中90%的用户仅浏览,用图片缓存、redis等等手段解决
  • 5.还有10%是付费用户,日均成交50万单
  • 6.集中在三四个小时,每秒几十单
    • 一般不需要预估调优
  • 7.大促集中在几分钟,每秒2000单
    • 例如秒杀场景,通过负载均衡,假设用4台机器,每台机器大概是每秒500单
    • 测试发现,每个订单处理过程中会占据0.2MB大小的空间
    • 每秒会产生100M内存空间,1秒后都会变成垃圾对象
    • 假设服务器配置3G,java -Xmx3072M -jar order.jar,同时老年代占2个g,新生代占1个g,其中eden:from:to=8:1:1
    • 那么每8秒就可以填满eden区,此时就会触发minor gc,如果采用ps垃圾回收器,用户线程会停止,此时有些对象被创建出来,但是还不一定可回收,这些对象还在eden区,假设是100M,这100M不会被回收,假设进入s0区,s区会进行动态年龄判断,如果相同年龄所有对象的大小超过s区的一半,即使age=1,也会直接晋级到old区,所以这100M会直接进入old区,所以每隔8秒就会有100M的对象进入老年代,所以大概160秒以后,老年代就会满了,此时会触发fullgc,回收时间就比较长了,但是这些对象本来应该是朝生夕死的,而fullgc要回收新生代、老年代和元空间,所以怎么避免不必要的fullgc?
      • -Xms3072M
      • -Xmx3072M
      • -Xmn2048M
      • -XX:SurvivorRatio=7
      • -Xss256K
      • -XX:MetaspaceSize=128M
      • -XX:MaxMetaspaceSize= 128M
      • -XX:MaxTenuringThreshold=2
      • -XX:ParallelGCThreads=8
      • -XX:+UseConcMarkSweepGC
    • 1.老年代只有1个g了,新生代是2个g,eden:from:to=7:1.5:1.5,所以eden区是1.4个g,from区和to区都是300M,
    • 2.此时,需要14秒才能填满eden区,minorgc的频次就降低了,还是有100M的对象不能回收,此时进入到s0区,下次再minorgc的时候,上一个100M被回收了,s1区存放100M,这样就不会发生fullgc了,这种计算只能在微服务中这样计算,所以是通过避免不必要的fullgc的发生
    • 3.把虚拟机栈的默认大小改成256k,虚拟机栈中存放的是局部变量表,八大基础数据,对应的引用,只要程序不要一个方法写成几千行,写成256k是够用的
    • 4.同时减少元空间的大小,因为程序跑起来后,元空间是固定的,所以把元空间设小一点,设成128M
    • 5.设置分代年龄是2,对象如果没有被回收,分代年龄是1,每经过一次minorgc,当年龄到达15后,进入老年代,但是我们一般都是使用spring架构,spring里面有一些长期存活的bean,这些bean对象最好让它尽快进入老年代,没必要在s0和s1之间晃来晃去,反正死不了
    • 6.设置并行垃圾回收器的并发线程数是8,默认是JVM可用的cpu数量,所以这个参数应该根据机器的配置情况来定,如果机器性能好,并且是新机器,但是jvm虚拟化的cpu数量少,可以适当改大一点,一般是cpu核心整数倍,不要加一减一,如果是核心数是8,可以是4,或者8,或者16
    • 7.设置垃圾回收器是CMS或者g1,为什么要改呢?如果业务能做到老年代不回收,是可以不用改的,如果做不到不回收老年代,为了满足延时要求,也就是不卡顿,所以要更换,如果内存超过6g,可以换成g1

GC预估

  • 见上一步的第6和第7点

GC预估调优

  • 详细见上一步的第7点

问题排查实战

项目介绍

  • 从数据库取数据,并计算用户的征信(银行业务)

  • package ex13;
    
    import java.math.BigDecimal;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * VM参数: -XX:+PrintGC -Xms200M -Xmx200M
     *  GC调优---生产服务器推荐开启(默认是关闭的)
     *  -XX:+HeapDumpOnOutOfMemoryError
     */
    
    public class FullGCProblem {
        //线程池
        private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
                new ThreadPoolExecutor.DiscardOldestPolicy());
    
        public static void main(String[] args) throws Exception {
            //启动50个线程来处理计算
            executor.setMaximumPoolSize(50);
            while (true){
                calc();
                Thread.sleep(100);
            }
        }
        //多线程执行任务计算
        private static void calc(){
            List<UserInfo> taskList = getAllCardInfo();
            taskList.forEach(userInfo -> {
                executor.scheduleWithFixedDelay(() -> {
                    userInfo.user();
                }, 2, 3, TimeUnit.SECONDS);
            });
        }
        //模拟从数据库读取数据,返回
        // 假设取100个,而不是取一个计算一个
        private static List<UserInfo> getAllCardInfo(){
            List<UserInfo> taskList = new ArrayList<>();
            for (int i = 0; i < 100; i++) {
                UserInfo userInfo = new UserInfo();
                taskList.add(userInfo);
            }
            return taskList;
        }
        private static class UserInfo {
            String name = "king";
            int age = 18;
            BigDecimal money = new BigDecimal(999999.99);
    
            public void user() {
                //
            }
        }
    }
    
    

代码介绍

  • 见上一步

  • 这段代码不会立马oom,也不会立马频繁gc,但是会慢慢增长这种问题是最难排查的

    • 服务刚起来的时候cpu和memory都只有5%左右,利用jmap查看堆空间,发现在默认情况下eden:from:to基本是1:1:1的关系,这是因为-XX:+UseAdaptiveSizePolicy这个设置是默认开启的,它会自动调整survivor区和eden区的比例

    • 过了10分钟后,发现有一个进程的%cpu超过100%了,然后输入top -p 具体cpu异常的进程号,就只显示这一条进程了,怎么看线程呢?输入一个H,可以看到cpu占用率最高的线程

CPU占用过高排查实战

CPU占用过高排查思路

  • 1.Top命令分析进程
    • top命令是linux常见命令之一,实时显示正在进行的进程信息,pid是进程id,每一条信息显示的是对应进程的cpu信息和memory信息,这是下半部分的一个列表,而上半部分会显示一个统计信息,cpu,memory,交换区等等
    • 首先找出cpu占用最高的进程号
  • 2.定位CPU占用高的进程
    • top -p cpu异常进程的进程号
  • 3.在第二步的界面中,输入H,获取当前进程下的所有线程信息
  • 4.从第三步的线程信息中找到消耗cpu特别高的线程编号,并记录
  • 5.用jstack输出线程信息
    • jstack 具体的进程号
    • 这里会有一大堆信息,其中nid就是线程id
  • 6.将第四步得到的线程编号转成16进制
  • 7.解读线程信息,定位具体代码
    • 发现异常的线程是gc线程,程序在进程疯狂的垃圾回收,cpu100%是由于垃圾回收造成的
    • 如果没有开启gc日志,可以通过jinfo动态开启gc打印,或者通过jstat打印gc信息
    • 发现没有进行minorgc,而是在频繁进行fullgc
    • 而且进行fullgc后,使用率一直超过99%,所以肯定是发生了内存泄漏,我们认为对象都是朝生夕死的,干完活就死了,jvm中是根据gcroots来判断的,gcroots是长期持有的,业务代码跑完了,我们认为没用了,但是jvm根据可达性分析认为还是可用的,长期占用我们的内存空间,不断变大,占满后,就会进行fullgc,每次回收一点点,然后开始疯狂的fullgc
    • 因为cpu问题是内存问题相关的,所以进入下一步内存过高的排查

CPU100%问题的原因可能有哪些

  • 1.gc100%,在频繁gc
  • 2.业务线程100%,有很多死循环,比如HashMap,多线程情况下发送死循环也有可能100%

内存过高排查实战

内存占用过高排查思路

  • jmap分析

  • jmap -histo 进程号 | head 20

    • 找出该进程中对象大小排名前20的

    • 可以看到最大的是ScheduledFutureTask、BigInteger、BigDecimal、[I、UserInfo、RunnableAdapter、Lambda都有60万个实例,占用的空间也很大

    • 为什么会有这么多实例?

      线程池是50个,但是每次有100个任务要执行,因此有50个要进入阻塞队列排队,并且这个队列是无界队列,对象创建出来一直在里面,并且是futuretask的异步任务,jvm认为这些任务不是垃圾,慢慢的累计,就会把堆空间占满,其实中间已经抛出来oom了,程序已经死了,但是还占据着cpu,业务根本访问不上来,也就是为什么阿里要求线程池必须要有名字,方便定位

常见原因分析

  • 超大对象
    • 超大对象一般直接进入old区,如果还有个引用,回收不了,超大对象会占用内存,会导致gc频繁,甚至oom,比如从数据库批量查询数据,有时候没写限制,如果查询回来的数量很多
  • 超出预期访问量
    • 比如上面cpu占用过高中的后台任务,也有可能是前台的,系统要应付高峰请求,需要两个g内存来存放对象,但是空间不够,就会导致JVM回收
  • 过多使用Finalizer
    • 如果使用了finalizer,但是不会马上使用,优先级比较低,会导致累积
  • 内存泄漏
    • 大量的引用没有释放掉,占着空间,但是又没什么用
  • 代码问题
内存泄漏的场景
长生命周期的对象持有短生命周期对象的引用
  • 例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
  • 如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
变量作用域不合理
  • 例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为 null
    • 下面的案例2
内部类持有外部类
  • Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命
    周期,程序很容易就产生内存泄漏
  • 如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导
    致垃圾回收器不能正常工作)
  • 解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部
    类是否被回收;
Hash 值改变
  • 在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露(有代码案例 Node 类)
内存泄漏的案例
案例1
  • package ex13.leak;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class HashMapLeakDemo {
        public static class Key {
            String title;
    
            public Key(String title) {
                this.title = title;
            }
        }
    
        public static void main(String[] args) {
            Map<Key, Integer> map = new HashMap<>();
    
            map.put(new Key("1"), 1);
            map.put(new Key("2"), 2);
            map.put(new Key("3"), 2);
    
            Integer integer = map.get(new Key("2"));
            System.out.println(integer);
        }
    }
    
  • key是个对象,每次new出来不同,根本获取不到

案例2
  • package ex13.leak;
    
    /**
     * @author King老师
     * 手写一个栈
     */
    public class Stack {
    
        public Object[] elements;//数组来保存
        private int size = 0;
        private static final int Cap = 200000;
    
        public Stack() {
            elements = new Object[Cap];
        }
    
        public void push(Object e) { //入栈
            elements[size] = e;
            size++;
        }
    
        public Object pop() {  //出栈
            size = size - 1;
            Object o = elements[size];
            elements[size] = null; //不用---引用干掉,GC可以正常回收次对象
            return o;
        }
    }
    
    
  • 手写一个stack,用数组实现,初始20万大小

  • 注意,出栈哪里一定要把那个位置置为null,如果不置为null,引用还是存在的,gc就回收不了,像arraylist中remove的方法也要把相应位置置为null

案例3
  • package ex13.leak;
    
    /**
     * @author King老师
     * 内存泄漏--案例
     */
    public class UseStack {
        static Stack stack = new Stack();  //new一个栈
    
        public static void main(String[] args) throws Exception {
    
            for (int i = 0; i < 100000; i++) {//10万的数据入栈
                stack.push(new String[1 * 1000]); //入栈
            }
            for (int i = 0; i < 100000; i++) {//10万的数据出栈
                Object o1 = stack.pop(); //出栈
            }
            Thread.sleep(Integer.MAX_VALUE);
    
        }
    
    
    }
    
    
判断内存泄漏的逻辑
  • 1.程序优化,效果通常非常大;
  • 2.扩容,如果金钱的成本比较小,不要和自己过不去;
  • 3.参数调优,在成本、吞吐量、延迟之间找一个平衡点
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值