前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM学习-类加载

JVM学习-类加载

作者头像
sgr997
发布2022-11-10 13:48:12
2500
发布2022-11-10 13:48:12
举报
文章被收录于专栏:博客分享

类加载阶段

加载

  • 将类的字节码载入到方法区中,内部采用C++的instanceKlass描述java类,他的重要field有:
    • java_mirror即java的类镜像,例如对String来说,就是String.class,作用是吧klass暴露给java使用
    • _super即父类
    • _field即成员变量
    • _method即方法
    • constants即常量池
    • _class_loader即类加载器
    • vtable虚方法表
    • _itable接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意

  • instabceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中

链接

验证:验证类是否符合JVM规范,安全性检查。用UE等支持二进制的编辑器修改HelloWord.class的魔数(CA FE BA BE),在控制台运行

准备:为static变量分配空间,设置默认值

  • static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
  • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果static变量是final的基本类型,以及字符创常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成

解析:将常量池中的符号引用解析为直接引用

初始化

()V方法 初始化即调用()v,虚拟机会保证这个类的【构造方法】的线程安全 发生的时机 概括的说,类初始化是【懒惰的】

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new会导致初始化

不会导致类初始化的情况

  • 访问类的static final 静态变量(基本类型和字符串)不会触发初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName的参数2为false时
代码语言:javascript
复制
public class Load3 {
    static {
        System.out.println("main init");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        // 静态final变量(基本类型和字符串)不会触发初始化
        System.out.println(B.b);
        // 类对象.class不会触发初始化
        System.out.println(B.class);
        // 创建类的数组不会触发初始化
        System.out.println(new B[0]);
        // 不会初始化B,但会加载A B
        ClassLoader c1 = Thread.currentThread().getContextClassLoader();
        c1.loadClass("cn.itcast.load.B");
        // 不会初始化B,但会加载A B
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("cn.itcast.load.B", false, c2);

        // 首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 子类初始化,如果父类还没初始化,会引发
        System.out.println(B.c);
        // 子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 会初始化类B,并先初始化类A
        Class.forName("cn.itcast.load.B");
    }
}

class A {
    static int a = 0;

    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;

    static {
        System.out.println("b init");
    }
}

懒加载且线程安全的单例模式

代码语言:javascript
复制
public class Load9 {
    public static void main(String[] args) {
        // 只调用test方法是不会触发LazyHolder的加载
        Singleton.test();
        // 此时才会加载LazyHolder
        Singleton.getInstance();
    }
}

class Singleton {

    private Singleton() {
    }

    public static void test() {
        System.out.println("test");
    }

    private static class LazyHolder {

        private static final Singleton SINGLETON = new Singleton();

        static {
            System.out.println("lazy holder init");
        }
    }

    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

类加载器

以JDK8为例:

名称

加载哪的类

说明

Bootstrap ClassLoader

JAVA_HOME/jre/lib

无法直接访问

Extension ClassLoader

JAVA_HOME/jre/lib/ext

上级为Bootstrap,显示为null

Application ClassLoader

classpath

上级为Extension

自定义类加载器

自定义

上级为Application

验证Extension :jar -cvf my.jar com/learn/G.class 打为jar包,放至JAVA_HOME/jre/lib/ext

代码语言:javascript
复制
D:\JavaProject\learn\classload\out\production\classload>jar -cvf my2.jar com\learn\G.class
已添加清单
正在添加: com/learn/LoaderTest.class(输入 = 872) (输出 = 486)(压缩了 44%)
代码语言:javascript
复制
public class Loader {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.learn.G");
        System.out.println(aClass.getClassLoader());// 输出:sun.misc.Launcher$ExtClassLoader@135fbaa4
    }
}

双亲委派模式

所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则 ClassLoader.loadClass()方法源码

代码语言:javascript
复制
protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1.检查该类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                // 没加载
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2. 如果有上级,委派上级loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                        // 就是去JAVA_HOME/jre/lib找
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    // 4. 每一层都找不到,调用findClass方法(每个类加载器自己扩展)来加载
                    c = findClass(name);

                    // 记录耗时
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

线程上下文类加载器

我们在使用JDBC时,都需要加载Driver驱动,不知道你注意到没有,不写

代码语言:javascript
复制
Class.forName("com,mysql.jdbc.Driver")

也是可以让com.mysql.jdbc.Driver正确加载的,你知道是怎么做的吗? 让我们追踪一下源码:

代码语言:javascript
复制
public class DriverManager {


    // 注册驱动集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    // 初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

先不看别的,看看DriverManager的类加载器

代码语言:javascript
复制
System.out.println(DriverManager.class.getClassLoader()); // 输出为null,说明是Bootstrap加载的

打印null,表示它的类加载器是Bootstrap ClassLoader,会到JAVA_HOME/jre/lib下搜索类,但JAVA_HOME/jre/lib下显然没有mysql-connertor-java.jar包,这样问题就来了,在DriverManager的静态代码块中,怎么能正确加载com.mysql.jdbc.Driver呢?

继续看loadInitialDrivers()方法:

代码语言:javascript
复制
private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        // 1)使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                // 此处Iterator实例数据来源其实是在ServiceLoader的内部类LazyIterator中
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        // 2)使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 3)ClassLoader.getSystemClassLoader()其实就是应用程序类加载器 Application ClassLoader
                // 此处相当于打破了双亲委派模式的类加载机制
                // DriverManager理应使用Bootstrap类加载器加载,而这里实际使用的是Application ClassLoader
                // 因为Bootstrap压根加载不到mysql的驱动类
                Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

注意3)处,此处相当于打破了双亲委派模式的类加载机制,DriverManager作为JAVA_HOME/jre/lib下的jar包,理应使用Bootstrap ClassLoader加载,而这里实际使用的是Application ClassLoader,因为Bootstrap压根加载不到mysql的驱动类,只能由Application ClassLoader来加载,所以某些情况,jdk要打破双亲委派机制才能完成某些类的加载。 再看1)他就是大名鼎鼎的Service Provider Interface(SPI) 约定如下,在jar包的META_INF/services包下,以接口全限定名为文件名,文件内容就是实现类名称

如上图,这样就可以使用

代码语言:javascript
复制
ServiceLoader<Driver> allImpls = ServiceLoader.load(Driver.class);
Iterator<Driver> iter = allImpls.iterator();
while (iter.hasNext()) {
    iter.next();
}

获取到com.mysql.jdbc.Driver和FabricMySQLDriver实现类,体现的是【面向接口编程+解耦】的思想 接着看ServiceLoader.load方法:

代码语言:javascript
复制
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部有是由Class.forName调用了线程上下文类加载器完成类加载,具体代码在ServiceLoader的内部类LazyIterator中:

代码语言:javascript
复制
private class LazyIterator
        implements Iterator<S>
    {
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                // 缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

自定义类加载器

问问自己,什么时候需要自定义类加载器

  • 想加载非classpath随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器

步骤

  1. 继承ClassLoader父类
  2. 要遵从双亲委派机制,重写findClass方法 a. 注意不是重写loadClass方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的defineClass方法来加载类
  5. 使用者调用该类加载器的loadClass方法
代码语言:javascript
复制
public class CustomClassLoader extends ClassLoader {
    
    public static void main(String[] args) throws ClassNotFoundException {
        CustomClassLoader classLoader = new CustomClassLoader();
        classLoader.loadClass("MapImpl1");
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "D:\\" + name + ".class";
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            Files.copy(Paths.get(path), os);
            byte[] bytes = os.toByteArray();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到:" + name, e);
        }
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-23,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类加载阶段
    • 加载
      • 链接
        • 初始化
        • 类加载器
          • 双亲委派模式
            • 线程上下文类加载器
              • 自定义类加载器
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档