关于Java类加载的回忆

论坛 期权论坛 脚本     
匿名技术用户   2021-1-7 09:29   48   0

源于蚂蚁课堂的学习,点击这里查看(老余很给力)

Java底层是由C++去编写的,而C/C++由是封装汇编指令,进而转为二进制被计算机识别。所以当我们作为Java开发者,写好Java代码后,是需要进过JavaComplier进行编译生成class字节码文件,然后由类加载器将字节码文件加载至Java虚拟机内存中进行使用,JVM通过调用C的指令转汇编,从而使得代码生效。

本文将简单聊聊博主对Java类加载机制相关知识的理解。

一、Class字节码生成原理

这对于大多数开发者而已,应该耳熟能详,.java文件经过Java编译器的编译后生成.class文件,交由JVM运行。Java开发者引以为豪的地方就是Java的代码,“一次编程,导出使用”。其缘由,在不同的操作系统上,有着不同的JVM,所以我们代码只需要编译一次即可,不同JVM加载相同的class字节码文件,通过硬件层面屏蔽操作系统的一些指令,从而使得代码产生相同的功能。

二、触发类加载的条件

Java文件编译为class文件后,需要通过类加载器加载至虚拟机内存中。那么,哪些情况下,类加载器才会去加载class文件呢?

1、创建类的实例

如new对象,反射生成,反序列化,克隆。都会先加载类的信息。

2、访问类的静态信息

访问类的静态方法、静态变量等类特有信息时,会把类加载至内存 。

3、Class.forName()

通过反射的方式主动加载类。

4、Main方法

此类中包含Java应用程序运行的入口,则一定会被加载。

5、子类被加载

子类加载时一定会先加载父类。

三、类加载器

类加载器就是将class字节码加载至JVM的工具。通常分为四种:

1、启动类加载器(BootStrapClassLoader)

最上层的类加载器,主要职责为加载$JAVA_HOME/jre/lib下的class。

2、扩展类加载器(ExtClassLoader)

第二层类加载器,主要职责为加载$JAVA_HOME/jre/lib/ext下的class。

3、应用类加载器(AppClassLoader)

我们自己创建的工程中的类加载器,加载自己项目中定义的那些class。

4、自定义类加载器

自定义的类加载器,可以加载指定范围下的class

四、双亲委派机制

当类被触发加载时,会先找到当前线程的类加载器,向上查找至最顶层,也就是启动类加载器,然后自上而下判断当前类加载器中是否有此类的class,没有就向下查找,否则,就行class的加载并停止向下查找。 我们可以通过源码得出此规律。

ClassLoader通过loadClass()方法加载类,此方法中有一处核心代码。
if (parent != null) {
 c = parent.loadClass(name, false);
} else {
 c = findBootstrapClassOrNull(name);
}

这里parent指的是当前线程类加载器的上级类加载器。通过此代码可以看出,从当前线程类加载器不断向上查找,直到parent == null,即查找到当前类加载器为扩展类加载器(注意,启动类加载器是C++编写,非Java对象,故扩展类加载器的parent为null,也代表启动类加载器)。开始执行findBootstrapClassOrNull方法。

private Class<?> findBootstrapClassOrNull(String name)
{
 if (!checkName(name)) return null;

 return findBootstrapClass(name);
}

private native Class<?> findBootstrapClass(String name);

源码可得,最终通过本地方法栈调用findBootstrapClass去启动类加载器的职责中查找当前类的信息。

如果启动类加载器查找不到,则进入下面代码
if (c == null) {
 
 long t1 = System.nanoTime();
 c = findClass(name);

 // this is the defining class loader; record the stats
 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
 sun.misc.PerfCounter.getFindClasses().increment();
}

而这个findClass方法是非常重要的方法,自定义类加载器都会重写findClass而非loadClass(类似于模板方法设计模式的原则)。

默认采用URLClassLoader,在此类中重写的findClass方法根据参数className进行文件地址的拼接,然后去扩展类加载器中查找此类,扩展类加载器没有的话,再向下查找。

双亲委派机制最大好处是当我们自己定义的类全路径名称和JDK定义的冲突时,不会冲突,而是按照优先级加载。

五、打破双亲委派机制

双亲委派机制核心点在于classLoader的loadClass方法中循环查找。所以想要打破双亲委派原则,我们只需饶佐loadClass或者重新findClass即可。

1、SPI机制

可以将指定接口的实现类加载至内存中,其约定了在/resources/services/META-INF/下定义以接口全类名为文件名的文件,内容是接口的实现类。如果通过ServiceLoad.load(接口.class)的方式返回接口的实现类集合。在其遍历使用时,进行加载。

public static <S> ServiceLoader<S> load(Class<S> service) {
 ClassLoader cl = Thread.currentThread().getContextClassLoader();
 return ServiceLoader.load(service, cl);
}
获取当前线程类加载器后,调用load方法。
public static <S> ServiceLoader<S> load(Class<S> service,
          ClassLoader loader)
{
 return new ServiceLoader<>(service, loader);
}

创建ServiceLoader对象。值的一提的是,此类的属性中private static final String PREFIX = "META-INF/services/";此处约定了文件地址。

构造方法中初始化了一个迭代器,且设置类加载器(如果没有就为当前的appClassLoader)

private ServiceLoader(Class<S> svc, ClassLoader cl) {
 service = Objects.requireNonNull(svc, "Service interface cannot be null");
 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
 reload();
}


public void reload() {
 providers.clear();
 lookupIterator = new LazyIterator(service, loader);
}

当我们遍历这个迭代器时,其netx方法中进行配置文件的读取,直接通过类加载器将类加载

private boolean hasNextService() {
 if (nextName != null) {
  return true;
 }
 if (configs == null) {
  try {
   String fullName = PREFIX + service.getName();
   if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
   else
    configs = loader.getResources(fullName);
  } catch (IOException x) {
   fail(service, "Error locating configuration files", x);
  }
 }
 while ((pending == null) || !pending.hasNext()) {
  if (!configs.hasMoreElements()) {
   return false;
  }
  pending = parse(service, configs.nextElement());
 }
 nextName = pending.next();
 return true;
}

2、自定义类加载器

通过继承的方式我们可以自己定义一个类加载器,然后重写其findClass方法(官方推荐),也可以直接重写loadClass方法(但是如果这么做了,相当于此类加载器想要加载一个类,类的最上层Object需要它手动加载才行,太繁琐)。

原理即绕过这个class加载时触发双亲委派原则的逻辑即可。附一个自定义的类加载器

package live.yanxiaohui.classLoader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/**
 * @Description TODO
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author yanxh<br>
 * @Date 2020/10/22 9:46<br>
 * @Version 1.0<br>
 */
public class YXHClassLoader extends ClassLoader {


    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        try {
            byte[] data = getClassFileBytes(getFileObject());
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private File fileObject;

    public void setFileObject(File fileObject) {
        this.fileObject = fileObject;
    }

    public File getFileObject() {
        return fileObject;
    }

    /**
     * 官方不建议重写该loadClass
     *
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
//    @Override
//    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//        return super.loadClass(name, resolve);
//    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 读取我们硬盘上的class文件
        try {
            byte[] data = getClassFileBytes(getFileObject());
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

    }


    /**
     * 从文件中读取去class文件
     *
     * @throws Exception
     */
    private byte[] getClassFileBytes(File file) throws Exception {
        //采用NIO读取
        FileInputStream fis = new FileInputStream(file);
        FileChannel fileC = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel outC = Channels.newChannel(baos);
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        while (true) {
            int i = fileC.read(buffer);
            if (i == 0 || i == -1) {
                break;
            }
            buffer.flip();
            outC.write(buffer);
            buffer.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
}

欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!

公众号:帝都的雁

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP