java虚拟机classloader_深入Java虚拟机(番外篇)ClassLoader 初探

本篇文章是在深入Java虚拟机(四)的基础上产生的一些想法,从另一个方面研究下类加载问题。

面向接口编程

在项目工程目录定义了一个获取钱包余额的接口,而作为普通人会返回50大洋,供你吃喝玩乐。

public interface Pocket{

int getBalance();

}

public class NormalUser implements Pocket{

@Override

public int getBalance(){

return 50;

}

}

复制代码

我在桌面编译了另一个版本的NormalUser,给我的钱包提提额度。

public class NormalUser implements Pocket{

@Override

public int getBalance(){

return 50000;

}

}

复制代码

现在我想改善下生活品质,于是我做了下面的事情:

在项目中自定义一个类加载器:

public class LocalClassLoader extends ClassLoader{

@Override

protected Class> findClass(String name) throws ClassNotFoundException {

String path = "/Users/lijie/Desktop/NormalUser.class";

try {

FileInputStream ins = new FileInputStream(path);

int size = ins.available();

byte[] clazzBytes = new byte[size];

if (ins.read(clazzBytes) > 0) {

return defineClass("hua.lee.classloader.NormalUser", clazzBytes, 0, size);

} else {

throw new ClassNotFoundException();

}

} catch (IOException e) {

e.printStackTrace();

}

throw new ClassNotFoundException();

}

}

复制代码

LocalClassLoader写的比较死板,但是挣钱的思路是有了的。。。。

LocalClassLoader重写了 findClass方法,没有重写loadClass,这样双亲委托机制还有效。对于系统中查找不到的类型,都会走到这里,而在findClass方法中,我就会把桌面的NormalUser加载进来。

我们看下测试代码:

public class ClassLoaderTest{

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException{

Class> clazz = Class.forName("hua.lee.classloader.NormalUser");

System.out.println("系统自带 ClassLoader=" + clazz.getClassLoader());

Pocket pocket = (Pocket) clazz.newInstance();

System.out.println("系统自带:"+pocket.getBalance());

//没有重写 loadClass,维持双亲委派机制

LocalClassLoader lcl = new LocalClassLoader();

//我们加个后缀名,这样双亲委托查找失败了,就调用LocalClassLoader.findClass 方法加载

//其实 findClass 方法还是加载 NormalUser类,只是换了个位置和方式:

//defineClass("hua.lee.classloader.NormalUser", clazzBytes, 0, size);

Class> clazzOut02 = lcl.loadClass("hua.lee.classloader.NormalUser_Temp");

//双亲委托机制先检查当前类加载器中有没有这个类的 Class 实例,没有的话再搜寻父类,

//查找到 hua.lee.classloader.NormalUser 的 Class 实例,就返回该对象

Class> clazzOut01 = lcl.loadClass("hua.lee.classloader.NormalUser");

System.out.println("clazzOut01 自定义 ClassLoader=" + clazzOut01.getClassLoader());

pocket = (Pocket) clazzOut01.newInstance();

System.out.println("clazzOut01 自定义加载器获取余额:"+pocket.getBalance());

System.out.println("clazzOut02 自定义 ClassLoader=" + clazzOut02.getClassLoader());

pocket = (Pocket) clazzOut02.newInstance();

System.out.println("clazzOut02 自定义加载器获取余额:"+pocket.getBalance());

System.out.println(clazz==clazzOut01);

System.out.println(clazz==clazzOut02);

System.out.println(clazzOut01==clazzOut02);

}

复制代码

控制台输出是这样的:

系统自带 ClassLoader=sun.misc.Launcher$AppClassLoader@18b4aac2

系统自带:50

clazzOut01 自定义 ClassLoader=hua.lee.classloader.LocalClassLoader@60e53b93

clazzOut01 自定义加载器获取余额:50000

clazzOut02 自定义 ClassLoader=hua.lee.classloader.LocalClassLoader@60e53b93

clazzOut02 自定义加载器获取余额:50000

false //clazz==clazzOut01

false //clazz==clazzOut02

true //clazzOut01==clazzOut02

复制代码

感觉找到了致富的方向。虚拟机里的加载约束没看到呢.

面向抽象类编程

上面的致富之路好像很顺利,那我们尝试用如下的方式:

public abstract class Pocket{

abstract int getBalance();

}

public class NormalUser extends Pocket{

@Override

public int getBalance(){

return 50;

}

}

复制代码

同样,我在桌面编译了一个富人版本:

public class NormalUser extends Pocket{

@Override

public int getBalance(){

return 50000;

}

}

复制代码

LocalClassLoader和测试代码没变,但是打印结果确实这样的:

系统自带 ClassLoader=sun.misc.Launcher$AppClassLoader@18b4aac2

系统自带:50

clazzOut01 自定义 ClassLoader=hua.lee.classloader.LocalClassLoader@60e53b93

Exception in thread "main" java.lang.AbstractMethodError

at hua.lee.classloader.ClassLoaderTest.main(ClassLoaderTest.java:25)

复制代码

虚拟机在解析抽象类实现和接口实现的时候有什么区别么?

不成熟的推测

首先面向接口中的模式应该是躲过了装载约束的,或者虚拟机设计上是支持接口这么搞的。

既然提示AbstractMethodError,那我把测试代码改一下,不再用抽象类Pocket,直接指向NormalUser:

public class ClassLoaderTest{

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException{

Class> clazz = Class.forName("hua.lee.classloader.NormalUser");

System.out.println("系统自带 ClassLoader=" + clazz.getClassLoader());

//请注意此处⬇️

NormalUser pocket = (NormalUser) clazz.newInstance();

//请注意此处⬆️

System.out.println("系统自带:"+pocket.getBalance());

//没有重写 loadClass,维持双亲委派机制

LocalClassLoader lcl = new LocalClassLoader();

//我们加个后缀名,这样双亲委托查找失败了,就调用LocalClassLoader.findClass 方法加载

//其实 findClass 方法还是加载 NormalUser类,只是换了个位置和方式:

//defineClass("hua.lee.classloader.NormalUser", clazzBytes, 0, size);

Class> clazzOut02 = lcl.loadClass("hua.lee.classloader.NormalUser_Temp");

//双亲委托机制会自动搜寻父类,

//查找到 hua.lee.classloader.NormalUser 的 Class 实例,就返回该对象

Class> clazzOut01 = lcl.loadClass("hua.lee.classloader.NormalUser");

System.out.println("clazzOut01 自定义 ClassLoader=" + clazzOut01.getClassLoader());

pocket = (NormalUser) clazzOut01.newInstance();

System.out.println("clazzOut01 自定义加载器获取余额:"+pocket.getBalance());

System.out.println("clazzOut02 自定义 ClassLoader=" + clazzOut02.getClassLoader());

pocket = (NormalUser) clazzOut02.newInstance();

System.out.println("clazzOut02 自定义加载器获取余额:"+pocket.getBalance());

System.out.println(clazz==clazzOut01);

System.out.println(clazz==clazzOut02);

System.out.println(clazzOut01==clazzOut02);

}

}

复制代码

惊喜的事情来了,输出变成了下面这样:

系统自带 ClassLoader=sun.misc.Launcher$AppClassLoader@18b4aac2

系统自带:50

clazzOut01 自定义 ClassLoader=hua.lee.classloader.LocalClassLoader@60e53b93

Exception in thread "main" java.lang.ClassCastException: hua.lee.classloader.NormalUser cannot be cast to hua.lee.classloader.NormalUser

at hua.lee.classloader.ClassLoaderTest.main(ClassLoaderTest.java:24)

复制代码

这个异常说明了一个问题我们自定义加载进来的hua.lee.classloader.NormalUser不是虚拟机想要的hua.lee.classloader.NormalUser。

第一个问题:虚拟机怎么知道这两个NormalUser不一样的?

和定义类装载器有关系,同一个全限定名的类型,如果不是同一个定义类装载器,虚拟机就认为不是同一个类型。(按照装载约束应该报错的)

啥是定义类装载器?就是真正把 class 文件装载进来的那个ClassLoader

第二个问题:为什么接口可以这样跑呢?

那是因为面向接口实现的版本没有明确指名类型NormalUser,类型转换都是按照Pocket类型执行方法,虚拟机自动查找接口类型的实现。只要是Pocket的实现就行。

如果你把pocket = (Pocket) clazzOut01.newInstance();换成pocket = (NormalUser) clazzOut01.newInstance();,此时,面向接口的版本也会报ClassCastException。

对于虚拟机来说,在接口的实现类,它并不 care 你的实现类的来源(哪个加载器加载的无所谓啦),符合接口定义就可以了。

但是当你指定类型转换的时候,它就要去做类型检查了。

第三个问题:抽象类为什么不行?

首先 YY 一下java.lang.AbstractMethodError:

当Pocket为抽象类时,根据抽象类的定义,要有特定实现类来执行。

而对于pocket = (Pocket) clazzOut01.newInstance();这句话,Pocket的强制类型转换是成功(因为Poket需要执行双亲委派逻辑,大家都是同一个Poket),但是当寻找实现类时就发现这个clazzOut01.newInstance()生出来的对象类型NormalUser不是自己想要的,虚拟机以为没有对应的实现,所以当执行到pocket.getBalance()时,虚拟机意味程序要单纯的执行Pocket的抽象方法,于是报错java.lang.AbstractMethodError。

如果上面的理解了,那么java.lang.ClassCastException也就好理解了:

对于pocket = (NormalUser) clazzOut01.newInstance();这句话,虚拟机会把clazzOut01.newInstance()产生的对象强制转换为NormalUser,到这里就回到第一个问题的答案了:clazzOut01所代表的NormalUser和要强转的NormalUser不是一个类型,虚拟机就直接报错了

结语

为啥这个要有结语呢?

因为不成熟的推测真的是本人 YY 的结果,不敢保证真实有效。如有错误之处,还望不吝赐教。

下一篇会续上Java虚拟机(五)垃圾收集(老是感觉要写写垃圾分类啥的,猪都不能吃的那种)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值