Java类加载机制
2019-05-07 本文已影响105人
与搬砖有关的日子
1、类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在方法区的内存中,然后再堆区创建一个java.lang.class对象,用来封装类再方法区中的数据结构。类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它
2、类的生命周期
image.png
-
加载
在加载阶段,虚拟机需要完成以下三件事情:
a.通过一个类的全限定名来获取其定义的二进制字节流。
b.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
c.在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。加载完成后,虚拟机外部的二进制字节流就按虚拟机要求的格式存在方法区中,同时在堆区创建了一个java.lang.class对象用来访问方法区中的数据。 -
验证
为了确保加载类的正确性
a.文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
b.元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
c.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
d.符号引用验证:确保解析动作能正确执行。 -
准备
为类的静态变量分配内存,并将其初始化为默认值
a、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
b、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
c、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。 -
解析
把类中的符号引用转换为直接引用 -
初始化
为类的静态变量赋予正确的初始值,只有当对类的主动使用的时候才会导致类的初始化
初始化步骤:
a、假如这个类还没有被加载和连接,则程序先加载并连接该类
b、假如该类的直接父类还没有被初始化,则先初始化其直接父类
c、假如类中有初始化语句,则系统依次执行这些初始化语句
3、对象的创建过程
image.png
-
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 -
分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
image
-
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 -
执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
4、类加载器
在类加载的第一个阶段“加载”过程中,需要通过一个类的全限定名来获取此类的二进制字节流,完成这个动作的代码块就是类加载器。

-
启动类加载器
Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 -
扩展类加载器
Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。 -
应用程序类加载器
Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
5、双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
6、类的实例化顺序(原文地址)
先静态,先父后子;先静态:父静态>子静态;优先级:父类>子类静态代码块>非静态代码块>构造函数。
- 父类中的static代码块,当前类的static代码块(注意代码块并不指静态方法);
- 顺序执行父类的普通代码块;
- 父类的构造函数;
- 子类普通代码块;
- 子类的构造函数,按顺序执行;
- 子类方法的执行。静态代码块只执行一次无论你创建几次对象。
7、String对象的两种创建方式

字符串拼接会重新创建对象。
java基本数据类型Byte、Short、Integer、Long、Character、Boolean实现了常量池,这5类数据创建了[128,127]的相应类型的缓存数据,超过此范围仍然需要去创建新的对象,Float、Double没有实现常量池。
