正文开始之前,先看一张图
java文件经过编译后生成class文件,再经由类装载器子系统将class文件加载到运行时数据区,最后由执行引擎解析指令并执行。
类加载器子系统
概述:
- 类加载器子系统负责从文件系统或者网络中将Class文件加载到运行时数据区
- Class Loader只负责Class文件的加载,至于它是否可以运行,则由执行引擎决定
- 加载的类信息(类的元信息,也称为DNA元数据模版,比如类所在的包、父类、实现的接口等)存放于一块称为方法区的内存空间
类加载器
- 引导类加载器(启动类加载器):用来加载Java的核心库,例如
java.lang
包中的类 - 扩展类加载器:负责加载Java的扩展类库,位于
jre/lib/ext
目录下的JAR文件 - 系统类加载器:该加载器是程序中默认的类加载器,所有自定义的类都是 由该类加载器加载
除以上三个类加载器外,用户也可以自定义类加载器 实现步骤:
- 继承抽象类
java.lang.ClassLoader
- 在JDK1.2之前,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载器,但是在JDK1.2之后,官方已经不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
PS:用户使用自定义的类加载器可以打破双亲委派机制
双亲委派机制
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
简单理解就是一个类想要加载到内存中,会首先进到系统类加载器,系统类加载器并不会马上加载,而是向上委托到扩展类加载器,扩展类加载器同理也不会马上加载,而是向上委托到启动类加载器,此时已经到了最顶层,那从这里开始,启动类加载器就会开始加载。启动类加载器如果发现该类自己可以加载,那就执行加载操作,如果发现该类不归自己管,那就会一层层的向下递交,由扩展类加载器或系统类加载器尝试加载,直至成功将该类加载到内存中
举个例子:当你现在有一个苹果,出于礼貌你需要先问问你的父亲吃不吃,你的父亲也要先问问你爷爷吃不吃,如果你爷爷吃,那这个苹果在你爷爷那里就已经被吃掉了,如果你爷爷不吃,那苹果就会到你父亲手上了,如果你父亲也不吃,那你才能吃这块苹果
类加载器向上委派顺序:系统类加载器-->扩展类加载器-->引导类加载器
双亲委派机制的优势
- 避免类的重复加载(当Java虚拟机中已经存在一个类的实例,并且试图再次加载同名的类时,会出现类重复加载的问题。这可能是由于不同的类加载器加载了同一个类导致的。)
- 当某一个类加载器加载到某个类后,其他类加载器就不会再次加载该类
- 保护程序安全,防止核心API被随意篡改
- 比如用户自定义一个
java.lang.String
,类加载时,会先由引导类加载器尝试加载,引导类加载器发现可以加载该类,那么就会以JDK所在目录作为根目录,从Java自己的核心库去加载,而不是加载用户自定义的String类
- 比如用户自定义一个
类的加载过程
Java类加载过程包括三个主要阶段:加载(Loading)、链接(Linking)和初始化(Initialization)。
- 加载 loading
- 通过一个类的全限定类名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 链接 linking
- 验证 verify
- 验证class文件的字节流包含信息是否符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备 prepare
- 为类变量(被static修饰的变量)分配内存并且设置该类变量的默认初始值,即零值或null值
- 如果是被final和static同时修饰,那这个变量就变成了一个常量,在此处会被显式初始化
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
- 解析 resolve
- 将符号引用转换为直接引用。符号引用是一种动态的引用,可以在运行时进行解析成直接引用。解析过程包括类或接口的解析、字段解析、方法解析等。
- 验证 verify
- 初始化 initialization
- 初始化阶段就是执行类构造器方法<clinit>()的过程,这里的类构造器方法不是指类构造器
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令按语句在源文件中出现的顺序执行
- 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
- 虚拟机必须要正一个类的<clinit>()方法在多线程下被同步加锁
- 这一步就是给所有静态变量赋值、执行类中的静态代码块