美文网首页
G1垃圾回收器实战

G1垃圾回收器实战

作者: 后来丶_a24d | 来源:发表于2020-09-24 14:25 被阅读0次

    目录

    目录.png

    参数配置


    堆内内存问题

    六种OOM

    1. java.lang.StackOverFlowError 虚拟机栈
    2. java.lang.OutOfMemoryError:Java heap space 堆
    3. java.langOutOfMemoryError:Metaspace 元数据空间
    4. java.lang.OutOfMemoryError:Direct buffer memory 堆外
    5. java.lang.OutOfMemoryError:GC overhead limit exceeded GC回收时间长时会抛出OutOfMemoryError
    6. java.lang.OutOfMemoryError:unable to create new native thread linux系统默认允许单个进程可以创建的线程数是1024个

    堆内内存,但无oom文件

    • 在grafana上面看到堆内内存上升
    • 拉取一台机器,并且生成dump文件,然后使用MAT分析。
    • MAT简介


      MAT简介.png
      MAT_dump文件分析.png
    • 在thread_override中可以看到push_data_thread有30个,占用了大部分内存


      image.png
    • 从dominator_tree中可以看出是PriceChangePullPushService中push_data_thread中HttpClient的BufferedWritter写入过多String


      image.png
    • 代码定位,这块代码只是用来测试模拟,堆内内存溢出
    jvm配置: -Xms350m -Xmx350m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=200m
    
    代码定位.png
    参考文章

    堆内内存有oom

    • 线上邮件收到报OOM,因为配置OOM时生成dump文件
    • 用MAT分析, 模拟100M内存,这里看占用了绝大多数


      image.png
    • 定位代码
    jvm配置: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ -XX:+PrintGCDetails -Xloggc:./gc.log
    
    public class TestOOM implements Runnable {
        private List<String> list = new ArrayList<>();
        @Override
        public void run() {
            int i = 0;
            ++i;
            list.add(String.valueOf(i));
        }
    
        public void test(){
            //System.out.println("I am testoom");
        }
    }
    
    public class Test {
        private final static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8, 8,
                30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), r -> new Thread("test oom thread"));
    
        public static void main(String[] args) {
            while (true) {
                threadPoolExecutor.execute(new TestOOM());
            }
    
        }
    }
    

    Humous大对象分配测试

    • MAT中能看到超过1M的大对象
    public class TestHumous {
        public static void main(String[] args) {
            TestHumous testHumous = new TestHumous();
            testHumous.test();
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private void test(){
            int size = 1024 * 1024;
    
            byte[] myAlloc1 = new byte[ size];
            byte[] myAlloc2 = new byte[ size];
            byte[] myAlloc3 = new byte[ size];
            byte[] myAlloc4 = new byte[ size];
    
            System.out.println("MyTest.main");
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    堆外内存

    代码模拟堆外内存溢出

    jvm option: -XX:NativeMemoryTracking=detail -XX:+UseG1GC 
    
    // 开始先给点时间设置基准线
    try {
        Thread.sleep(120000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("开始测试");
    ByteBuffer buffer = ByteBuffer.allocateDirect(50 * 1024 * 1024);
    try {
        Thread.sleep(300000000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    

    排查思路

    • 先确定下是否堆问题。如果是堆问题,再确定下堆内内存有问题,不是堆内内存大概率就是堆外
    • NMT是Java7U40引入的HotSpot新特性,配合jcmd命令我们就可以看到具体内存组成了。jvm参数需开启-XX:NativeMemoryTracking=summary,会略微耗性能。
    • 排查命令
    // 先看下出问题pid,测试的话是可以直接通过jps -l看到的, 线上top看看
    jps -l 
    // 先设置个基准线
    jcmd 96328 VM.native_memory baseline
    // 过段时间看下diff
    jcmd 96328 VM.native_memory summary.diff
    
    • native_memory日志
    Native Memory Tracking:
    
    Total: reserved=10116110KB +51219KB, committed=927226KB +51219KB
    
    Java Heap (reserved=8388608KB, committed=524288KB)
                (mmap: reserved=8388608KB, committed=524288KB)
    
        Class (reserved=1068164KB, committed=16260KB)
                (classes #460 +1)
                (malloc=11396KB #236 +5)
                (mmap: reserved=1056768KB, committed=4864KB)
        Thread (reserved=25706KB, committed=25706KB)
                (thread #26)
                (stack: reserved=25600KB, committed=25600KB)
                (malloc=76KB #137)
                (arena=30KB #47)
    
         Code (reserved=249636KB, committed=2572KB)
                (malloc=36KB #323 +1)
                (mmap: reserved=249600KB, committed=2536KB)
    
            GC (reserved=319169KB, committed=293573KB)
                (malloc=12689KB #137)
                (mmap: reserved=306480KB, committed=280884KB)
    
      Compiler (reserved=133KB, committed=133KB)
                (malloc=2KB #25)
                (arena=131KB #7)
     // 可以看到这里加了很多,也就是代码里面的堆外内存分配
     Internal (reserved=62863KB +51201KB, committed=62863KB +51201KB)
                (malloc=62831KB +51201KB #3597 +6)
                (mmap: reserved=32KB, committed=32KB)
    
        Symbol (reserved=1441KB, committed=1441KB)
                (malloc=953KB #564)
                (arena=488KB #1)
    
    Native Memory Tracking (reserved=215KB +18KB, committed=215KB +18KB)
                (malloc=111KB +14KB #1565 +197)
                (tracking overhead=104KB +3KB)
    
    Arena Chunk (reserved=175KB, committed=175KB)
                (malloc=175KB)
    //  原生内存 + 堆保留内存 = jvm 进程使用内存
    
    • Java Heap部分表示heap内存目前占用了7372MB;Class部分表示已经加载的classes个数为460,占用了16260KB包含metadata;Thread部分表示目前有26个
      线程,占用了25706KB;Code部分表示JIT生成的或者缓存的instructions占用了2572KB;GC部分表示目前已经占用了293573KB的内存空间用于帮助GC;
      Internal部分表示命令行解析、JVMTI(JVM Tool Interface由Java虚拟机提供的native编程接口)等占用了62863KB;Symbol部分表示诸如String table及constant pool等symbol占用了1441KB;Native Memory Tracking部分表示该功能自身占用了215KB
    • DirectByteBuffer分配内存的话,是需要full GC或者手动system.gc来进行回收的(所以最好不要使用-XX:+DisableExplicitGC)。 之前项目是CMS回收器时分析过,-XX:+DisableExplicitGC导致堆外内存很高。这里链接里面是先用上述分析,方法再jstack 抓,才解决: CMS堆外内存泄露案例1

    拓展

    • 除堆内内存和方法区,这些地方也要注意
      1.Direct Memory:可以通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError:Direct buffer memory。注意有时候堆外内存太小也会有问题。
      2.线程堆栈:可通过-Xss调整大小内存不足时抛出StackoverflowErroe(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。
      3.Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB的内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。
      4.JNI代码:如果代码中使用JNI调用本地库,那么本地库使用内存也不在堆中
      5.虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。

    参考文章


    metaspace引发full gc

    代码模拟metaspae溢出

    jvm option:  -XX:+UseG1GC -Xms1000M -Xmx1000M -XX:+UseG1GC -XX:+PrintGCDetails 
    -XX:+HeapDumpOnOutOfMemoryError 
    -XX:HeapDumpPath=./ -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=200m
    // 注意这个,如果改成非0比如7000这种数值,就不会有以下说的大量加载卸载恩恩现象
    -XX:SoftRefLRUPolicyMSPerMB=0 
    -XX:MaxMetaspaceSize=10m -XX:+TraceClassLoading -XX:+TraceClassUnloading
    
    public class TestMetaSpaceOverFlow {
        static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3000, 3000, 30, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
        public static void main(String[] args) throws Exception {
            Thread.sleep(1000);
            System.out.println("+++++++++");
            // 模拟日常gc
            new Thread(() -> {
                while (true) {
                    System.gc();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.gc();
                }
            }).start();
            // 如果反射用到了软引用则因为并发问题会生成及少量的GeneratedMethodAccessor***, 但是如果
            // 不用软引用,则会有可能生成超大量的GeneratedMethodAccessor**对应很多个类加载器DelegatingClassLoader
            // 有可能造成方法区内存溢出
            for(int i = 0; i < 2000000; i++) {
                threadPoolExecutor.execute(() -> {
                    try {
                        Class<?> clazz1 = Class.forName("TestOOM");
                        TestOOM testOOM1 = (TestOOM) clazz1.getDeclaredConstructor().newInstance();
                        Method method1 = clazz1.getDeclaredMethod("test");
                        method1.invoke(testOOM1);
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
            Thread.sleep(1000000000);
        }
    }
    

    高并发场景下重现

    用的jdk1.8_201和G1回收器。Metaspace当时调的是256Mb, 邮件收到OOM: Error Metaspace。遇到这个问题后先把Metaspace调整成500Mb,但是还有上升趋势。加了-XX:+TraceClassLoading -XX:+TraceClassUnloading后发现catalina.out文件有很多反射类加载卸载,公司RPC大量用到反射。查看jvm参数发现-XX:SoftRefLRUPolicyMSPerMB=0(不知道谁设置的,得问问) ,这个是罪魁祸首,后面改成-XX:SoftRefLRUPolicyMSPerMB=1000。metaspace就正常了,也没这么多类加载卸载了。

    [Loaded sun.reflect.GeneratedMethodAccessor16 from __JVM_DefineClass__]
    [UnLoaded sun.reflect.GeneratedMethodAccessor16 from __JVM_DefineClass__]
    

    拓展

    反射
    • Metaspace内存空间如何泄漏: Metaspace空间泄露,其实就是ClassLoader内存空间泄露。
      Metaspace由: klass, 是我们熟知的class文件在jvm里的运行时数据结构; NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的; Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块;
    • 动态代理类生成和反射调用,这些都会导致ClassLoader空间出现泄露。在反射调用重灾区的项目中可能会存在很多的DelegatingClassLoader对象没有及时回收,对应加载的类也没 有及时卸载,当并发量大的时候可能导致Metaspace空间不足从而引发FullGC。
    • 反射原理: 反射原理
      // 反射用到了软引用则因为并发问题会生成及少量的GeneratedMethodAccessor***, 但是如果
      // 不用软引用,则会有可能生成超大量的GeneratedMethodAccessor**对应很多个类加载器DelegatingClassLoader
     // 有可能造成方法区内存溢出
     Class<?> clazz1 = Class.forName("TestOOM");
     TestOOM testOOM1 = (TestOOM) clazz1.getDeclaredConstructor().newInstance();
      Method method1 = clazz1.getDeclaredMethod("test");
      method1.invoke(testOOM1);
    
    软引用
    • 软引用回收时机会:
    clock - timestamp > free_heap*ms_per_mb 则被回收
    clock: 最后一次GC时间
    timestamp: 使用软引用后最后一次GC时间
    free: 空间内存/mb
    ms_per_mb: -XX:SoftRefLRUPolicyMSPerMB的值
    
    • 测试软引用, 如果测试软引用不过期则改变XX:SoftRefLRUPolicyMSPerMB值即可
    jvm option: -Xmx20M -XX:+PrintGCDetails -verbose:gc -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+UseG1GC -XX:MaxMetaspaceSize=10m
    private static final int _4MB= 30*512*1024;
    public static void main(String[] args) throws IOException, InterruptedException {
        soft();
    }
    public static void soft() throws InterruptedException {
        SoftReference<byte[]> reference = new SoftReference<>(new byte[_4MB]);
        System.out.println("=======" + reference.get());
        // 这次gc更新了timestamp的值
        System.gc();
        Thread.sleep(3000);
        // 这次gc更新了clock的值
        System.gc();
        Thread.sleep(3000);
        // clock - timestamp 的值 > freeSpace * SoftRefLRUPolicyMSPerMB=0, 所以被回收了
        // 改变SoftRefLRUPolicyMSPerMB的值就有可能不变回收左边表达式小于右边就不回收
        System.out.println("+++++++++" + reference.get());
    }
    

    参考文献


    老年代不足引发fullgc


    cpu问题

    • top查看资源占用pid
    • top -H -p pid 定位到线程


      image.png
    • jstack <进程号> | grep <tid 16进制(上图中的pid 比如14293的16进制)>
    • 结合第三点第二点,查到对应堆栈,找到对应代码
      ps arthas也可以用作排查工具

    参考文章


    磁盘问题


    参考文章

    相关文章

      网友评论

          本文标题:G1垃圾回收器实战

          本文链接:https://www.haomeiwen.com/subject/dqjvyktx.html