前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM问题典型案例定位学习

JVM问题典型案例定位学习

作者头像
菩提树下的杨过
发布2020-03-19 11:38:01
7010
发布2020-03-19 11:38:01
举报

下面这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时间恢复,说明大概率就是这个原因。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-03-15 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档