类加载
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
提供了更大的灵活性,增加了更多的可能性
类加载器深入剖析
Java虚拟机与程序的生命周期在如下几种情况下,Java虚拟机将结束生命周期。
- 1、执行了System.exit()方法
- 2、程序正常执行结束
- 3、程序在执行过程中遇到了异常或错误而异常终止
- 4、由于操作系统出现错误而导致Java虚拟机进程终止
类的加载、连接与初始化
![](https://img.haomeiwen.com/i13587608/593a896a3776e596.png)
- 1、加载:查找并加载类的二进制数据
- 2、连接
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
-
3、初始化:为类的静态变量赋予正确的初始值
- 4、使用:如创建类的对象,调用类的方法
- 5、卸载:将驻留在内存里的类的数据结构销毁掉,卸载后不能再使用该类创建对象了
Java程序对类的使用方式可分为两种
- 1、主动使用
- 2、被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
主动使用(七种)
- 1、创建类的实例
- 2、访问某个类或接口的静态变量,或者对该静态变量赋值
- 3、调用该类的静态方法
- 4、反射(如Class.forName(“com.yibo.Test”))
- 5、初始化一个类的子类
- 6、Java虚拟机启动时被标为启动类的类(包含main方法的类)
- 7、JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle 实例的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化。
被动使用
除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化(可能会加载和连接但不会初始化)
类的加载
-
1、类的加载指的是将类的
.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象(规范并没有说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中),用来封装类在方法区内的数据结构。 -
2、加载
.class
文件的方式- 1、从本地系统中直接加载
- 2、通过网络下载
.class
文件(URLClassLoader) - 3、从zip,jar等归档文件中加载
.class
文件 - 4、从专有数据库中提取
.class
文件 - 5、将Java源文件动态编译为
.class
-
3、类的加载最终产品是位于内存中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区的数据结构的接口
案例1:
- 对于静态字段来说,只有直接定义了该字段的类才会被初始化
- 当一个类在初始化时,要求其父类全部都已经初始化完毕了
/**
* @Description:
* 对于静态字段来说,只有直接定义了该字段的类才会被初始化
* 当一个类在初始化时,要求其父类全部都已经初始化完毕了
* -XX:+TraceClassLoading 用于追踪类的加载信息并打印出来
*
* JVM参数
* 三种配置参数情况:
* -XX:+<option> :开启option选项
* -XX:-<option> :关闭option选项
* -XX:<option>=<value> :将option选项的值设置为value
*/
public class MyTest1 {
public static void main(String[] args) {
//子类类名.父类静态变量
// System.out.println(MyChild1.str);
//子类类名.子类静态变量
System.out.println(MyChild1.str2);
}
}
class MyParent1{
public static String str = "hello world";
static{
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "welcome";
static{
System.out.println("MyChild1 static block");
}
}
案例2:
- 常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中
- 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量类的初始化
/**
* @Description:
* 常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中
* 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量类的初始化
* 注意:这里指的是将常量存放到了MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了
* 甚至,我们可以将MyParent2的class文件删除
*
* 助记符:
* ldc:表示将int、float和String类型的常量值从常量池中推送至栈顶
* bipush:表示将单字节(-128 - 127)的常量值推送至栈顶
* sipush:表示将一个短整型常量值(-32768 - 32767)的常量值推送至栈顶
* iconst_1:表示将int类型的1推送至栈顶 (iconst_m1 - iconst_5)
*/
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
System.out.println(MyParent2.sh);
System.out.println(MyParent2.i);
System.out.println(MyParent2.in);
}
}
class MyParent2{
public static final String str = "hello world";
public static final short sh = 7;
public static final int i = 1;
public static final int in = 128;
static{
System.out.println("MyParent2 static block");
}
}
通过反编译可以看到更直观的信息
![](https://img.haomeiwen.com/i13587608/9a8fd218030f36f2.png)
案例3:
- 当一个常量的值在编译期不能确定时,那么其值就不会被放到调用类的常量池中
- 这时在程序运行,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
/**
* @Description:
* 当一个常量的值在编译期不能确定时,那么其值就不会被放到调用类的常量池中
* 这时在程序运行,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
*/
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent3 static code");
}
}
案例4:
/**
* @Description:
* 对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为[Lcom.yibo.jvm.classloader.MyParent4这种形式
* 动态生成的类型,其父类型就是Object
* 对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降低一个维度后的类型
*
* 助记符:
* anewarray:表示创建一个引用类似的(类、接口、数组)数组,并将其引用值压入栈顶
* newarray:表示创建一个指定的基本类型(int、float、char等)的数组,并将其引用值压入栈顶
*
*/
public class MyTest4 {
public static void main(String[] args) {
// MyParent4 myParent4 = new MyParent4();
MyParent4[] myParent4s = new MyParent4[1];
System.out.println(myParent4s.getClass());
System.out.println(myParent4s.getClass().getSuperclass());
MyParent4[][] myParent4s1 = new MyParent4[1][1];
System.out.println(myParent4s1.getClass());
System.out.println(myParent4s1.getClass().getSuperclass());
int[] arr = new int[1];
System.out.println(arr.getClass());
System.out.println(arr.getClass().getSuperclass());
}
}
class MyParent4{
static {
System.out.println("MyParent4 static block");
}
}
完整流程:
![](https://img.haomeiwen.com/i13587608/2face73305834ede.png)
- 1、加载:就是把二进制形式的java类型class文件读入java虚拟机中。
- 2、验证:
- A、准备:为类变量分配内存,设置默认值,但是在到达初始化之前,类变量都是进行的默认初始化。
- B、解析:解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
- 3、初始化:为类变量进行显示初始化。
- 4、类的实例化:
- A、为新的对象分配内存。
- B、为实例变量进行默认初始化。
- C、为实例变量进行显示初始化。
- D、java编译器为它编译的每一个类都至少生成一个实例初始化方法,在java的class文件中,这个实例初始化方法被称为"<init>",针对源代码中每一个类的构造方法,java编译器都产生一个<init>方法。
有两种类型的类加载器:
Java虚拟机自带的加载器
- 根类加载器(Bootstrap)(使用 C++编写,程序员无法在 Java 代码中获得该类)
- 扩展类加载器(Extension),使用 Java 代码实现
- 系统类加载器(应用加载器)(System),使用 Java 代码实现
用户自定义的类加载器
-
java.lang.ClassLoader的子类(用户自定义的类加载器都是java.lang.ClassLoader 的子类)
-
用户可以定制类的加载
-
类加载器并不需要等到某个类被“首次主动使用”时再加载它。
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
- 如果这个类一直没有被程序主动使用,那么类加载器就不会报告
类的验证
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
类的验证的内容
* 类文件的结构检查:确保类文件遵从Java类文件的固定格式。
* 语义检查:确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖
* 字节码验证:确保字节码流可以被Java虚拟机安全地执行。字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
* 二进制兼容性的验证
类的准备
- 在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。
类的解析
- 在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用。
类的初始化
-
1、在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:
- A、在静态变量的声明处进行初始化;
- B、在静态代码块中进行初始化。
-
2、静态变量的声明语句,以及静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。
类的初始化的步骤
- 1、假如这个类还没有被加载和连接,那就先进行加载和连接。
- 2、加入类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。
- 3、假如类中存在初始化语句,那就依次执行这些初始化语句。
类的初始化时机
- 1、当Java虚拟机初始化一个类时,要求它的所有父类都系应被初始化,但是这条规则并不适用于接口。
- 2、在初始化一个类时,并不会先初始化它所实现的接口。
- 3、初始化一个接口时,并不会先初始化它的父接口。
- 4、因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
- 5、程序中对子类的“主动使用”会导致父类被初始化;但对父类的“主动”使用并不会导致子类初始化(不可能说生成一个 Object 类的对象就导致系统中所有的子类都会被初始化)
- 6、只有当程序访问的静态变量或静态方法确实在 当前类或当前接口中定义 时,才可以认为是对类或接口的主动使用。
- 7、调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
类加载器
类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。
类加载器ClassLoader,它是一个抽象类,ClassLoader的具体实例负责把java字节码读取到JVM当中,ClassLoader还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader的负责整个类装载流程中的“加载”阶段。
有两种类型的类加载器:
Java虚拟机自带的加载器
- 引导类加载器(BootStrap ClassLoader):它用来加载 Java 的核心库,使用 C++编写,程序员无法在 Java 代码中获得该类,并且不继承自 java.lang.ClassLoader。
- 扩展类加载器(Extension ClassLoader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(App ClassLoader/System ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
用户自定义的类加载器
- java.lang.ClassLoader的子类(用户自定义的类加载器都是java.lang.ClassLoader 的子类)
- 用户可以定制类的加载
类加载的双亲委派机制
双亲委派模式工作原理
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
![](https://img.haomeiwen.com/i13587608/d8ff534327f4747a.png)
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
ClassLoader的重要方法:
载入并返回一个类。
public Class<?> loadClass(String name) throws ClassNotFoundException
定义一个类,该方法不公开被调用。
protected final Class<?> defineClass(byte[] b, int off, int len)
查找类,loadClass的回调方法
protected Class<?> findClass(String name) throws ClassNotFoundException
查找已经加载的类。
protected final Class<?> findLoadedClass(String name)
每个ClassLoader都有另外一个ClassLoader作为父ClassLoader,BootStrap Classloader除外,它没有父Classloader。
ClassLoader加载机制如下:
![](https://img.haomeiwen.com/i13587608/91f074e0e74f1c1f.png)
自下向上检查类是否被加载,一般情况下,首先从App ClassLoader中调用findLoadedClass方法查看是否已经加载,如果没有加载,则会交给父类,Extension ClassLoader去查看是否加载,还没加载,则再调用其父类,BootstrapClassLoader查看是否已经加载,如果仍然没有,自顶向下尝试加载类,那么从 Bootstrap ClassLoader到 App ClassLoader依次尝试加载。
值得注意的是即使两个类来源于相同的class文件,如果使用不同的类加载器加载,加载后的对象是完全不同的,这个不同反应在对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。
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 t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
从代码上可以看出,首先查看这个类是否被加载,如果没有则调用父类的loadClass方法,直到BootstrapClassLoader(没有父类),我们把这个过程叫做双亲委派,或父类委托。
创建用户自定义的类加载器
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public MyClassLoader(String classLoaderName){
super();//将系统类加载器作为该类加载器的父加载器
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent,String classLoaderName){
super(parent);//显示指定该类加载器的父加载器
this.classLoaderName = classLoaderName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "MyTest15{" +
"classLoaderName='" + classLoaderName + '\'' +
'}';
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("findClass invoked:"+className);
System.out.println("class loader name:"+this.classLoaderName);
byte[] data = this.loadClassData(className);
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream bos = null;
try {
this.path = path.replace(".","/");
this.classLoaderName = classLoaderName.replace(".","/");
inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
bos = new ByteArrayOutputStream();
int ch;
while((ch = inputStream.read()) != -1){
bos.write(ch);
}
data = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz1:" + clazz1);
Object object1 = clazz1.newInstance();
System.out.println(object1);
System.out.println(myClassLoader1.getClass().getClassLoader());
System.out.println("------------------");
MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz2:" + clazz2);
Object object2 = clazz2.newInstance();
System.out.println(object2);
System.out.println("------------------");
MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz3:" + clazz3);
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
类加载器的命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
运行时包
由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一运行时包,不仅要看他们的包名是否相同,还要看定义类加载器是都相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的可见成员。假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。
不同类加载器的命名空间关系
- 同一个命名空间内的类是相互可见的。
- 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
- 由父加载器加载的类不能看见子加载器加载的类。
- 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
- 用反射可以访问
类的卸载
- 1、当一个类被加载、连接和初始化后,它的生命周期就开始了。当代表这个类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,这个类在方法区内的数据也会被卸载,从而结束这个类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
- 2、由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
- 3、由用户自定义的类加载器所加载的类是可以被卸载的。
双亲模式的问题
顶层ClassLoader,无法加载底层ClassLoader的类
Java框架(rt.jar)如何加载应用的类?
比如:javax.xml.parsers包中定义了xml解析的类接口
Service Provider Interface SPI 位于rt.jar
即接口在启动ClassLoader中。
而SPI的实现类,在AppLoader。
这样就无法用BootstrapClassLoader去加载SPI的实现类。
解决
JDK中提供了一个方法:
- 1、Thread. setContextClassLoader()
用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题;
基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例。
获得ClassLoader的途径
- 1、获取当前类ClassLoader:clazz.getClassLoader()
- 2、获取当前线程上下文的ClassLoader: Thread.currentThread().getContextClassLoader()
- 3、获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
- 4、获取调用者的ClassLoader:DriverManager.getCallerClassLoader()
Jar Hell 问题以及解决方案
- 当一个类或一个资源文件存在多个jar中,就会存在jar hell问题
- 可以通过以下代码来诊断问题:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String resouceName = "java/lang/String.class";
Enumeration<URL> resources;
try {
resources = cl.getResources(resouceName);
while (resources.hasMoreElements()) {
URL nextElement = resources.nextElement();
System.out.println(nextElement);
}
} catch (IOException e) {
}
双亲模式的破坏
双亲模式是默认的模式,但不是必须这么做;
Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent;
OSGi的ClassLoader形成网状结构,根据需要自由加载Class。
参考:
https://blog.csdn.net/weixin_43907332/article/details/86625277
https://www.cnblogs.com/luckgood/p/8981508.html
网友评论