要明白类加载机制是什么,首先得知道什么是类
我们写的java代码都是以.java为后缀的java源码文件,想要让计算机识别,需要将.java文件编译成.class二进制文件,那么这个class就是我们今天讲的类,有.class文件还不行,java程序是运行在JVM上的,所以还需要将.class文件加载到JVM中,所有就引出了咱们今天的话题,类加载机制,那么类是如何加载的呢
为了避免开发工具为我们做的自动化操作,我们就用最原始的写代码的方式来看一下,用记事本
测试代码如下
public class Demo1{
static {
System.out.println("Demo1初始化了..");
}
public static void main(String[] args) {
Class clazz = Object.class;
System.out.println("Object类的加载器为:" + clazz.getClassLoader());
System.out.println("Demo1类的加载器为:" + Demo1.class.getClassLoader());
}
}
看一下运行结果
运行环境有问题的可以看博主这篇文章一看就懂的jdk环境变量配置
输出第一行Demo1初始化了..
说明Demo1这个类加载到了JVM中,换句话说,如果不加载Demo1,程序也没法跑起来,这个是毋庸置疑的
输出第三行Demo1类的加载器为:sun.misc.Launcher$AppClassLoader@73d16e93
通过Class对象的getClassLoader方法即可拿到该类的类加载器
那有人会问Object、System类我并没有去编译、也没有去加载,为何能在代码中执行呢?
Object、System类大家都知道是在rt.jar
包中的,在JVM启动的时候根加载器会加载java程序中的基本类库,所以这些都是预加载好了的,就可以直接调用。
那我们能不能拿到根加载器呢,从第二行输出可以看到是不行的,根加载器不是用java实现的,所以是不可能拿到的
那么类加载器分别有哪些呢?
当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构。
- Bootstrap ClassLoader:根加载器
- Extension ClassLoader:扩展类加载器
- System ClassLoader:系统加载器
也就是这3个类加载器会帮我们加载一些类,那么加载的类都在哪里呢?
public class BootstrapTest {
public static void main(String[] args) {
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i=0; i<urls.length; i++){
System.out.println(urls[i].toExternalForm());
}
}
}
运行这段代码可以看到结果如下
file:/E:/software/jdk1.8.0_121/jre/lib/resources.jar
file:/E:/software/jdk1.8.0_121/jre/lib/rt.jar
file:/E:/software/jdk1.8.0_121/jre/lib/sunrsasign.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jsse.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jce.jar
file:/E:/software/jdk1.8.0_121/jre/lib/charsets.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jfr.jar
file:/E:/software/jdk1.8.0_121/jre/classes
由运行结果可知,根加载器加载了咱们程序中标准的类库如rt.jar
再来个Demo看看系统加载器加载类的路径在哪
import java.net.URL;
import java.util.Enumeration;
public class Demo1 {
public static void main(String[] args) throws Exception{
Enumeration<URL> urls = Demo1.class.getClassLoader().getResources("");
while (urls.hasMoreElements()){
System.out.println(urls.nextElement());
}
}
}
可以看到输出结果为
file:/F:/study_demo/
file:/D:/
再看看我的系统环境变量
大家明白了为什么要配置classpath了吧
那有人要说了,我在IDE开发工具中没有配置classpath这个东西
拿intellij idea为例,它是在下图所示配置了在哪里去加载我们自己写的class文件
除了以上3种类加载器外,我们还可以自定义类加载器,只要继承了ClassLoader即可
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
public class CompileClassLoader extends ClassLoader {
//读取一个文件的内容
private byte[] getBytes(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
try(FileInputStream fin = new FileInputStream(file)) {
//一次读取Class文件的全部二进制数据
int r = fin.read(raw);
if (r != len) throw new IOException("无法读取全部文件:" + r + " !=" + len);
return raw;
}
}
//定义编译指定Java文件的方法
private boolean compile(String javaFile) throws IOException{
System.out.println("CompileClassLoader:正在编译" + javaFile + "...");
//调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try {
//其他线程都等待这个线程完成
p.waitFor();
}catch (InterruptedException ie){
System.out.println(ie);
}
//获取javac线程的退出值
int ret = p.exitValue();
//返回编译是否成功
return ret == 0;
}
//重写ClassLoader的findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
Class clazz = null;
//将包路径中的点(.)替换成斜线(/)
String fileStub = name.replace(".", "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
//当指定Java源文件存在,且Class文件不存在,或者Java源文件的修改时间比Class文件的修改时间更晚时,重新编译
if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())){
try {
//如果编译失败,或者该Class文件不存在
if (!this.compile(javaFilename) || !classFile.exists()){
throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
}
}catch (IOException e){
e.printStackTrace();
}
}
//如果Class文件存在,系统负责将该文件转换成Class对象
if (classFile.exists()){
try {
//将Class文件的二进制数据读入数组
byte[] raw = this.getBytes(classFilename);
//调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz = this.defineClass(name, raw, 0, raw.length);
}catch (IOException e){
e.printStackTrace();
}
}
//如果clazz为null,表明加载失败,则抛出异常
if (clazz == null){
throw new ClassNotFoundException(name);
}
return clazz;
}
//定义一个主方法
public static void main(String[] args) throws Exception{
//如果运行该程序时没有参数,即没有目标类
if (args.length < 1){
System.out.println("缺少目标类,请按如下格式运行Java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
//第一个参数是需要运行的类
String progClass = args[0];
//剩下的参数将作为目标类时的参数,将这些参数复制到一个新数组中
String[] progArgs = new String[args.length - 1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
//加载需要运行的类
Class<?> clazz = ccl.loadClass(progClass);
//获取需要运行的类的主方法
Method main = clazz.getMethod("main", (new String[0]).getClass());
Object argsArray[] = {progArgs};
main.invoke(null, argsArray);
}
}
Hello类如下
public class Hello {
public static void main(String[] args){
for(String arg : args){
System.out.println("运行Hello的参数" + arg);
}
}
}
查看结果
我们无须手动编译Hello.java类,我们自定义的ClassLoder帮我们完成了,当然这里只是为了演示功能,真正的编译工作肯定不需要我们自己去做。
为了验证Hello类的加载器是我们自定义的CompileClassLoader,我们可以在Hello类中打印System.out.println(Hello.class.getClassLoader());
结果如下
bingo!
多说几句,其实从上面的代码中就可以看到类加载的过程,先要编译java源文件,然后读取class二进制文件到内存中,最后将二进制数据转换成Class对象,我们就可以通过Class对象去实例化对象了。
//调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
这行代码就相当于我们在DOS窗口输入的javac命令
ps:本文部分示例来自李刚老师《疯狂java讲义》一书