java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > JVM的双亲委派、内存溢出

Java中JVM的双亲委派、内存溢出、垃圾回收和调优详解

作者:燕双嘤

这篇文章主要介绍了Java中JVM的双亲委派、内存溢出、垃圾回收和调优详解,类加载器是Java虚拟机(JVM)的一个重要组成部分,它的主要作用是将类的字节码加载到内存中,并生成对应的Class对象,需要的朋友可以参考下

1.双亲委派

1.1 类加载过程

【加载】:加载是指将类的字节码文件读入内存,并在内存中创建一个Class对象,用来描述该类的结构信息。类的字节码可以来自本地磁盘、网络等各种来源。

【连接】:连接是指对类的字节码进行验证、准备和解析的过程。

【初始化】:初始化是类加载过程的最后一步,主要是对类的静态变量赋值和执行静态代码块。

1.2 类加载器

类加载器:是Java虚拟机(JVM)的一个重要组成部分,它的主要作用是将类的字节码加载到内存中,并生成对应的Class对象。

System.setProperty("java.system.class.loader", "com.example.MyClassLoader");

类加载器分类:

(1)第一种是 c++ 语言写的 启动类加载器:Bootstrap ClassLoader。

(2)第二种是 java 语言写的 其他各种类加载器。可以分为扩展类加载器, 应用程序类加载器 以及各种各样的自定义类加载器。自定义类加载器 一般是需要进行动态加载或者其他业务目的类加载器。

  • 启动类加载器:它是JVM的一部分,主要负责加载Java的核心类库,比如java.lang包中的类。
  • 扩展类加载器:它负责加载JVM扩展目录(java.ext.dirs)中的类库,比如javax包中的类。
  • 应用程序类加载器:它负责加载应用程序classpath目录下的类库,是程序中默认的类加载器。
  • 自定义类加载器:它是开发人员自己实现的类加载器,可以根据自己的需求实现不同的加载逻辑。

1.3 双亲委派机制

【类的唯一性】在一个虚拟机里,一个类只有一个对应的类对象。 现在对类的唯一性的理解,应该更为准确地描述为: 在一个加载器下,一个类只有一个对应的类对象。如果有多个类加载器都各自加载了同一个类,那么他们将得到不同的类对象。

【双亲委派机制】JVM中的双亲委派机制是指,当一个类被加载时,JVM会首先委托其父类加载器去加载该类,只有当父类加载器无法加载该类时,才会由子类加载器去加载。这样可以保证Java虚拟机中的类不会出现同名的情况,也能保证Java核心类库的安全性和稳定性。

需要注意的是,在使用类加载器打破双亲委派机制时,应该保证加载的类与父加载器加载的类之间没有依赖关系,否则可能会导致异常或者出现未知的错误。

(1)双亲委派机制的意义主要是保护一些基本类不受影响。比如常用的 String类, 其全限定名是 java.lang.String,只是 java.lang 这个包下的类在使用的时候,可以不用 import 而直接使用。像这种基本类按照双亲委派机制 都应该从 rt.jar 里去获取,而不应该从自定义加载器里去获取某个开发人员自己写的 java.lang.String,毕竟开发人员自己写的 java.lang.String 可能有很多 bug,通过这种方式,无论如何大家使用的都是 rt.jar 里的 java.lang.String 类了。

(2)避免类的重复加载。由于每个类加载器都有自己的命名空间,当父类加载器已经加载了一个类时,子类加载器再去加载该类时,就会导致重复加载,浪费内存空间。通过双亲委派机制,可以避免重复加载,提高JVM的运行效率。

(3)确保类的一致性。由于父类加载器的优先级高于子类加载器,所以子类加载器无法覆盖父类加载器已经加载的类。这样可以保证不同的类加载器加载的同一个类是一致的,避免了由于不同版本的类加载器加载同一个类而导致的问题。

【注意】双亲委派机制对应的英文是:Parents Delegation Model,这个 Parents 后面有个 s,所以就被翻译成双亲了。而 parents 英文本身想表达的意思是上溯,父辈,祖先的意思,即爸爸,爷爷,祖爷爷~但是却被翻译成双亲,双亲在中文里强调的是父亲和母亲两个。所以是翻译不够准确,略微会引起一些歧义。

虽然双亲委派机制可以避免类的重复加载、提高JVM的运行效率和系统的稳定性,但在一些特定的场景下,也需要打破双亲委派机制,例如:

  • 实现类隔离。在某些情况下,需要使用不同的类加载器加载同一个类的不同版本,或者需要使用不同的类加载器加载同一个类的不同实现,这时就需要打破双亲委派机制,实现类的隔离。
  • 自定义类加载器。在某些情况下,需要使用自定义的类加载器加载类,例如在OSGi等动态模块化框架中就需要使用自定义的类加载器。此时也需要打破双亲委派机制。
  • 热部署。在热部署的场景下,需要动态更新已经加载的类,而双亲委派机制会阻止子类加载器重新加载已经由父类加载器加载的类,因此需要打破双亲委派机制,实现热部署功能。

1.4 自定义类加载器

为什么需要类加载器:每个 Web 应用各自都需要配置一个专有的类加载器。Tomcat会有多个不同的war包,需要实现类隔离。

  • 创建一个类 CustomizedClassLoader ,继承自 ClassLoader。
  • 定义属性 classesFolder 其值是当前项目下的 classes_4_test 目录。
  • 重写 loadClassData 方法,它会把传进去的全限定类名,匹配到文件。
  • 重写 findClass 方法,把这个字节数组通过调用 defineClass 方法,就转换成 HOW2J 这个类对应的 Class 对象了。
  • 拿到这个类对象之后,通过反射机制,调用其 hello 方法,就能看到如图所示的字符串:"Hello!"
import java.io.File;
import java.lang.reflect.Method;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
public class CustomizedClassLoader extends ClassLoader {
    private File classesFolder = new File(System.getProperty("user.dir"),"classes_4_test");
    protected Class<?> findClass(String QualifiedName) throws ClassNotFoundException {
        byte[] data = loadClassData(QualifiedName);
        return defineClass(QualifiedName, data, 0, data.length);
    }
    private byte[] loadClassData(String fullQualifiedName) throws ClassNotFoundException {
        String fileName = StrUtil.replace(fullQualifiedName, ".", "/") + ".class";
        File classFile = new File(classesFolder, fileName);
        if(!classFile.exists())
            throw new ClassNotFoundException(fullQualifiedName);
        return FileUtil.readBytes(classFile);
    }
    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader = new CustomizedClassLoader();
        Class<?> how2jClass = loader.loadClass("cn.ysy.diytomcat.Test");
        Object o = how2jClass.newInstance();
        Method m = how2jClass.getMethod("hello");
        m.invoke(o);
        System.out.println(how2jClass.getClassLoader());
    }
}

2. 内存溢出&内存泄露

2.1 内存模型

【基本定义】Java的内存模型定义了Java程序在运行时的内存结构以及多线程情况下,多个线程之间如何共享内存。Java的内存模型保证了线程安全性,避免了多线程访问共享内存时出现的数据竞争、死锁等问题。

【组成部分】Java内存模型将内存分为两个部分:线程工作内存和主内存。线程工作内存是线程独有的内存空间,用于存储线程运行时的局部变量等数据,而主内存是所有线程共享的内存空间,用于存储Java程序中定义的全局变量等数据。

【特性】Java内存模型定义了一组规则,确保多个线程之间对共享内存的访问是正确的。其中包括:

  • 可见性:当一个线程修改了共享变量的值后,其他线程可以立即看到该变量的修改。
  • 原子性:对共享变量的读写操作应该被视为一个原子操作,不可被中断。
  • 有序性:线程之间的操作可能会被编译器、处理器进行指令重排等优化,但是Java内存模型保证了操作执行的顺序不会影响程序的正确性。

2.2 内存溢出(OOM)

内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。

内存溢出原因(案例,场景):

  • 内存中加载的数据量过于庞大,比如sql全表扫描 ;
  • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  • 代码中存在死循环或循环产生过多重复的对象实体;
  • 启动参数内存值设定的过小。
  • 堆栈溢出

解决内存溢出:

  • 修改JVM启动参数,直接增加内存。
  • 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
  • 对代码进行走查和分析,找出可能发生内存溢出的位置。
  • 使用内存查看工具动态查看内存使用情况。

堆栈溢出:

  • Java堆溢出 Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
  • 虚拟机栈和本地方法栈溢出 HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
  • 方法区和运行时常量池溢出 方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。
  • 本地直接内存溢出 直接内存的容量大小可通过`-XX:MaxDirectMemorySize`参数来指定,如果不去指定,则默认与Java堆最大值一致。如果直接通过反射获取Unsafe实例进行内存分配,并超出了上述的限制时,将会引发OOM异常。

2.3 内存泄露

内存泄漏是指不再使用的对象仍然被引用,导致垃圾收集器无法回收它们的内存。由于不再使用的对象仍然无法清理,甚至这种情况可能会越积越多,最终导致致命的OutOfMemoryError。

内存泄漏场景:

  • 动态分配内存但没有及时释放:如果程序中使用了动态内存分配(例如使用 malloc 函数),但没有及时释放内存(例如使用 free 函数),就会导致内存泄漏。
  • 循环引用:如果程序中存在循环引用(例如两个对象互相引用),就会导致这些对象无法被垃圾回收器回收,最终导致内存泄漏。
  • 静态变量未释放:如果程序中存在静态变量,且这些静态变量在程序运行过程中未被释放,就会导致内存泄漏。
  • 没有正确使用引用计数:如果程序中使用了引用计数,但没有正确地使用引用计数,就会导致内存泄漏。
  • 文件描述符未关闭:如果程序中使用了文件描述符(例如打开文件),但没有正确关闭文件,就会导致内存泄漏。

内存泄露的原因:

  • 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。
  • 不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露 。

解决内存泄漏:

3.垃圾回收机制

3.1 传统垃圾回收

垃圾回收(Garbage Collection,GC)就是释放垃圾占用的内存空间,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,防止内存泄露。

【垃圾判断算法】

【垃圾回收算法】

【Full GC&Minor GC】

【Full GC触发条件】

3.2 G1垃圾回收器

Garbage First(G1)收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。其他收集器的垃圾收集的目标范围要么是整个新生代,要么就是整个老年代,再要么就是整个Java堆(Full GC)。而G1跳出了这个限制,G1垃圾回收器相较于其他垃圾回收器的一大特点就是它支持将Java堆内存划分为多个大小相等的区域,每个区域能够独立管理,并且可以根据应用程序的实时需求动态地选择哪些区域进行垃圾回收。

G1收集器的运作过程大致可划分为以下四个步骤:初始标记、并发标记、最终标记、筛选回收。其中,初始标记和最终标记阶段仍然需要停顿所有的线程,但是耗时很短。

从JDK 9开始,G1成为了默认的垃圾回收器,它是HotSpot在JVM上推出的垃圾回收器,旨在取代CMS。G1的主要特点包括:

3.3 CMS垃圾回收器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,是基于标记清除算法实现的。它最主要的优点是:并发收集、低停顿。它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:初始标记、并发标记、重新标记、并发清除。

CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

G1与CMS的对比:G1从整体来看是基于标记整理算法实现的收集器,但从局部上看又是基于标记复制算法实现。无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。比起CMS,G1的弱项也可以列举出不少。例如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高。

3.4 ZGC垃圾回收

ZGC(Z Garbage Collector)是一个可伸缩性非常好的低延迟垃圾回收器,它在JDK 11中引入,旨在处理大型堆,适用于云部署和其他内存密集型应用程序。

ZGC的主要特点包括:

ZGC具有高性能、低延迟、可预测和背压感知等特点,适用于多核CPU的应用场景,它可以在不牺牲吞吐量的情况下,大大减少GC停顿时间,从而提高应用程序的响应速度和性能。

4.JVM调优 

4.1 JVM常用参数

4.2 JVM调优策略

到此这篇关于Java中JVM的双亲委派、内存溢出、垃圾回收和调优详解的文章就介绍到这了,更多相关JVM的双亲委派、内存溢出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文