类加载过程
类加载过程:即JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
比如:JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
由此可见,JVM并不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类 / 预料某个类将要被使用时才会加载,且只加载一次。(在同一个类加载器下,一个类型只会被初始化一次)
如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
类是什么时候加载的?
在java代码中,类型的加载、连接、与初始化过程都是在程序运行期间完成的(类从磁盘加载到内存中经历的三个阶段)
1. 加载
加载阶段是类加载的第一个阶段
步骤:
1. 将类的.class文件(字节码文件)中的二进制数据读入到内存中
2. 再将其(二进制数据)放在运行时数据区的方法区内
3. 然后在堆区创建一个 java.lang.Class 对象
4. 向Java程序员提供了访问方法区内的数据结构的接口
加载.calss文件的方式:
(1)从本地系统中直接加载
(2)通过网络下载.class文件
(3)从zip,jar等归档文件中加载.class文件
(4)从专用数据库中提取.class文件
(5)将java源文件动态编译为.class文件
2. 验证(了解)
验证就是为了确保Class文件中字节流包含的信息符合当前虚拟机的要求
文件格式验证: 是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内…
元数据验证: 字节码描述的信息进行语义分析
字节码验证
符号引用验证
验证阶段是非常重要的,但不是必须的。可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3. 准备
类变量:指的是被 static 修饰的变量
类成员变量:除过被static修饰的变量
3.1 内存分配
在准备阶段,JVM 只会为类变量(static)分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始。
public static int OLAF= 666;
public String ICON = "jvm";
例如:在准备阶段,只会为 OLAF 属性分配内存,而不会为 ICON 属性分配内存。
3.2 初始化类型
在JVM为类变量(static)分配内存之后,会对其进行初始化。
但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。
public static int OLAF = 666;
例如:在准备阶段之后,OLAF的值将是 0,而不是 666。
但是:对于常量来说(被 static final 修饰),在准备阶段,属性便会被赋予用户希望的值。
public static final int OLAF = 666;
例如:在准备阶段之后,OLAF 的值将是 666,而不再会是 0。
原因:被 final 修饰的变量final 一旦赋值就不会再改变了。
而没有被 final 修饰的类变量,其值可能在初始化阶段/运行阶段发生变化,所以没有必要在准备阶段对它赋予用户想要的值。
换句话说,只对static修饰的变量或语句进行初始化。
4. 解析(了解)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
5. 初始化
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。
java程序对类的使用方式可分为两种:
1.主动使用
2.被动使用
一般来说只有当对类的首次主动使用的时候才会导致类的初始化
类的主动使用包含以下几种情况:
1、 创建类的实例 (即 new 的方式)// 注意:通过数组定义来引用类,不会触发此类的初始化
2、 访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰/其实更准确的说应该是:编译器把结果放入常量池的静态字段除外)
3、 调用类的静态方法
4、 反射(如 Class.forName(“com.gx.yichun”))
5、 初始化某个类的子类,则其父类也会被初始化
6、 Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会首先被初始化
类的实例化与类的初始化区别
类的实例化是指创建一个类的实例(对象)的过程;
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。
6. 使用(了解)
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
7. 卸载(了解)
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
什么时候 JVM 结束生命周期呢?
1、程序正常执行结束
2、 程序在执行过程中遇到了异常或错误而异常终止
3、 执行了 System.exit()方法
4、 由于操作系统出现错误而导致Java虚拟机进程终止
接口与类加载过程区别
当一个类在初始化时,要求其父类全部都已经初始化过了。
而一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
例题学习
class Father2{
public static String strFather="HelloJVM_Father";
static{
System.out.println("Father静态代码块");
}
}
class Son2 extends Father2{
public static String strSon="HelloJVM_Son";
static{
System.out.println("Son静态代码块");
}
}
public class InitativeUseTest2 {
public static void main(String[] args) {
System.out.println(Son2.strSon);
}
}
运行结果:
Father静态代码块
Son静态代码块
HelloJVM_Son
原因:Son2.strSon是调用了Son类自己的静态方法属于主动使用,所以会初始化Son类。
又由于继承关系,类继承原则是初始化一个子类,所以会先去初始化其父类。
class YeYe{
static {
System.out.println("YeYe静态代码块");
}
}
class Father extends YeYe{
public static String strFather="HelloJVM_Father";
static{
System.out.println("Father静态代码块");
}
}
class Son extends Father{
public static String strSon="HelloJVM_Son";
static{
System.out.println("Son静态代码块");
}
}
public class InitiativeUse {
public static void main(String[] args) {
System.out.println(Son.strFather);
}
}
运行结果:
YeYe静态代码块
Father静态代码块
HelloJVM_Father
原因:对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显!
Son.strFather中的静态字段是属于父类Father的,也就是说直接定义这个字段的类是父类Father。
所以在执行 System.out.println(Son.strFather); 这句代码的时候会去初始化Father类而不是子类Son!
class YeYe{
static {
System.out.println("YeYe静态代码块");
}
}
class Father extends YeYe{
public final static String strFather="HelloJVM_Father";
static{
System.out.println("Father静态代码块");
}
}
class Son extends Father{
public static String strSon="HelloJVM_Son";
static{
System.out.println("Son静态代码块");
}
}
public class InitiativeUse {
public static void main(String[] args) {
System.out.println(Son.strFather);
}
}
运行结果:HelloJVM_Father
原因:final static
所以并不会初始化任何类(Main方法所在类除外)
class Test{
static {
System.out.println("static 静态代码块");
}
public static final double str=Math.random(); //编译期不确定
}
public class FinalUUidTest {
public static void main(String[] args) {
System.out.println(Test.str);
}
}
运行结果:static 静态代码块
0.7338688977344875
原因:这里final已经不是重点了,重点是编译器把结果放入常量池这一步。
当一个常量的值并非编译期可以确定的,那么这个值就不会被放到调用类的常量池中。
public class ClassAndObjectLnitialize {
public static void main(String[] args) {
System.out.println("输出的打印语句");
}
public ClassAndObjectLnitialize(){
System.out.println("构造方法");
System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
}
{
System.out.println("普通代码块");
}
int ZhiShang = 250;
static int QingShang = 666;
static
{
System.out.println("静态代码块");
}
}
运行结果
静态代码块
输出的打印语句
此例中类初始化时会执行以下内容:
static int QingShang = 666; //类变量(static变量)的赋值语句
static
{
System.out.println("静态代码块");
}
public class ClassAndObjectLnitialize {
public static void main(String[] args) {
new ClassAndObjectLnitialize();
System.out.println("输出的打印语句");
}
public ClassAndObjectLnitialize(){
System.out.println("构造方法");
System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
}
{
System.out.println("普通代码块");
}
int ZhiShang = 250;
static int QingShang = 666;
static
{
System.out.println("静态代码块");
}
}
运行结果:
静态代码块
普通代码块
构造方法
我是熊孩子我的智商=250,情商=666
输出的打印语句