JVM学习札记(一) ~ 类加载器深入解析与阶段分解

一、类加载

1. 在Java代码中,类型的加载,连接与初始化过程都是在程序运行阶段完成的
2. 提供了强大的灵活性,增加了更多的可能性

二、类加载器深入剖析

1. Java虚拟机与程序的生命周期
2. 在如下情况下,Java虚拟机将结束生命周期
  • 执行了System.exit()方法
  • 程序正常运行结束
  • 程序在运行过程中遇到了异常或者错误而终止
  • 由于操作系统错误导致Java虚拟机进程终止

三、类的生命周期

在这里插入图片描述

1. 加载:查找并并加载类的二进制数据
2. 连接
3. 初始化:为类的静态变量赋予正确的初始值

将静态变量的默认值替换为正确的初始化值

4. 使用
5. 卸载

OSGi(开放服务网关协议,Open Service Gateway Initiative)技术是Java动态化模块化系统的一系列规范

四、类的加载

  • 什么是类的加载?

类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构

  • 方法区:jdk1.7及1.7之前jvm中都会存在方法区,1.8之后进行了改造,称之为meta space(元空间)
  • 加载.class文件的方式
  1. 从本地系统中直接加载(最常用)
  2. 通过网络下载.class文件
  3. 从zip,jar等归档文件中加载.class文件
  4. 从专有数据库中提取.class文件
  5. 将Java源文件动态编译为.class文件(动态代理的代理类在编译期不存在,在运行期动态生成;或者servlet)
  • 类的加载最终产品就是位于内存中class对象
  • class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问改方法区内数据结构的接口
  • 有两种类型的类加载器
  1. java虚拟机自带的类加载器
  • 根类加载器(Bootstrap):没有父加载器,负责加载Java的核心类库
  • 扩展类加载器(Extension):父加载器为根类加载器
  • 系统(应用)类加载器(System):也称为应用类加载器,父加载器为扩展类加载器
  1. 用户自定义的类加载器
  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式

在这里插入图片描述

  • 类加载器并不需要等到某个类“首次主动使用”时再加载它
  • JVM规范允许类加载器在预料某个类将要使用时预先加载加它,如果预先加载过程中遇到了.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageErreor错误)
  • 如果这个类一直没有被程序主动使用,类加载器则不会报告错误
  • 双亲委托机制

类加载器用来把类加载到Java虚拟机中,从JDK 1.2开始,类的加载采用双亲委托机制,这种机制能更好的保证Java平台的安全;在双亲委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载都有且只有一个父加载器

五、类的连接(验证,准备,解析)

类被加载后,就进入连接阶段。连接就是将已读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

1. 验证

  • 验证:确保被加载的类的正确性

主要验证字节码文件是否符合Java虚拟机规范

  • 验证内容(主要)
  • 类文件的结构检测
  • 语义检查
  • 字节码验证
  • 二进制兼容性验证

2. 准备

  • 准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段,对象还未被创建,静态变量相当于全局变量;初始化的时候会首先初始化为该变量类型的默认值,而不是初始化为给变量的赋值

3. 解析

  • 解析:把类中的符号引用转为直接引用

六、类的初始化

1. Java程序对类的使用分为两种
  1. 主动使用
  2. 被动使用
2. 类初始化的时机:首次主动使用的时候
  • 主动使用的情况(七种)
  1. 创建类的实例(最常见的通过new的方式)
  2. 访问某个类或接口的静态变量(取值),或者对该静态变量赋值(赋值)
  3. 调用类的静态方法(本质同第2种,可以利用javap反编译class文件查看jvm助记符)
  4. 反射(如Class.forName(“com.lizza.Test”))
  5. 初始化一个类的子类(当初始化子类的时候必然会初始化所有的父类)
  6. java虚拟机启动时被表明为启动类的的类(Java Test)
  7. JDK 1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化(了解)
  • 注意:除了以上7种情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化
3. 类初始化的步骤
  1. 假如这个类还没有被加载和连接,则先进行加载和连接
  2. 假如这个类存在直接父类,并且这个父类还没有初始化,则先初始化直接父类
  3. 假如类中存在初始化语句,则依次执行这些初始化语句
  • 注意:当Java虚拟机初始化一个类的时候,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
  • 初始化一个类的时候,并不会先初始化它所实现的接口
  • 初始化一个接口时,并不会先初始化它的父接口

总结:因此一个父接口并不会因为它的子接口或实现类的初始化而初始化,只有当程序首次使用特定的接口的静态变量时,才会导致该接口初始化

示例1:验证首次使用时初始化
/**
 * 1. 对于静态字段来讲, 只有直接定义了该字段的类才会被初始化;
 * 2. 当一个类被初始化时, 要求其父类必须初始化完成;
 * 3. -XX:+TraceClassLoading 用于追踪类的信息并打印出来
 * 4. jvm 参数使用
 *    -XX:+<option> 表示开启option选项
 *    -XX:-<option> 表示关闭option选项
 *    -XX:<option>=<value> 表示将option的值设置为value
 */
public class ClassLoad_01 {

    public static void main(String[] args){
        System.out.println(Child.p_str);
    }
}

class Parent {

    public static String p_str = "Hello World!";

    /** 类被初始化时, 静态代码块会被执行 **/
    static {
        System.out.println("Parent Static Block!");
    }
}

class Child extends Parent {

    public static String c_str = "Welcome!";

    /** 类被初始化时, 静态代码块会被执行 **/
    static {
        System.out.println("Child Static Block!");
    }
}

输出:

Parent Static Block!
Hello World!

注意

  • 对于静态字段来讲, 只有直接定义了该字段的类才会被初始化;
  • 当一个类被初始化时, 要求其父类必须初始化完成;
  • -XX:+TraceClassLoading 用于追踪类的加载信息并打印出来;

jvm 参数使用

  • -XX:+ 表示开启option选项
  • -XX:- 表示关闭option选项
  • -XX:= 表示将option的值设置为value
示例2:调用常量不会初始化定义常量的类
/**
 * 1. 常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中
 * 2. 本质上, 调用常量时并没有直接引用定义常量的类, 因此不会触发定义常量的类的初始化
 */
 
/**
 * 助记符(通过javap -c反编译查看详细信息)
 * 1. lbc 表示将int, float, String类型的常量值从常量池中推送至栈顶
 * 2. bipush 表示将单字节(127 ~ -128)的常量值推送至栈顶
 * 3. sipush 表示将短整型(32767 ~ -32768)的常量值推送至栈顶
 * 4. iconst_1 表示将int类型的1推送至栈顶, jvm内置了int类型的-1~5的助记符(iconst_m1~iconst_5)
 */
public class ClassLoad_02 {

    public static void main(String[] args){
        System.out.println(Sub.i_2);
    }
}

class Sub {
    public static final String str = "Hello Word!";
    public static final short s = 127;
    public static final int i_1 = 128;
    public static final int i_2 = -1;
    public static final boolean b = false;

    static {
        System.out.println("Sub Static Block!");
    }
}

输出:

Hello Word!
示例3:不能在编译期确定值的常量,不会被放入调用者的常量池
/**
 * 1. 当一个常量的值不能在编译器确定, 那么该常量就不会被放到调用类的常量池中
 * 2. 在程序运行时, 会导致主动使用这个常量所在的类, 便会导致该类被初始化
 */
public class ClassLoad_03 {

    public static void main(String[] args){
        System.out.println(Child_03.str);
    }
}

class Child_03 {
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("Child_03 Static code!");
    }
}
示例4:数组类型的对象的初始化及助记符
/**
 * 1. 当一个类被首次主动使用(7种主动使用情况的第1种)时, 会被初始化
 * 2. 不是首次主动使用的时候, 则不会再去初始化了
 * 3. 对于数组类型, 其类型是由jvm在运行期动态生成的, 表示为[Lxxx.xxx.xxx
 *    父类型为Object
 * 4. 对于数组, Java DOC将构成元素称之为Component, 实际就是将数组降低一个
 *    维度后得到的类型
 */
/**
 * 助记符:
 * 1. anewarray 表示创建一个引用类型(如类, 接口, 数组)的数组, 并将其压入栈顶
 * 2. newarray  表示创建一个指定的基本类型(如int, float)的数组, 并将其压入栈顶
 */
public class ClassLoad_04 {

    public static void main(String[] args){
        Child_04[] array = new Child_04[4];
        System.out.println(array.getClass());
        System.out.println("----------");
        Child_04 child_1 = new Child_04();
        System.out.println("----------");
        Child_04 child_2 = new Child_04();
    }
}

class Child_04 {

    static {
        System.out.println("Child_04 Static Code!");
    }
}
示例5:父子接口的初始化
package com.lizza;

/**
 * 1. 当一个接口初始化时, 并不要求其父接口都完成了初始化
 * 2. 只有在真正使用到父接口时(比如引用了父接口中定义的常量), 才会完成初始化
 */
public class ClassLoad_05 {

    public static void main(String[] args){
        System.out.println(Child_05.b);
    }
}

interface Parent_05 {
    int a = 5;
}

interface Child_05 extends Parent_05 {
    int b = 6;
}
示例6:静态变量在类的准备阶段和初始化阶段的状态
package com.lizza;

/**
 * 1. 类的准备阶段: 静态变量会被赋予默认的初始值
 * 2. 类的初始化阶段: 静态变量会被赋予正确的初始值
 */
public class ClassLoad_06 {

    public static void main(String[] args){
        Singleton singleton = Singleton.getInstance();
    	System.out.println("count_1: " + Singleton.count_1);
    	System.out.println("count_2: " + Singleton.count_2);
    }

}

class Singleton {

    public static int count_1 = 1;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        count_1 ++;
        count_2 ++;
    }

    /** 注意count_2的位置: 在类的准备阶段, 静态变量被赋予默认值; 初始化阶段, 静态变量被赋予正确的值 **/
    public static int count_2 = 0;

    public static Singleton getInstance() {
        return singleton;
    }
}

结果:

count_1: 2
count_2: 0
示例7:初始化一个类(接口)不会初始化其父接口
/**
 * 1. 初始化一个类不会初始化其父接口
 * 2. 初始化一个接口不会初始化父接口
 * 3. 只有在真正使用到父接口时(比如引用了父接口中定义的常量), 才会完成初始化
 * 4. 当一个类被初始化的时候, 它所实现的接口是不会被初始化的; 但是接口会被加载
 * 5. 当一个类的变量为常量时, 使用该常量, 不会导致该类被加载, 更不会被初始化
 */
public class ClassLoad_05 {

    public static void main(String[] args){
        System.out.println(Child_05.b);
    }
}

class Parent_05 {
    public static Thread thread = new Thread(){
        {
            System.out.println("Parent_05 inited!");
        }
    };
}

class Child_05 extends Parent_05 {
    public static int b = 6;
}

七、总结

在这里插入图片描述

  • 加载:将二进制的.class文件从磁盘读入内存中
  • 验证:确保被加载的类的正确性
  • 准备:为类变量分配内存,设置默认值(非正确的初始值)
  • 解析:将类的常量池中类,接口,字段,方法的符号引用变为直接引用
  • 初始化:为变量赋予真正的初始值
  • 类实例化:为新的对象分配内存;为实例变量赋默认值;为实例变量赋正确的初始值;Java编译器为它编译的每一个类都至少生成一个实例初始化方法,在java的class文件中,这个实例初始化方法被称之为“”;针对源代码中每一个类的构造方法,java编译器都产生一个方法

源码地址:https://github.com/KJGManGlory/jvm.git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值