跳到主要内容

05、JVM 实战 - JVM类加载器即类加载过程

类加载器

类加载器子系统负责从文件系统或者网络中加载class文件,.class文件在文件开头有特定的文件标识(CAFEBABE)。

类加载器(ClassLoader)只负责class文件的加载,至于它是否可以运仃,则由执行引擎(ExecutionEngine)决定。

加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串、字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

(可以在当前class位置下使用指令:javap -v 类名.class 进行反编译)

 

1、 classfile存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例;
2、 classfile加载到JVM中,被称为DNA元数据模板,放在方法区;
3、 在.class文件->JVM(中的类加载器来实现的)->最终成为元数据模板,此过程就要一个运输工具(类装载器ClassLoader),扮演一个快递员的角色以二进制流来加载的;
 

类加载过程

 

加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区(永久代,元空间)的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载.class文件的方式

  • 从本地系统中直接加教;
  • 通过网络获取,典型场景: web Applet;
  • 从zip压缩包中读取,成为日后jar、war格式的基础;
  • 运行时计算生成,使用最多的是:动态代理技术;动态代理
  • 由其他文件生成,典型场景:JSP应用;
  • 从专有数据库中提取.class文件,比较少见;
  • 从加密文件中获取,典型的防class文件被反编译的保护措施

链接

验证 (verify)

目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备 (prepare)

为类变量分配内存并且设置该类变量的默认初始值,即零值

//prepare:a = 0 ---> initial : a = 1
//准备阶段设置默认初始值,初始化阶段显示赋值
private static int a = 1;

没有用final修饰的static变量,准备阶段为类变量设置默认初始值,初始化阶段显示赋值。

//编译时分配,准备阶段会显式初始化
private final static int a = 1;

有用final修饰的static变量,被认为是常量,编译的时候就会被分配到方法区,准备阶段会显式初始化(因为final在编译的时候就会分配了,准备阶段会显式初始化)。

这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
什么是实例变量

解析 (initial)

  • 将常量池内的符号引用转换为直接引用的过程。
  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

初始化

  • 初始化阶段就是执行类构造器方法< clinit >()的过程。
  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。(如果类变量或静态代码块没有则不会有类构造器方法)
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • ()不同于类的构造器。(关联:构造器是虚拟机视角下的 ()))若该类具有父类,JVM会保证子类的()执行前,父类的 ()已经执行完毕。
  • 虚拟机必须保证一个类的 ()方法在多线程下被同步加锁。

初始化:主要对静态变量进行初始化。

初始化方式两种

1、声明静态变量时指定的值。
2、使用静态初始化块为其指定初始值。

JVM会按他们的顺序运行。

初始化包含下面步骤

1、如果该类没有被载入和连接。则先载入并连接该类。
2、如果他的父类没有被初始化。则先初始化他的父类
3、如果类中有初始化语句,则系统依次运行。

 

实例化和初始化

public class Person{
   
     

    static{
   
     
    	staticInt = 6; 
    }

    {
   
     
    	vInt =15;  
    }

    static int staticInt = 3;

    int vInt = 10;

    Person(){
   
     

        staticInt = 9;

        vInt = 20;

    }

}

----------------------------
Person  p = new Person();

对象p实例化过程:

1、JVM会看有个变量p。然后就给它分配一个空间(这里的空间指的是指针,而不是实际的对象)

2、分完了以后发现它须要Person这个类来进行实例化,然后就到内存里找。看这个类有没有被载入到内存里来,假设有救直接用了。假设没有就会进行载入。我们就来说没有的情况

3、载入的时候。将类的class文件读入内存。并为之创建一个java.lang.class对象。

这里须要重点说一下。在创建这个对象的时候,就会保存这个类的全部信息。比方这个类有哪些属性(静态的非静态的都包含)。有哪些方法(静态的非静态的都包含),有什么代码块。都会被记录。

4、把类的二进制数据合并到JRE中。检查被载入的类是否有正确的内部结构,并和其它类协调一致。JVM跑到java.lang.class对象里看看,都有啥静态变量。为他们分配内存。设置默认值,将类的二进制数据中的符号引用替换成直接引用。

5、然后对静态Field进行初始化,初始化方式两种:声明静态Field时指定的值。使用静态初始化块为其指定初始值。JVM会按他们的顺序运行。这个地方须要注意:如上代码,静态的变量staticInt最后会被赋值为3,由于静态代码块在声明之前。JVM是先跑到java.lang.class对象看有什么静态变量,给他分个空间,然后再运行的声明和静态代码块语句。因此初始化之后值为3.

现在,345都是类的载入和初始化。我们再来看看都做了些什么:生成java.lang.class对象。有该类里的属性方法代码块的全部信息。再为静态属性分配了内存并运行了静态代码块。按顺序把静态属性给初始化了。

这里并没有为非静态属性分配内存,也没有运行构造函数和非静态代码块,一句话总结就是:记录下这个类的全部属性和方法代码块等信息。为静态的变量分内存并赋值。

初始化之后,如今JVM改依据这个初始化好的类信息来进行实例化了。先前被初始化好的静态变量会被全部实例共享。静态代码块将不会再被运行,相当于失效了。我们来看看JVM接下来要干嘛?

6、JVM跑到java.lang.class对象里看一看。有哪些实例变量须要分配内存的,跟静态变量类似的,JVM先给实例变量分内存,分完之后,运行代码块和声明,在这里vInt为10。

7、最后运行构造函数,运行完后。vInt变成了20,staticInt变成了9,然后改构造函数隐性的返回一个Person实例对象给变量p。

实例化参考文章:https://blog.csdn.net/qq_41306795/article/details/99959534

可以简单认为实例化中包含着初始化过程

1、 类的载入和初始化和构造函数的关系和差别?;

仅仅要在类须要实例化的时候才会运行构造函数。而类的载入和初始化却在这些情况都会被运行:创建实例 调用静态方法 訪问某个静态Field(假设该变量还是final的,则在编译阶段就能确定下来,就不会初始化) ,初始化某个类子类

2、 类的初始化会为实例属性分配内存吗?;

不会为实例属性分配,仅仅有在实例化的时候才会分内存

总结

1、 将类的class文件读入内存,并为之创建一个java.lang.class对象;
2、 把类的二进制数据合并到JRE中检查被载入的类是否有正确的内部结构并和其它类协调一致,并为静态变量分内存;
3、 为静态变量初始化赋值;

第3点即为初始化

4、 为非静态变量分内存并赋值;
5、 构造函数,返回构造好的对象;

1到5步为实例化过程简介

假设有子类父类关系的时候:

父类和子类的class文件都载入到内存,当父类。和子类有Static时。先初始化父类的Static,再初始化子类的Static,再初始化父类的其它成员变量->父类构造方法->子类其它成员变量->子类的构造方法。