类加载机制
类加载、连接和初始化
当程序主动使用某个类时,如果给类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤对该类进行初始化。
类加载
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类加载由类加载器完成,类加载器由JVM提供。通过不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
- 从本地文件系统加载class文件。
- 从jar包加载class文件。
- 通过网络加载class文件。
- 把一个Java源文件动态编译,并执行加载。
类连接
当类被加载之后,系统为之生成一个Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下三个阶段:
- 验证:用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
- 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
- 解析:将类的二进制数据中的符号引用替换成直接引用。
类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要是对类变量进行初始化。在Java类中对类变量指定初始值有两种方式:1、声明类变量时指定初始值;2、使用静态初始化块为类变量指定初始值。
JVM初始化一个类包含如下几个步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
类的初始化时机
当Java程序首次通过下面6种方式使用某个类或接口时,系统就会初始化该类或接口。
- 创建类的实例。
- 调用某个累的类方法。
- 访问某个类或接口的类变量。
- 利用反射方式创建某个类或接口的Class对象。
- 初始化某个类的子类。
- 直接使用java.exe命令来运行某个主类。
对于一个final型的类变量,如果该类变量的值在编译时可以确定下来,那么这个类变量相当于“宏变量”,Javac命令编译后直接把这个类变量出现的地方替换成它的值因此程序使用该类变量时,不会导致该类的初始化。
类加载器
- Bootstrap ClassLoader:根类加载器。
- Extention ClassLoader:扩展类加载器。
- System ClassLoader:系统类加载器。
类加载机制
JVM类加载机制主要有如下三种:
- 全盘委托。当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责加载,除非显式使用另一个类加载器加载。
- 父类委托。先让父类加载器试图加载该Class,只有父类加载器无法加载该类时才会尝试从自己的类路径中加载该类。
- 缓存机制。缓存机制将会保证所有加载过的Class都会缓存,当程序中需要某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。
注意:这里类加载器的父子关系不是继承上的父子关系。
下面程序示范了访问JVM的类加载器:
/**
* Created by SqMax on 2018/5/12.
*/
public class ClassLoaderTest {
public static void main(String[] args) throws IOException {
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
Enumeration<URL> eml = systemLoader.getResources("");
while (eml.hasMoreElements()) {
System.out.println(eml.nextElement());
}
ClassLoader extendLoader = systemLoader.getParent();
System.out.println("扩展类加载器:" + extendLoader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent:" + extendLoader.getParent());
}
}
运行结果如下:
系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
file:/F:/IDEA_workspace/javaSE/target/classes/
扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
扩展类加载器的加载路径:E:\Java\jdk1.8.0_161\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
扩展类加载器的parent:null
可以看到,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是:E:\Java\jdk1.8.0_161\jre\lib\ext,但扩展类的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承自ClassLoader抽象类。扩展类加载器的父类加载器是根类加载器,只是根类加载器不是Java实现的。
创建并使用自定义的类加载器
JVM中除了根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类来自定义类加载器。
ClassLoader类有如下两个关键方法:
public Class<?> loadClass(String name)
;protected Class<?> findClass(String name)
;
其中loadClass()的执行步骤如下:
- 用findLoadedClass(String)来检查是否已经加载e类,如果已经加载则直接返回。
- 在分类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器加载。
- 调用findClass(String)方法查找类。
我们可以选择重写上面任一个方法自定义类加载器,但重写loadClass(String name)
比较复杂,这里我们选择重写findClass(String)
方法来自定义类加载器。
注意:在ClassLoader里还有一个核心方法:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
。
它负责将指定类的字节码文件读入字节数组,并把它转换为Class对象,我们待会可以用到。
下面自定义一个类加载器,他可以在加载类之前先编译该类,通过该类加载器可以直接运行Java源文件。
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)) {
int r = fin.read(raw);
if (r != len) {
throw new IOException("can't read all file:" + r + "!=" + len);
}
return raw;
}
}
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader is compiling " + javaFile + "......");
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try {
p.waitFor();
} catch (InterruptedException ie) {
System.out.println(ie);
}
int ret = p.exitValue();
return ret == 0;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
String fileSub = name.replace(".", "/");
String javaFileName = fileSub + ".java";
String classFileName = fileSub + ".class";
File javaFile = new File(javaFileName);
File classFile = new File(classFileName);
if (javaFile.exists() && (!classFile.exists())
|| javaFile.lastModified() > classFile.lastModified()) {
try {
//这里用自定义的compile()方法编译源文件
if (!compile(javaFileName) || !classFile.exists()) {}
} catch (IOException ex) {
ex.printStackTrace();
}
}
if (classFile.exists()) {
try {
byte[] raw = getBytes(classFileName);
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException ie) {
ie.printStackTrace();
}
}
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("lack the goal class,according to the format below:");
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);
}
}
首先用javac
命令将上面类加载器源文件编译为.class
文件,然后就可以使用这个类加载器来直接运行一个java源文件。
下面一个测试的源文件:
public class Hello {
public static void main(String[] args) {
for (String arg : args) {
System.out.println("the arg: " + arg);
}
}
}
不用编译上面Hello.java文件,直接使用如下命令直接运行Hello程序:
java CompileClassLoader Hello sqmax
运行结果如下:
CompileClassLoader is compiling Hello.java......
the arg:sqmax