类加载机制
把class文件加载到内存,并对数据进行校验,准备,解析,初始化,形成可以被虚拟机直接使用的字节码
类加载的时机(触发类的初始化)
- 使用new关键字实例化对象
- 读取一个类的静态代码块
- 使用java.lang.reflect包的方式对类进行反射调用
类加载过程
整个生命周期包括:加载、校验、准备、解析、初始化、使用和卸载7个阶段。
- 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
- 校验:校验是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟自身的安全。
- 准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 解析:解析阶段是虚拟机将常量池内的符号(Class文件内的符号)引用替换为直接引用(指针)的过程。
- 初始化:初始化阶段是类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码)。
加载
- 通过全类名获取class文件的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 生成一个代表该类的class对象,作为方法区这些数据的访问入口
根据字节码在java堆中生成一个代表这个类的java.lang.Class对象
校验
- 验证Class文件中二进制字节流符合虚拟机的要求,不会涉及到虚拟机的安全
- 文件格式验证
- 元数据验证
- 字节码验证
准备
- 为类变量分配内存空间和设置初始值的阶段
- 为新生对象分配内存
为新生对象分配内存
- 如果Java堆内存是规整连续的,采用“指针碰撞”的分配方式
- 如果不是连续规整的,采用“空闲列表”分配方式
内存是否规整取决于垃圾收集器是否带有压缩整理功能
这个初始值和初始化阶段的赋值不同,这里指的是变量的默认初始值,如果是final修饰的变量,就是赋予代码里制定的初始值
解析
虚拟机将符号引用替换为直接引用的过程
符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。个人理解为:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
直接引用 :直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。
初始化
根据程序代码去初始化变量和其他资源,构造函数
类加载器
BootstrapLoader/负责加载系统类
注意一个很重要的问题,就是java在逻辑上并不存在BootstrapKloader的实体,因为它是c++编写的,所以打印其内容会是null
启动类加载器主要加载 jre/lib下的jar文件。
ExtClassLoader/负责加载扩展类//继承类和实现类
扩展类加载器主要加载 jre/lib/ext 下的jar文件。
AppClassLoader/负责加载应用类
应用程序类加载器主要加载 classpath 下的文件
Android类加载器
对于Android而言,最终的apk文件包含的是dex类型的文件,dex文件是将class文件重新打包,打包的规则又不是简单地压缩,而是完全对class文件内部的各种函数表,变量表进行优化,产生一个新的文件,即dex文件。因此加载这种特殊的Class文件就需要特殊的类加载器DexClassLoader。
双亲委派模型
当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载
解释器
对字节码逐条解释执行,这种方式的执行速度相对会比较慢;重复执行需要重复解释
JIT(即时编译器)
在运行时,虚拟机将会把这些频繁调用的代码编译成与本地平台相关的机器码,并进行优化,可重复执行,缓存效率高
Class文件字节码结构
魔数—副版本号—主版本号—常量池计数器—常量池数据区—访问标志—类索引—父类索引—接口计数器—接口信息数据区—字段计数器—字段信息数据区—方法计数器—方法信息数据区—属性计数器—属性信息数据区
魔数:class文件的标志,确定这个文件是否为一个能被虚拟机接收的class文件
类B继承A,A、B两个类中都有静态变量、成员变量、静态代码块、构造方法执行顺序是什么?
1.父类【静态成员】和【静态代码块】,按在代码中出现的顺序依次执行。
2.子类【静态成员】和【静态代码块】,按在代码中出现的顺序依次执行。
3.父类的【普通成员变量被普通成员方法赋值】和【普通代码块】,按在代码中出现的顺序依次执行。
4.执行父类的构造方法。
5.子类的【普通成员变量被普通成员方法赋值】和【普通代码块】,按在代码中出现的顺序依次执行。
6.执行子类的构造方法。
JVM在搜索类的时候,又是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。
就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
类的加载过程,Person person = new Person();为例进行说明。
1).因为new用到了Person.class,所以会先找到Person.class文件,并加载到内存中;
2).执行该类中的static代码块,如果有的话,给Person.class类进行初始化;
3).在堆内存中开辟空间分配内存地址;
4).在堆内存中建立对象的特有属性,并进行默认初始化;
5).对属性进行显示初始化;
6).对对象进行构造代码块初始化;
7).对对象进行与之对应的构造函数进行初始化;
8).将内存地址付给栈内存中的p变量
class实例和class对象的区别
java有两种对象:实例对象和Class对象。
每个类的运行时的类型信息就是用Class对象表示的。它包含了与类有关的信息。
其实我们的实例对象就通过Class对象来创建的
有三种获得Class对象的方式:
- Class.forName(“类的全限定名”)
- 实例对象.getClass()
- 类名.class (类字面常量)
实例newInstance()
最后
看完有什么不懂的可以在评论区问我,觉得对你有帮助的话记得给我点个赞!