zoukankan      html  css  js  c++  java
  • JVM问题典型案例定位学习

    下面这4个案例来自大神“你假笨”(任职阿里期间,花名:寒泉子)在qcon上的分享,记录一下:

    一、类加载死锁

    现象:jstack将线程dump出来后,找不到deadlock字样的死锁信息,但是有大量的线程在调用Class.forName加载类

        @CallerSensitive
        public static Class<?> forName(String className)
                    throws ClassNotFoundException {
            Class<?> caller = Reflection.getCallerClass();
            return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
        }
    
        private static native Class<?> forName0(String name, boolean initialize,
                                                ClassLoader loader,
                                                Class<?> caller)
            throws ClassNotFoundException;
    

    可以看到forName0是一个native方法,分析该方法的C++源码实现,可以发现使用了锁(细节略)

    tips: jstack -m pid (可以看到native的详细输出信息,但不推荐生产上用,极端情况会让应用不稳定)

    类加载在底层要加锁的原因也不难理解 ,如上图,如果三个线程并发加载类C,如果没有锁,最后可能会把类的元数据信息,在perm区(JDK8以前的版本,JDK8后取消了Perm区)中存多份,很容易造成内存泄露,所以需要加锁,加锁后变成下面这样:

    这个并发加载的情况,从JDK7开始就做了优化,支持并发类加载,但是要使用该功能,必须注册成并行类加载器,否则仍然存在死锁可能。

    参考文章:

    https://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html

    https://www.cnblogs.com/cz123/p/6918708.html

    https://www.jianshu.com/p/8e8a5a773648

    解决方法:

    既然多线程并发加载可能出问题,那么就放在单线程里加载,可参考下面的示例,假设有2个类:Parent及Child

    package com.cnblogs.yjmyzz.test;
    
    public class Parent {
        static {
            System.out.println("Parent init.");
        }
        public static final Parent EMPTY = new Child();
        public static void test() {
            System.out.println("test called in class Parent.");
        }
    }
    

    package com.cnblogs.yjmyzz.test;
    
    public class Child extends Parent {
        static {
            System.out.println("Child init.");
        }
    }
    

    如果用2个线程并发加载:

        public static void main(String[] args) {
            new Thread(() -> new Child(), "T-1").start();
            new Thread(() -> Parent.test(), "T-2").start();
        }
    

    T-1线程中,new Child()时,要先初始化父类Parent,需要加载类Parent,而T-2线程中调用Parent时,其static成员EMPTY又会尝试加载子类Child. 上述这段代码,如果试着运行几次,就有很大概率会遇到死锁:

    会一直卡在这里。可以显式在主线程最开始用forName加载这2个类,这样类加载就变成在main线程中串行加载,问题得到解决:

        public static void main(String[] args) throws ClassNotFoundException {
            Class.forName("com.cnblogs.yjmyzz.test.Parent");
            Class.forName("com.cnblogs.yjmyzz.test.Child");
            new Thread(() -> new Child(), "T-1").start();
            new Thread(() -> Parent.test(), "T-2").start();
        }
    

      

    二、FinalReference堆积 

    现象:用jmap命令分析查看占用内存最多的对象时, 发现java.lang.ref.Finalizer实例排在最前面。

    原因:

    Object类有一个finalize方法,类似析构器,开发人员可以重载这个方法,用于清理资源。大多数情况下,java并不推荐重载该方法,因为jvm的GC已经把垃圾回收做得很好了。

    但如果有某种原因,开发人员确实需要重载该方法:

        @Override
        protected void finalize() throws Throwable {
            //开发人员自定义的清理逻辑
        }
    

    即:这里有些自定义的清理逻辑。这种重载了finalize方法,且实现代码非空的类,在类加载时会被特殊标识,当实例创建时,被包装成FinalReference,放入一个队列里,当GC发生时,如果该实例被标识为垃圾对象,GC清理完后,会用一个额外的线程(重点:这是1个独立的单线程),从队列里一个个取出来,调用重载的finalize方法,如果这种对象在JVM中有大量实例,而且finalize里的清理逻辑,耗时又比较久的话,单线程忙不过来,只能等到下1个GC周期,才会继续清理,因此造成堆积。

    建议:不用使用重载finalize的方式来清理资源。

    三、堆外内存不释放

    先回顾下堆外内存的分布,对于DirectByteBuffer之类的对象,JVM堆上只存放了其"引用",如下图,引用指向的实际内存块在JVM堆外(即:实际分配的堆外内存不受GC管控)

    GC能管理的只是堆上的"引用"数据,但是这块数据通常又非常小,就算经过GC不停折腾,从年青代晋升到老年代,只要老年代的空间还够,就会一直存活,因此其指向的堆外内存也不会释放。除非发生Full GC,把"引用"数据给干掉了,其指向的堆外内存,才会被释放。

    建议:使用-XX:MaxDirectMemorySize参数,限制堆外内存大小。

    四、YGC时间不断拉长

    现象:随着系统持续运行,单次YGC的时间越来越长。

    可能原因:大量调用了String.intern方法,导致字符串的常量池越来越大,而每次YGC都要先mark标记,字符串常量池越大,需要扫描mark的对象也越多,时间就变长了。

    排查方法:jmap -histo:live pid 强制触发一次Full GC,这会强制清理字符串常量池StringTable中无效的对象,如果YGC时间恢复,说明大概率就是这个原因。

  • 相关阅读:
    20201029模拟赛总结
    20201023模拟赛总结
    贪心题目选讲
    博客阅览帮助
    NTT&FFT(快速?变换,以及扩展)
    数论知识小结 [基础篇]
    数论知识小结 [微提高篇]
    零化多项式/特征多项式/最小多项式/常系数线性齐次递推
    牛顿迭代快速求解定义域为多项式的函数零点
    求导/泰勒展开
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/jvm-problem-troubleshooting.html
Copyright © 2011-2022 走看看