专栏首页菩提树下的杨过JVM问题典型案例定位学习

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • puremvc框架之Command

    在前一篇 puremvc框架之hello world! 里,已经对这个框架有了一个大概的认识,不过在消息的处理上,有一个不太适合的地方: 为了完成响应消息,Te...

    菩提树下的杨过
  • 利用java8对设计模式的重构

    java8中提供的很多新特性可以用来重构传统设计模式中的写法,下面是一些示例: 一、策略模式 ? 上图是策略模式的类图,假设我们现在要保存订单,OrderSer...

    菩提树下的杨过
  • mac上开启ftp

    开启 sudo -s launchctl load -w /System/Library/LaunchDaemons/ftp.plist 关闭 sudo...

    菩提树下的杨过
  • 谁是存在感最低的省会城市?

    当“一二三线、网红、新零售、抖音”等新兴标签正在不断为大大小小的城市增加声量,“省会”的称号却越来越失去往日光华。

    华章科技
  • 谁是存在感最低的省会城市?

    省会,曾几何时,被认为是地位仅次于首都、直辖市的城中之贵族,手握主政一方之大权,是历史上城市打破头拼抢的头衔。

    数据森麟
  • 学不会的JVM

    首先我们写的源文件叫.java文件,然后点击IDE的运行在硬盘会生成.class字节码文件,接着Java虚拟机从硬盘加载.class字节码文件,再者内部操作和解...

    晚上没宵夜
  • 实习杂记(30):虚拟机类的加载机制(1)

    其中  解析 这步是不确定的,是因为需要支持  运行时绑定,也称为:动态绑定或晚期绑定

    wust小吴
  • JVM解读-类加载机制

    Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。

    高广超
  • 你真的懂「类的加载机制」吗?

    高广超 :多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能互联网架构。目前就职于美团网,负责核心业务研发工作。本文首发在 高广超的简书博客,欢迎点...

    用户1093975
  • Java虚拟机详解(十)------类加载过程

      在上一篇文章中,我们详细的介绍了Java类文件结构,那么这些Class文件是如何被加载到内存,由虚拟机来直接使用的呢?这就是本篇博客将要介绍的——类加载过程...

    IT可乐

扫码关注云+社区

领取腾讯云代金券