JVM类装载机制

类装载流程

JVM对类的装载包含以下五个步骤,当然除了对.class文件的装载,JVM也可以对满足类规范的二进制流进行装载。而只有被JVM装载完成的Class,才能在程序中被调用
类装载流程图

加载

类加载的条件

一个类或接口,只有在被使用的时候,才会被装载。而类会初始化的场景如下:

  1. 创建对象时,也就是new对象或反射,反序列化,克隆
  2. 调用对象的静态方法或静态属性(final修饰的属性除外,final修饰的为常量,在JVM启动时,就已经被初始化在常量池中)
  3. 初始化子类,父类必须先与子类初始化

加载类

在加载类时,JVM必须完成以下工作

  1. 全类名获取二进制流,可以通过读入.class文件,或者jar包,甚至数据库中或HTTP传输的二进制流。
  2. 解析类的二进制流为方法区的数据结构
  3. 创建java.lang.Class类的实例,保存二进制流的元数据信息,比如类的方法,属性等等

验证

JVM验证过程

准备

准备阶段,也就是为这个类分配内存空间,并为常量设置一些初始值,如int初始为0

解析

解析也就是将类、接口、属性的符号引用,转为直接引用,也就是得到这个类属性、方法在内存中的偏移量。

初始化

初始化也就是生成类对应的clinit函数的指令。另外需要注意的是,类初始化的过程是加锁的,所以在多线程的环境下,需要注意类初始化产生相互等待,导致死锁的情况。


public class Demo1 {
    static{
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Demo4();
    }
}

public class Demo4 {
    static{
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Demo1();
    }
}

public class JVMTest {
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(2);
        ((ExecutorService) es).submit(()->{
            new Demo1();
        });
        ((ExecutorService) es).submit(()->{
            new Demo4();
        });
        es.shutdown();
    }
}

执行以上代码,我们会发现,程序一直处于阻塞状态。我们可以通过JDK自带的监控工具jvisualvm查看线程dump。linux系统可以通过jmap命令导出dump文件(jmap -dump:format=b,file=demodump.hprof pid)
在这里插入图片描述

认识ClassLoader

ClassLoader分类

类加载器包括启动类加载器、扩展类加载器、应用类加载器以及自定义加载器。启动类主要负责加载系统的核心类,如JDK中rt.jar中的类。扩展类加载器主要负责加载JDK中ext目录下的类。而应用类加载器一般就是用户的程序类。
类加载结构

ClassLoader的双亲委派模式

JVM在加载类时,会默认使用双亲委派模式,也就是会先判断当前类是否被加载,如果没有,则会委派给双亲加载,双亲加载失败后,则会自己加载。

双亲委派模式的优劣

双亲加载模式是单向的,加载层次结构清晰,职责明确。
但是也会带来一个问题,也就是顶层的类加载器假的类是无法访问底层类加载器加载的类。而顶层的类加载器加载的类,需要访问底层类加载器加载的类,这种场景是存在的。比如JDBC,XML解析,他们是在系统类中提供了一个接口,与工厂方法获取实例,但是接口和工程方法都是系统类加载器加载的,而具体的实现却是应用类加载器加载的。

双亲委派模式的补充

JDK的Thread类,提供了一个getContextClassLoader(),和setContextClassLoader(ClassLoader cl)方法,用于获取当期线程的上下文类加载器,对于当期线程来说,是可以全局获取的,也就是启动类加载器的类中,执行

ClassLoader cl = Thread.currentThread().getContextClassLoader();

那么就可以获取当期线程的传入类加载器,比如应用类加载器。这样就可以成功获取底层类加载器,加载相应的类了。

打破双亲委派模式

双亲委派模式,是通过ClassLoader的loadClass方法实现的,因此我们可以通过实现ClassLoader重写loadClass方法,来打破双亲委派模式。Tomcat就是这样实现的。
这样做有什么作用呢?
我们可以通过自定义类加载器,实现热替换功能,比如JSP,就是通过这种方式实现的。JSP其实就是JAVA代码,我们变更JSP文件后,那么就会生成新的.class类。我们可以自定义每个JSP对应一个类加载器,这样在变更JSP后,就会有新的类加载器去加载,而旧的类加载器直接废弃,从而实现热替换功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值