Java的类加载,为什么org.test.TestLoadedClass cannot be cast to org.test.TestLoadedClass?

1. java类加载是在干什么

众所周知java是一门解释型语言,其运行的最基本环境就是jre,在jre内部有一个核心虚拟机称为jvm,其功能就是将我们的class文件中的内容解释为机器码来让计算机运行。

class文件是由我们编写的.java文件编译而来的字节码文件,其后缀为.class。类加载就是将.class文件从我们的计算机硬盘解析到jvm内存空间的过程。

当一个类被加载完成后,其类信息将被存储到jvm内存区域的方法区中,类信息包括类的成员属性、成员方法、构造方法等等。

2. 类什么时候会被加载

注:本文所介绍的类加载均是在hot spot虚拟机环境下的类加载,其他类型的java虚拟机本文不具备参考意义

jvm会在类被使用的时候,具体场景如下:

  1. 实例化对象时
TestClassLoadObject tclo = new TestClassLoadObject();
  1. 通过类名调用静态变量或方法时
TestClassLoadObject.TEST_FIELD_ONE; //调用静态属性时
TestClassLoadObject.TEST_METHOD_ONE; //调用静态方法时
  1. 调用Class.forName("类名")方法时
Class.forName("org.test.TestClassLoadObject");
  1. 调用类.class时
org.test.TestClassLoadObject.class;
  1. jvm启动时的入口类,我们运行main方法的类会被jvm自动加载

  2. 对类进行反射调用时,如果类还没有被初始化,则需要先进行类的初始化

jvm类不会被加载的场景:

  1. 仅仅import了类,而没有使用该类
import org.test.TestClassLoadObject;
  1. 声明了该类的变量或常量,而没有实例化对象
TestClassLoadObject tclo = null;

3. 类是怎么被加载的

类加载具体会经历5个阶段,从前到后分别为加载、验证、准备、解析、初始化,通常将中间三个阶段称作连接(验证、准备、解析)。各阶段虚拟机将进行如下工作:

3.1. 加载

在此阶段jvm将我们静态的.class文件加载到jvm内存中,并在方法区中转换为运行时的数据结构,然后在堆中创建一个代表该类的class对象作为方法区中该类的数据的访问入口。

简单理解这个阶段就是jvm从二进制字节流中读取数据,转换为class对象,二进制字节流包括:
1. 从zip包中读取,包括jar、war等格式
2. 从网络中获取
3. 使用动态代理技术,动态生成代理类的二进制字节流
4. 由其他文件生成,例如JSP文件生成的class类
以及其他种种流的方式加载class文件

3.2. 验证

验证阶段主要分为四部分的校验:

  • 文件格式验证:验证class文件的版本是否符合当前jvm加载范围。验证class文件的格式是否符合《java虚拟机规范》的全部约束要求,保证加载该class文件后不会对jvm自身的安全造成危害。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
  • 字节码验证:通过数据流和控制流分析,确保被校验类的方法在运行时不会做出危害虚拟机安全的行为。比如会不会在运行成类中出现了一条不存在的指令。
  • 符号引用验证:校验器还将进符号引用的验证。比如校验类的方法中是否调用了另一个类根本不存在的方法。

3.3. 准备

经过校验没问题的类,jvm会在此阶段为其内存空间,并对该类的变量字段赋上默认值。常量字段在此阶段则会直接赋上终值。

例如我们的类中存在一个名为version的变量,其声明如下

public static Integer version = 10;

则在准备阶段,该字段将先被变为如下值。

public static Integer version = null;

引用类型的默认值为null,基本类型的默认值基本都为0

如果该类被修饰为final类型,则直接被赋为声明的值,如下例子中的name属性会在此阶段被直接赋值为“码小飞”。

public static final String name = "码小飞";

3.4. 解析

在此阶段将会把类中的符号引用变为直接引用。

简单理解就是我们代码中使用了另一个类中的属性或方法,那么此阶段就是将我们编写的代码中的静态的引用替换为jvm内存中具体的一块加载好的内存地址,用来程序在执行到此引用的时候能够直接调用对应的内存地址来获取结果。

3.5. 初始化

在此阶段,jvm会执行类的初始化代码,将静态属性的值赋为我们代码中定义的值,属性赋值的顺序为从上到下赋值。赋值完毕后执行静态代码块中的代码。

3.6. 双亲委派模型

在java的世界中,类都是被类加载器加载到内存中的,而类加载器又是以树形结构存在在jvm中的。

每一个类加载器都有一个父加载器,当一个类需要被加载时,当前类加载器总会先委托父加载器进行加载,当父加载器无法加载该类时,才会调用当前类加载器对类进行加载。当所有类加载器都无法加载此类的时候会抛出ClassNotFoundException。这种类加载的机制又称为类加载器的双亲委派模型

java中类加载器的树形结构如下图所示:

  • BootStrap ClassLoader
    它负责将存放在%JAVA_HOME%\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是JVM识别的类库加载到JVM内存中。它仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载。它是由C++语言实现的,无法被Java程序直接引用。
  • Extension ClassLoader
    它负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它由sun.misc.Launcher.ExtClassLoader实现,开发者可以直接使用扩展类加载器。
  • Application ClassLoader
    它负责加载用户类路径(ClassPath)上所指定的类库。由于它是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它由sun.misc.Launcher.AppClassLoader来实现,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

不同的类加载器负责加载不同目录下的类,我们也可以自定义类加载器来实现加载不在classpath下的类。

java为什么使用双亲委派模型呢?

双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。
一个非常明显的目的就是保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖。
例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
java通过双亲委派机制来保护一些系统类不会被第三方的类加载器加载而导致虚拟机崩溃。因为即使有第三方类加载器加载了Object类生成了新的class对象,但是此对象也不会替换启动类加载器加载生成的class对象,而是会在jvm中生成两个class对象。

4. 为什么会出现标题的错误

了解了双亲委派模型我们就可以理解了为什么会出现标题的错误
org.test.TestLoadedClass cannot be cast to org.test.TestLoadedClass

从错误上看,是TestLoadedClass类型的对象希望转换为TestLoadedClassd对象时报错了。

问题出现原因:
出现这个问题只有一个原因,那就是在jvm中加载了两个同名的class对象(同名是指类的全限定名都一致),然后通过其中一个class对象创建了一个实例对象,然后将这个实例对象转换成了另一个类的实例对象,就会出现这个错误。
尽管这两个class对象名称和结构都一致,但是jvm仍然认为这是两个不同的类文件加载来的

如何在一个jvm中出现两个相同名称的class对象呢?其实很简单,我们只需要自定义一个类加载器,然后将其父加载器设置为null。然后通过这个类加载器去加载在jvm中已经存在的类。由于我们自定义的类加载器并没有父加载器,因此双亲委派的机制对于此时的类加载并不生效。这样我们自定义的类加载器就可以顺利地将该类再次加载到jvm中,这样我们的jvm就会存在两个名称相同的class对象了。

以下是问题复现的伪代码演示:

CustomLoader customLoader = new CustomLoader();
Class<?> clazz = customLoader.loadClass("org.test.TestLoadedClass");
Object o = clazz.newInstance();
//当程序执行到这一行时就会出现上述错误。因为org.test.TestLoadedClass会被Application ClassLoader加载,而o对象是通过CustomLoader类加载器加载并创建的
org.test.TestLoadedClass convertedObject = (org.test.TestLoadedClass) o; 

通过这个问题我们可以推断出,一个jvm内部是可以同时存在多个同名class对象的,并且这些class对象并不能互相转换,尽管其属性和成员都一样。出现这种问题的原因多半是因为我们在定义和使用类加载器的时候没有注意目标类已经被加载了,然后又使用自定义类加载器再次对目标类进行了加载,然后通过新加载的class对象去实例化目标类对象,并进行了强制类型转换。

因此当我们自定义和使用类加载器时一定要注意判断加载类的时机是否合理,加载完后实例化的对象在进行类型转换时是否正确。

还有一点需要注意当我们的类被某个类加载器加载后,那么该类内涉及的其他类通常也会由该类加载器加载(被启动类加载器和拓展类加载器加载的类除外),这是由双亲委派的机制决定的。因此当我们对Object对象进行强制类型转换时可以通过这个技巧来判断这次强转是否正确合理。

本人才疏学浅,文中有写的不对或不好的地方还请大家及时批评指正。

最后为同事家种植的水果打一个广告,“砀山酥梨 现摘现发 清甜可口 润肺止咳  ”,本人已经试吃过梨子酥脆可口,煮粥、熬水和生吃都是不错的选择,秋天也是吃梨的好时节。如果大家有需要可以来本人的微店购买

砀山酥梨 现摘现发 清甜可口 润肺止咳    https://k.youshop10.com/OAHgc1Sx

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码小飞飞飞飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值