java类加载器例子_java类加载器机制

参考

https://blog.csdn.net/zhangjg_blog/article/details/16102131

https://www.jianshu.com/p/b6547abd0706

https://www.jianshu.com/p/8c8d6cba1f8e

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

类的加载机制简介

JVM除了比较类是否相等还要比较加载这两个类的类加载器是否相等,只有同时满足条件,两个类才能被认定是相等的。

JVM就是按照下面的顺序一步一步的将字节码文件加载到内存中并生成相应的对象的。

加载

连接(验证-准备-解析)

初始化

首先将字节码加载到内存中,然后对字节码进行连接,连接阶段包括了验证准备解析这3个步骤,连接完毕之后再进行初始化工作;下面我们一一了解。

获取Class对象的方式

获取Class对象的方式有3种:

Class.forName(包名.类名);

类名.class

对象.getClass()

这几种获取Class对象的不同

类名.class : JVM将使用类装载器, 将类装入内存(前提是:类还没有装入内存),不做类的静态初始化工作(不执行静态代码块),返回Class的对象。

实例对象.getClass() :既然都有对象了,那么是在创建对象时就把静态代码块和构造代码块都执行了,返回引用运行时真正所指的对象(因为:子对象的引用可能会赋给父对象的引用变量中)所属的类的Class的对象。

Class.forName(包名.类名) :装入类,并执行静态初始化工作(执行静态代码块),返回Class的对象。

Class.forName(String className)源码:

public static Class> forName(String className) throwsClassNotFoundException {

Class> caller =Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);

}private static native Class> forName0(String name, boolean initialize, ClassLoader loader, Class> caller)

就是说第二个参数initialize是控制是否对类进行静态初始化。而Class.forName(String className)内部是true,所以会执行静态初始化工作。

什么是类的加载?

虚拟机加载类有两种方式,一种方式ClassLoader.loadClass()方法,另一种是使用反射API,Class.forName()方法,其实Class.forName()方法内部也是使用的ClassLoader。

类的加载指的是将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区域的方法区内,然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构.只有java虚拟机才会创建class对象,并且是一一对应关系,这样才能通过反射找到相应的类信息。

我们上面提到过Class这个类,这个类我们并没有new过,这个类是由java虚拟机创建的。通过它可以找到类的信息,我们来看下源码:

/** Constructor. Only the Java Virtual Machine creates Class

* objects.*/

private Class() {...}

从上面贴出的Class类的构造方法源码中,我们知道这个构造器是私有的,并且只有虚拟机才能创建这个类的对象。

讲到类加载,我们不得不了解类加载器。

类加载器及各类加载器关系

java中(指的是javase)有几种类加载器。每个类加载器在创建的时候已经指定他们对应的目录, 也就是说每个类加载器去哪里加载类是确定的。

BootStrap        --   加载JRE/lib/rt.jar中的类

ExtClassLoader   --   加载jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下的类

AppClassLoader  --   加载classpath指定的路径中的类

自定义的ClassLoader

079eb813d5b8dd10ff962a2440b6da28.png

这几种ClassLoader并不是继承的关系,而是一种委托关系。

那么类加载器是如何工作的呢?可以参看jdk中ClassLoader类的源码。

private finalClassLoader parent;protected Class> loadClass(String name, boolean resolve) throwsClassNotFoundException

{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

}returnc;

}

}

...

从源码可以总结出三个特性,分别为委派,可见性和单一性,其他文章上对这三个特性的介绍如下:

委托机制是指将加载一个类的请求交给父加载器,如果这个父加载器不能够找到或者加载这个类,那么再加载它。

可见性的原理是子加载器可以看见所有的父加载器加载的类,而父加载器看不到子加载器加载的类。

单一性原理是指仅加载一个类一次,这是由委托机制确保子加载器不会再次加载父加载器加载过的类。

其中,委派机制是基础,在其他资料中也把这种机制叫做类加载器的双亲委派模型,其实说的是同一个意思。可加性和单一性是依赖于委派机制的。

他们之间的关系可以通过例子展示:

ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();

System.out.println(appClassLoader);//sun.misc.Launcher$AppClassLoader@19821f

ClassLoader extClassLoader=appClassLoader.getParent();

System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@addbf1//AppClassLoader的父加载器是ExtClassLoader

System.out.println(extClassLoader.getParent());//null//ExtClassLoader的父加载器是null, 也就是BootStrap,这是由c语言实现的

由打印结果可知,

加载我们自己编写的类它的加载器是AppClassLoader,

AppClassLoader的父加载器是ExtClassLoader,

在而ExtClassLoader的父加载器返回结果为null,这说明他的附加载器是BootStrap,这个加载器是和虚拟机紧密联系在一起的,在虚拟机启动时,就会加载jdk中的类,它是由C实现的,没有对应的java对象,所以返回null。但是在逻辑上,BootStrap仍是ExtClassLoader的父加载器。也就是说每当ExtClassLoader加载一个类时,总会委托给BootStrap加载。

另外我们自己实现的ClassLoader的parent是AppClassLoader。

自定义ClassLoader验证

packagecom.puppet.test;importjava.io.IOException;importjava.io.InputStream;public classMyClassLoader {public static void main(String args[]) throwsClassNotFoundException, InstantiationException, IllegalAccessException {

ClassLoader loader= newClassLoader() {

@Overridepublic Class> loadClass(String name) throwsClassNotFoundException {

String fileName= name.substring(name.lastIndexOf(".") + 1) + ".class";

InputStream inputStream=getClass().getResourceAsStream(fileName);if (inputStream == null) {return super.loadClass(name);

}else{try{byte[] bytes = new byte[inputStream.available()];

inputStream.read(bytes);return defineClass(name, bytes, 0, bytes.length);

}catch(IOException e) {

e.printStackTrace();throw newClassNotFoundException(name);

}

}

}

};

Object object= loader.loadClass("jvm.classloader.MyClassLoader").newInstance();

System.out.println(objectinstanceofcom.puppet.test.MyClassLoader);

}

}

结果为false。

所以,我们如果开发自己的类加载器,只需要继承jdk中的ClassLoader类,并覆盖findClass方法就可以了,剩下的而工作,父类会完成。其他java平台有的根据自己的需求,实现了自己特定的类加载器,例如javaee平台中的tomcat服务器,android平台中的dalvik虚拟机也定义了自己的类加载器。

系统类加载器和线程上下文类加载器

在java中,还存在两个概念,分别是系统类加载器和线程上下文类加载器。

其实系统类加载器就是AppClassLoader应用程序类加载器,它两个指的是同一个加载器,以下代码可以验证:

ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();

System.out.println(appClassLoader);//sun.misc.Launcher$AppClassLoader@19821f

ClassLoader sysClassLoader=ClassLoader.getSystemClassLoader();

System.out.println(sysClassLoader);//sun.misc.Launcher$AppClassLoader@19821f//由上面的验证可知, 应用程序类加载器和系统类加载器是相同的, 因为地址是一样的

这两个类加载器对应的输出,不仅类名相同,连对象的哈希值都是一样的,这充分说明系统类加载器和应用程序类加载器不仅是同一个类,更是同一个类的同一个对象。

每个线程都会有一个上下文类加载器,由于在线程执行时加载用到的类,默认情况下是父线程的上下文类加载器, 也就是AppClassLoader。

new Thread(newRunnable() {

@Overridepublic voidrun() {

ClassLoader threadcontextClassLosder=Thread.currentThread().getContextClassLoader();

System.out.println(threadcontextClassLosder);//sun.misc.Launcher$AppClassLoader@19821f

}

}).start();

这个子线程在执行时打印的信息为sun.misc.Launcher$AppClassLoader@19821f,可以看到和主线程中的AppClassLoader是同一个对象(哈希值相同)。

也可以为线程设置特定的类加载器,这样的话,线程在执行时就会使用这个特定的类加载器来加载使用到的类。如下代码:

Thread th = new Thread(newRunnable() {

@Overridepublic voidrun() {

ClassLoader threadcontextClassLosder=Thread.currentThread().getContextClassLoader();

System.out.println(threadcontextClassLosder);//jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74

}

});

th.setContextClassLoader(newClassLoader() {});

th.start();

在线程运行之前,为它设置了一个匿名内部类的类加载器对象,线程运行时,输出的信息为:jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74,也就是我们设置的那个类加载器对象。

类的连接

讲完了类的加载之后,我们需要了解一下类的连接。类的连接有三步,分别是验证,准备,解析。下面让我们一一了解

首先我们看看验证阶段。

验证阶段主要做了以下工作

将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。

类文件结构检查:格式符合jvm规范

语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖

字节码验证:确保字节码可以安全的被java虚拟机执行.

二进制兼容性检查:确保互相引用的类的一致性。如A类的a方法会调用B类的b方法,那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性,因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。

准备阶段

java虚拟机为类的静态变量分配内存并赋予默认的初始值。如int分配4个字节并赋值为0,long分配8字节并赋值为0;

解析阶段

解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。

初始化

类加载进内存后不一定会初始化,下边是几种触发类的主动初始化(类的静态初始化)的方式:

创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。

调用类的静态属性或者为静态属性赋值

调用类的静态方法

通过class文件反射创建对象

初始化一个类的子类:使用子类的时候先初始化父类

java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类

只有上面6种情况才是主动使用,也只有上面六种情况的发生才会引发类的初始化。

同时我们需要注意下面几个Tips:

在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.

调用 编译常量(在编译的时候能确定下来的),不会对类进行初始化;

调用 运行时常量(在编译时无法确定下来的),会对类进行初始化;

这里都是指的final static修饰的属性,而只有static修饰的属性就属于触发初始化的第二条。

如果这个类没有被加载和连接的话,那就需要进行加载和连接

如果这个类有父类并且这个父类没有被初始化,则先初始化父类.

如果类中存在初始化语句,依次执行初始化语句.

静态代码块和静态属性是同级别加载,就是说按照代码的编写顺序,谁在前谁就先执行

例子:

public classTest1 {public static voidmain(String args[]){

System.out.println(FinalTest.x);

}

}classFinalTest{public static final int x =6/3;static{

System.out.println("FinalTest static block");

}

}

上面和下面的例子大家对比下,然后自己看看输出的是什么?

public classTest2 {public static voidmain(String args[]){

System.out.println(FinalTest2.x);

}

}classFinalTest2{public static final int x =new Random().nextInt(100);static{

System.out.println("FinalTest2 static block");

}

}

第一个输出的是

2

第二个输出的是

FinalTest2 staticblock61(随机数)

那么将第一个例子的final去掉之后呢?输出又是什么呢?

这就是对类的首次主动使用,引用类的静态变量,输出的当然是:

FinalTest staticblock2

类的初始化步骤

讲到这里我们应该对类的加载-连接-初始化有一个全局概念了,那么接下来我们看看类具体初始化执行步骤。

我们分两种情况讨论,一种是类有父类,一种是类没有父类。(当然所有类的顶级父类都是Object)

没有父类的情况:

类的静态属性

类的静态代码块

类的非静态属性

类的非静态代码块

构造方法

有父类的情况:

父类的静态属性

父类的静态代码块

子类的静态属性

子类的静态代码块

父类的非静态属性

父类的非静态代码块

父类构造方法

子类非静态属性

子类非静态代码块

子类构造方法

在这要说明下,静态代码块和静态属性是等价的,他们是按照代码顺序执行的。

例子

public classSingleton {private static Singleton singleton = newSingleton();public static intcounter1;public static int counter2 = 0;privateSingleton() {

counter1++;

counter2++;

}public staticSingleton getSingleton() {returnsingleton;

}

}public classTestSingleton {public static voidmain(String args[]){

Singleton singleton=Singleton.getSingleton();

System.out.println("counter1="+singleton.counter1);

System.out.println("counter2="+singleton.counter2);

}

}

输出是:

counter1=1

counter2=0

注:调整counter1,counter2和singleton的顺序会得到不同结果。

总结

在Android中,QQZone团队提出的基于Dex分包的热修复解决方案就属于加载外部的类,本来应当由开发者自己实现classloader来实现加载过程,但是Android本身已经为我们封装好了一个classloader,就是DexClassLoader。

事实上,如今Java中很多插件化开发,动态部署,热修复等动态技术都是基于Java的类加载器来展开的。因此,我才会想专门用一篇文章总结Java的类加载器和加载机制。后面我会找时间基于HotFix详细的分析其中的类加载过程。毕竟理论总要落实到代码才会让人印象深刻。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值