文章中的阐述都是经过再三斟酌后编辑的,需要细嚼慢咽才能彻底理解~
在Java代码中,类的加载、链接、初始化过程都是在程序运行期间完成的。这样为开发程序就提供了更大的灵活性,增加了更多的可能性。
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化三个步骤来对该类进行初始化,如果不出意外,JVM会连续完成这三个步骤,所以有时这三个步骤也被称为类的加载或类的初始化。
对类的主动使用大致有7种:
1.创建类的实例,也就是new一个对象
2.访问类或者接口的静态变量,或对该变量赋值
3.调用类的静态方法
4.反射(Class.forName("xxx.xxx.Xxx"))
5.初始化一个类的子类
6.JAVA虚拟机标注为启动类的类
7.JDK1.7提供的动态语言支持
1、类的加载过程
1.1 加载:
查找并加载类文件的二进制数据,从类文件中提取出类型信息并放在方法区中,然后为之创建一个java.lang.Class对象。这个java.lang.Class对象犹如类型的一面镜子,可以通过它快速的找到类型的各种数据。
类型信息:类型信息是由类加载器在类加载时从类文件中提取出来的
这个回答里面详细收录了JVM需要保存的.class文件数据结构(元数据)。特别推荐看这个回答最后举例代码怎样调用方法区的信息,很有用!
java.lang.Class对象:参照OpenJDK1.8的源码,Class对象应该存在于Heap中。
这个回答直接从 openJDK 1.8 中关于虚拟机实现的源码入手,分析了 Class 对象分配内存的过程,最后指出 Class 确实是分配在 Heap 上。openJDK 1.8 这部分源码是用 C/C++ 写的,初学者慎入!(有理有据,代码说话,但我看不懂…)
类的查找即类加载的方式:
1.在本地系统磁盘中直接加载
2.在 zip、jar等归档文件中加载
3.将Java源文件动态编译为.class文件(动态代理,jsp文件)
4.通过网络下载.class文件
5.在专有数据库中加载.class文件
JVM规范允许类加载器在预料某各类将要被使用时就预先加载它,如果在加载的过程中遇到.class文件缺失或存在错误,类加载器必须在程序首次主动使用的时候才报告错误(LinkageError错误),若果这个类一直没有被程序使用,那么类加载器就不会报告错误。
1.2 链接:
链接又分为三个阶段:
1、验证:确保被加载的类的正确性,完整性,安全性,以及和其他类协调一致。主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
2、准备:为类的静态变量分配内存,并设置默认的初始值(例如 int 类型的变量初值为 0)
3、解析:将二进制类数据中的符号引用替换为直接引用。
文件格式验证:主要验证字节流是否符合class文件规范,并且能被当前虚拟机加载处理。比如主,次版本号是否在当前虚拟机处理范围之内,魔数,常量池中是否又不被支持的常量类型。指向常量的索引值是否存在不存在的常量和不符合类型的常量。
元数据验证:对字节码的描述信息进行语义分析,是否符合java语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要针对元数据验证后对方法体的验证。保证类方法在运行时不会出现危害。
符号引用验证:主要针对符号引用转为直接引用的时候,会延伸到第三的解析阶段,确定访问类型等涉及到引用的情况。主要是保证引用一定会被访问到,不出现类等无法访问的情况。
符号引用:主要是以一组符号来描述所引用的目标,符号可以是任何字面形式的字面量,只要不会出现在冲突能够定位到就行。布局和内存无关。
直接引用:是指向目标的指针、偏移量或者是能够直接定位的句柄。该引用和内存中的布局有关,并且一定加载进来的。
1.3 初始化:为类的静态变量赋予给定的初始值,执行静态代码块。
除此之外,下面的情况需要特殊指出
对于一个final类型的静态变量,也就是常量。如果该变量的值在编译期间就可以确定下来,那么这个变量相当于“宏变量”,java编译器会在编译时直接把这个变量出现的地方替换为它的值,加载的时候这个变量会被放到调用它的那个方法所在的类的常量池中,因此即使程序使用该静态变量,也不会初始化定义它的类。
反之,如果final定义的静态变量的值不能在编译时确定下来,必须得等到运行时才可以确定该变量的值,如果通过定义他的类来访这个静态变量时,则该类会被初始化。
1.4 使用:
1.5 卸载:
二、类加载器和双亲委派机制
类加载的过程主要是由类加载器来完成的,Java里有如下几种类加载器:
引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,例如:rt.jar、charsets.jar等。
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的jar包。
应用程序类加载器:负责加载位于ClassPath路径下的的类包,主要就是加载你自己写的那些类包。
自定义类加载器:负责加载用户自定义路径下的类包。
类加载器演示:
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESedeKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
ClassLoader parent = classLoader.getParent();
ClassLoader parent1 = parent.getParent();
System.out.println("appClassLoader=" + classLoader);
System.out.println("extClassLoader=" + parent);
System.out.println("bootstrapClassLoader=" + parent1);
System.out.println();
System.out.println("启动类加载器加载的路径:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL u : urls){
System.out.println(u.getPath());
}
System.out.println("扩展类加载器加载的路径:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("应用类加载加载的路径:");
System.out.println(System.getProperty("java.class.path"));
}
类加载器初始化的过程:
sun.misc.Launcher类,初始化过程使用了单例模式设计,保证一个JVM虚拟机只有一个sun.misc.Launcher实例。在该类构造方法内部,创建了两个类加载器
sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)
JVM默认使用Launcher类的getClassLoader()方法返回的AppClassLoader类加载器来加载我们的应用程序。
双亲委派机制:
这里类加载的双亲委派机制就是,加载某个类的时候,先委托父加载器寻找目标类,找不到再委托上次类加载器加载,如果所有父加载器在他自己加载路径下都找不到目标类,则在自己的加载路径下查找并加载目标类。
简单来说就是,先让父加载器加载,不行再由自己加载。
为什么要设计双亲委派机制:
沙箱安全机制:自己写的java.lang.String.class类不会被加载,防止核心API被随意篡改。
避免类的重复加载:当父加载器已经加载该类时,子类加载器就没有必要在加载一次,保证被加载类的唯一性。
全盘负责委托机制:
是指当一个ClassLoader装载一个类时,除非显示的指定使用另外一个CLassLoader,该类所依赖以及引用的类也由这个ClassLoader来装载。
自定义类加载器的示例;
自定义类加载器只需要继承java.lang.ClassLoader类(该类有两个核心方法,loadClass(String,Boolean),实现了双亲委派机制,findClass()默认实现是空方法)自定义类加载器要重新findClass()方法。
public class TestClassLoader {
public static void main(String[] args) {
try {
MyClassLoader loader = new MyClassLoader("D:/test");
Class clazz = loader.loadClass("com.zpc.classLoader.TestUser");
Object o = clazz.newInstance();
Method addUser = clazz.getDeclaredMethod("addUser", null);
addUser.invoke(o,null);
System.out.println(clazz.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private byte[] loadByte(String name) throws IOException {
name = name.replace("\\.", "/");
FileInputStream fs = new FileInputStream(classPath + "/" + name + ".class");
int len = fs.available();
byte[] b = new byte[len];
fs.read(b);
fs.close();
return b;
}
}
}
打破双亲委派机制:
重写loadClass方法,实现自己的逻辑,不委派给双亲加载。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public static void main(String[] args) {
try {
MyClassLoader loader = new MyClassLoader("D:/test");
// 自己写一个String类
Class clazz = loader.loadClass("com.zpc.classLoader.String");
System.out.println(clazz.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
Tomcat打破双亲委派机制:
tomcat是个web容器,他需要解决的问题有:
1.部署两个应用程序,不同的应用程序可能会依赖两个不同版本第三方类库,不能要求同一类库在同一个服务器只有一份,要保证每个应用程序的类库都是独立的,保证相互隔离。
2.部署在同一个web容器中相同类库相同版本可以共享,否则,如果有10个应用程序,那么要加载10个相同类库到虚拟机。
3.web容器也有自己要依赖的库,不能与应用程序类库混淆。基于安全考虑,应该让应用程序的类库与容器的类库隔离开。
4.web容器要支持jsp的修改,jsp文件最终要编译成class文件才能才虚拟机中运行,程序运行中修改jsp已经是司空见惯的事,web容器需要支持修改jsp后不重启。