java(Android)类加载机制

1、Java的类加载过程

加载、链接(验证、准备、解析)、初始化

加载就是把class文件字节码加载进jvm内存,变成Class对象。验证class字节流中包含的信息是jvm需要且有效的。准备是给类变量分配内存并设置初始化值。解析是把符号引用变成直接引用。初始化就是执行静态初始化器(静态代码块)和静态变量初始化。

2、有几种类加载器,它们有什么不同?

启动(Bootstrap)类加载器、扩展(Extension)类加载器和系统(System)加载器(也成应用加载器)

启动加载器用于加载jvm需要用到的类,是C++实现的,加载<JAVA_HOME>/lib路径下的核心类库或者-Xbootclasspath参数指定的路径下的jar包。

扩展类加载器,是java实现的,是Launcher的静态内部类sun.misc.Launcher$ExtClassLoader ,它负责加载<JAVA_HOME>/lib/ext目录下或由系统变量-Djava.ext.dir指定路径中的库。

系统类加载器,也是Launcher的静态内部类sun.misc.Launcher$AppClassLoader,它负责加载系统路径java -classpath或者-D java.class.path指定路径下的类库。一般情况下该类加载器是程序中默认的类加载器,通过ClassLoader.getSystemClassLoader()方法可以获取该类加载器。

所以,我们知道,不同类加载器都有自己特定的加载路径,而且只能加载自己特定路径下的类。

3、有三种类加载器,jvm是怎么保证一个类只加载一次的?

先明确一下概念,每个类加载器都有自己的类名称空间,也就是说同一个class文件,不同的类加载器加载会生成不同的Class对象。也就是说,Class对象要相等,(1)类的全限定名是相同的(2)加载类是相同的

所以,为了保证一个class文件只加载一次只生成一个Class对象,class文件只能让一个类加载器加载,jvm使用了“双亲委派模式”来保证。

考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String 已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变 JDK 中 ClassLoader 搜索类的默认算法。

首先,用类全限定名查询类是否被加载过,没有加载过,先尝试用父加载器加载,递归加载,如果父加载器不能加载就自己加载。

这里的父加载器不是继承关系而是组合关系。自定义类加载器的父加载器是应用类加载器,应用类加载器的父加载器是扩展类加载器,扩展类加载器没有父加载器。

看看java的类的继承实现

4、双亲委派模型确实保证了了Class对象唯一性的问题,可是有问题是:目前只有启动类加载器加载的接口,可是,需要用到这个接口实现类中的方法,实现类是第三方实现的,启动类加载器是不能加载的,因为不在启动类加载器的加载目录下,这个时候就得用到线程上下文类加载器了。

5、自定义类加载器

自定义类加载器是很重要的技能,自定义类加载器是为了加载指定目录下的类,而此时上面说的三个类加载器都失效了。最典型的使用就是Android中的类加载器 PathClassLoader和DexClassLoader,都是继承自BaseDexClassLoader(extends ClassLoader)。PathClassLoader是加载系统类和安装应用自己的类的,我们一般使用DexClassLoader,自定义类加载器也是需要满足“双亲委派模式”的,不要重写loadClass,而是要重写findClass。具体看文章Android类加载机制的细枝末节

6、简述一下android的类加载机制

android中有两个类加载器,PathClassLoader和DexClassLoader,这两个类都继承自BaseDexClassLoader,Android中的类加载器属于java中的自定义类加载器,所以得满足java 类记载器的规则。PathClassLoader是用于加载系统类和应用安装的类的,(应用安装的时候会对类进行优化的,所以,安装的类和网络上下载的类是不一样的),DexClassLoader用于加载jar、apk、zip和dex文件,执行未被应用安装过的代码。

dex文件被加载后进行优化,并且会存起来,BaseDexClassLoader构造函数的第二个参数optimizedDirectory指定了存放目录。PathClassLoader是用于加载执行应用安装过的代码的,这些代码在安装的时候已经被优化过了,存放目录是/data/dalvik-cache/,所以PathClassLoader调用父类BaseDexClassLoader构造函数的时候,optimizedDirectory是null。

PathClassLoader是系统使用的,我们不能用,我们可以使用DexClassLoader,主要工作还是在BaseDexClassLoader里面,BaseDexClassLoader构造的时候,就会把dex进行优化放到optimizedDirectory目录下(dexElements),并且会获取到系统的so和包里的so(nativeLibraryDirectories ),然后加载指定类的时候调用loadClass,也是双亲委派模型(DexClassLoader->PathClassLoader->BootClassLoader),首次加载的话,最终会调用DexClassLoader的findClass加载,也就是BaseDexClassLoader的findClass方法,它会从dexElements中加载。

7、

我们知道类的加载过程是分为七步的,但是

(1)并不一定,只要加载类,七步都要一起做完,这是需要注意的。

什么情况需要开始类加载过程的第一阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则严格规定了以下几种情况必须立即对类进行初始化,如果类没有进行过初始化,则需要先触发其初始化。

虚拟机规范严格规定了有且只有5中情况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到 new, getstatic, putstatic, invokestatic 这些字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用jdk1.7动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果REF_getstatic, REF_putstatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

有个概念是要求类需要初始化,我们可以把这个称为对类的“主动引用”,其它不触发类的初始化,我称为对类的“被动引用”

从一道面试题来认识java类加载时机与过程

这篇文章中有被动引用的例子

(2)这七步,有些步骤顺序可以调整,有些步骤是交叉在一起完成的。

看下面的红色文字,理解一下,加载和验证是怎么交叉在一起的

 “加载”(Loading)阶段是“类加载”(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:

  • 通过全限定类名获取类的二进制字节流(不管文件的来源,网络也好,磁盘文件或压缩包也罢,只要能读入二进制流就可以);
  • 将读取的字节流代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中(并没有明确规定是在堆中,HotSpot中Class对象虽然也是对象,但是却是存储在方法区中的)生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种信息的入口。

加载阶段即可以使用系统提供的类加载器在完成,也可以由用户自定义的类加载器来完成。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

1、文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。

(3)类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

8、一个问题,父类是什么时候被加载的呢?

这个问题,本身问的就不是很专业,应该是父类是在什么时候被初始化的。加载只是一个类加载过程中的第一步,我理解,由于虚拟机规范没有对类加载的时机做定义,是由虚拟机的具体实现决定的,我觉得可能在加载子类的时候,也可能是在需要初始化父类的时候。

那父类是什么时候被初始化的呢?虚拟机规范对初始化时机是有规定的,其中一个时机就是子类初始化的时候,父类一定要先初始化。

初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

有一个概念是类构造器<clinit>(),这个是虚拟机帮我们生成的,下面的描述参考Java类加载过程

<clinit>() <init>()

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。


第6点,就是为什么可以使用静态内部类实现单例的理论基础。设计模式之单例模式

  1. <clinit>()方法是由编译器自动收集类中静态变量的赋值语句以及静态代码块中定义的语句合并产生的,且内部的语句的顺序是由定义的顺序决定的,后面的语句可以访问前面定义的变量,反过来可以赋值,但是不能访问。
  2. <clinit>()<init>()是不同的,前者对应的是类,后者对应的是实例,即前一个是Class的构造器(是编译器生成的),后者是实例对象的构造器(也就是我们定义或继承的构造函数)。且虚拟机会保证子类的<clinit>()执行之前,其父类的<clinit>()一定执行完成,无需显式指定。所以第一个执行<clinit>()的类是java.lang.Object;
  3. 因为父类比子类先执行<clinit>(),所以父类的静态变量和静态代码块是先于子类执行的。
  4. 如果一个类或者接口中没有静态变量或静态代码块,编译器可以不生成<clinit>()
  5. 接口中没有静态代码块,但是可以有静态变量。所以可以有<clinit>()的初始化动作,但是接口和类不同之处在于接口不需要先执行父接口的<clinit>()方法。
  6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中能被正确的同步,利用这一点可以实现线程安全的单例模式。

9、被动引用

常量(static final)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
 

深入理解Java类加载器(ClassLoader)

Android 类加载机制及热修复原理

两道面试题,带你解析Java类加载机制

Java类加载机制的七个阶段,加载、验证、准备、解析、初始化、使用、卸载

从一道面试题来认识java类加载时机与过程

回归Java基础:触发类加载的六大时机,你知道几个?

Java类加载过程

java类加载的时机和触发类的初始化的条件

JVM常量池浅析

JVM中的直接引用和符号引用

浅析 JVM 中的符号引用与直接引用

双亲委派模型,类的加载机制,搞定大厂高频面试题​​​​​​​

发布了189 篇原创文章 · 获赞 25 · 访问量 22万+

猜你喜欢

转载自blog.csdn.net/lizhongyisailang/article/details/105173745