JVM一:类加载器

本文详细介绍了Java类加载的过程,包括加载、验证、准备、解析和初始化五个阶段,并探讨了JVM内存中的方法区、运行时常量池和字符串常量池等概念。同时,分析了类加载的触发条件和验证的重要性,以及静态解析和动态解析的区别。此外,还讨论了静态变量、实例变量和常量的存储位置。最后通过代码示例解释了类加载与对象创建时的内存行为。
摘要由CSDN通过智能技术生成

不必死记硬背,理解之后,每次写代码的时候就去思考该类在 JVM 的运行过程

简介

JVM 里的搬运工和检察官,把 .class 文件加载到机器内存中,并在内存中构建 Java 类的原型—类模版对象!

类加载过程

第一阶段:加载

读取 .class 文件,将其转化为某种静态数据结构存储在方法区,并在堆内存中生成一个 java.lang.Class 对象。

第二阶段:连接(验证、准备、解析)

  • 验证:主要验证第一阶段加载在内存中的 class 静态结构,确保其符合 JVM 规范,保证这些代码运行后不会危害虚拟机。

    1)文件格式验证:魔数是否以 0xCAFEBABY 开头、主次版本号、编码是否符合等等,在上一步加载阶段其实已经完成
    2)元数据验证:是否有父类、是否覆盖了 final 、是否实现了父类接口中所有方法等等
    3)字节码验证:确定程序语意是否合法、主要验证 class 文件中的 code 属性(存储了编译后的字节码指令)
    4)符号引用验证:对类自身以外的引用属性进行匹配性验证,这个验证过程其实是在解析阶段完成的
    5)-Xverifynone:关闭大部分类的验证

  • 准备:为类的静态变量分配内存设置默认值

    注意:类变量(静态变量)会分配在方法区的运行时常量池中

  • 解析:符号引用转换为直接引用(对引用的类、接口、方法、字段进行处理)

    符号引用:字面上的表现形式,看到这个符号就知道代表哪个东西,此时被引用的目标不一定已经被加载到虚拟机内存了
    直接引用:可以理解为在内存的地址(指向目标的指针、相对偏移量、间接定位的句柄),此时被引用的目标一定已经被加载到虚拟机内存了

    这个阶段可以是在初始化环节之后进行,实现所谓的“后期绑定”

第三阶段:初始化

类变量(静态变量、实例变量)初始化为指定的值(如果指定的话),并执行静态代码块,主动权由 JVM 转移到应用程序,触发初始化有以下场景:

new的时候、设置或读取一个静态字段、调用一个静态方法
进行反射调用
JDK8 新引用了由 default 修饰的接口方法,如果该接口的实现类发生了初始化,那么该接口要先一步初始化

第四阶段:使用

由 JVM 动态调用执行

第五阶段:卸载

当一个 class 对象不再被引用,那么它的生命周期也将结束,对应方法区的数据也会被卸载
JVM 自带的 ClassLoader 装载的类是不会出现卸载的,只有我们自定义的 ClassLoader 装载的类才可以卸载

命令

// 把一个 .class 文件转换成可读的 txt 文件
javap -v Test.class > Test.txt
// 反编译
javap -c Test.class 

思考

javac 编译过程中已经进行了一次校验怎么在 jvm 还要再进行一次

1)防止出现 .class 文件本身就是非法的,采用了不规范的编译器,或者恶意修改了 .class 文件
2)防止加载 .class 文件过程中发生数据丢失
3)版本检查,jdk8 编译的 .class 文件不能在 jdk8 以下的版本运行

关于方法区的实现

方法区是 JVM 规范中的抽象概念,永久代元空间均是其在不同版本的实现方式,概念不要混淆了。
JDK6 以前:使用永久代实现方法区,用来存储:类的元信息(类型信息、域信息、方法信息)、JIT代码缓存、静态变量、字符串常量池
JDK7 版本:使用永久代实现方法区,静态变量、字符串常量池移到堆内存中
JDK8 以后:使用元空间实现方法区

解析阶段的一些思考

A.class 中引用了 B.class,此时 B.class 并未加载到内存中,在 A 中用符号 Y 来代表 B,那么 Y 就是 BA符号引用
静态解析:当加载 A 的时候发现 B 没有被加载,那么就会自动触发 B 的加载,对应的 Y 也会被替换为 B内存中的实际地址,这个实际地址就是直接引用
动态解析:对于 Java 开发来说多态是很重要的特性,如果 B 作为接口有多个实现类,当加载 A 的时候,无法确定加载 B 的哪一个实现类,只有当运行过程中发生了调用的时候,这时虚拟机调用栈中会有具体的信息来确定了要用哪个实现类,这时才会把 Y 替换成 B 的实现类的直接引用

静态变量、实例变量、常量、静态常量池、运行时常量池、字符串常量池

  • 静态变量:由 static 修饰的类变量,被该类的所有实例对象共享
  • 实例变量:非 static 修饰的类变量,只被当前实例对象拥有
  • 常量:其实就是 final 修饰得变量,可用来修饰静态变量、实例变量、局部变量
  • 静态常量池:可理解为 Class 文件的常量池,包含了字面量(声明为 final 的变量)符号引用量编译期产生
  • 运行时常量池:把静态常量池加载到内存中形成的,经常说的常量池其实就是运行时常量池,相比于静态常量池多了些动态性,在运行期间也可以将常量放在常量池中,每个类在方法区都会有一个运行时常量池
  • 字符串常量池:字符串对象在堆内存创建后,会把引用存放到字符串常量池中,是由 StringTable 实现的,是一个哈希表。

声明为 final 的常量一定会在编译期投放在 Class 文件的常量池吗

不一定,取决于这个值能不能在编译期就确定下,钉是钉铆是铆,整个随机数肯定不能被当作常量。

对一个类进行调用的时候,一定会触发这个类的类加载吗

不一定,如果是调用了这个类中已经确定的常量(投放在 Class 文件常量池的),这时就不需要对该类进行类加载

代码悟道

public class Test {

    public static void main(String[] args) {
        String a = "hello";
        String b = "hello";
        String c = new String("hello");
        String d = new String("hello");
        // a b c d 对应的地址值差异思考
        System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(a)));
        System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(b)));
        System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(c)));
        System.err.println(String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(d)));
    }
}
public class Test {
	// 会怎样输出呢
    public static void main(String[] args) {
        System.out.println(Constant.A);
        System.out.println(Constant.B);
    }

}

class Constant {
    public static String A = "A";
    public static final String B = "B";
    static {
        System.out.println("load this static code block");
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值