|
源于蚂蚁课堂的学习,点击这里查看(老余很给力)
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();
}
}
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁

|