1. 什么是类加载过程
-
类加载过程就是
将.class文件中类的元信息加载进内存,创建Class对象并进行解析、初始化类变量等的过程
-
类加载一共分为以下三个阶段
[ 装载, 链接, 初始化 ]
-
JVM并不是一开始就会将所有的类加载到内存,而是用到某个类,才会去加载,只加载一次,后续会说到
类的加载时机
1.1 loading
类的装载主要的职责为
通过类的全名将.class文件的二进制字节流读入内存
(JDK1.7及之前为JVM内存,JDK1.8及之后为本地内存)- 解析类的二进制数据流为
方法区的数据结构
(类模板-反射机制就是基于这一基础所诞生的) - 并在
堆内存中为之创建Class对象
,作为.class进入内存后的数据的访问入口。
拓展
-
在这里只是读入二进制字节流,后续的验证阶段就是要拿二进制字节流来验证.class文件,验证通过,才会将.class文件转为运行时数据结构
-
在JDK1.7及以前,Hot Spot JVM(普遍在用的JVM)存在一块叫做方法区的内存,也称之为永久代,这块区域用于存放类的元数据信息,包括类的字段,版本,方法等,这块区域,可以理解为.class文件进入内存后的位置。在JDK1.8,取消了方法区,取而代之的是元数据区,该元数据区并非JVM内存,而是本地内存。此外在JDK1.7时,将常量池从方法区移除,在堆内存开辟了一块空间作为常量池,有人说这是为取消方法区做的准备
-
为何取消方法区?
官方说法为:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
现实使用中存在问题:方法区存储类的元数据信息,我们不清楚一个程序到底有多少类需要被加载,且方法区位于JVM内存,我们不清楚需要给方法区分配多大内存,太小容易PermGen OOM,太大,在触发Full GC时又极其影响性能,同时还存在一些内存泄露的问题
1.2 linking
链接阶段有三个步骤
1.2.1 验证
该阶段主要是为了保证加载进来的字节流符合JVM的规范,不会对JVM有安全性问题。其中有对元数据的验证,例如检查类是否继承了被final修饰的类;还有对符号引用的验证,例如校验符号引用是否可以通过全限定名找到,或者是检查符号引用的权限(private、public)是否符合语法规定等。
1.2.2 准备
准备阶段的主要任务是为类的静态变量开辟空间并赋默认值
静态变量是基本类型
(int、long、short、char、byte、boolean、float、double)的默认值为0
静态变量是引用类型
的,默认值为null
静态常量
默认值为声明时设定的值
例如:public static final int i = 3; 在准备阶段,i的值即为3
1.2.3 解析
该阶段的主要职责为将Class在常量池中的符号引用转变为直接引用
,此处针对的是静态方法及属性和私有方法与属性,因为这类方法与私有方法不能被重写,静态属性在运行期也没有多态这一说,即在编译器可知,运行期不可变,所以适合在该阶段解析,譬如类方法main替换为直接引用,为静态连接,区别于运行时的动态连接(后续我会写关于JVM内存结构的文章,在讲解栈帧时会介绍动态链接)。
符号引用即字符串
,说白了可以是一个字段名,或者一个方法名;直接引用即偏移量
,说白了就是类的元信息位于内存的地址串,例如,一个类的方法为test(),则符号引用即为test,这个方法存在于内存中的地址假设为0x123456,则这个地址则为直接引用。
1.3 initialization
topic : 在前两个阶段并未执行到代码, 而在初始化阶段开始执行代码了
初始化阶段主要任务
- 给静态变量
显示赋值
(注意链接阶段的准备环节是赋初值 0这个要区别开) - 给
静态代码块
里的变量显示赋值
拓展
-
在初始化阶段, 如果有声明静态变量或静态代码块中的变量并且显式赋值了的话, 就会调用<clinit>()方法来初始化这些变量, 并且<clinit>()方法是加了锁的会产生死锁现象
-
区分<clinit>() 和 <init>()
<init>() 方法在调用某个类的构造方法是调用, 并且会把不是静态的都执行一遍并且 执行顺序优先于构造方法
<clinit>() 方法在类的初始化阶段时(如果有静态变量并且显示赋值)则会调用该方法, 把静态变量的赋值操作以及静态代码块都会执行一遍