java jvm虚拟机类加载过程

 

加载

在加载阶段, 虚拟机需要完成以下3件事情:
1) 通过一个类的全限定名来获取定义此类的二进制字节流。
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3) 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
虚拟机规范的这3点要求其实并不算具体, 因此虚拟机实现与具体应用的灵活度都是相当大的。 例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条, 它没有指明二进制字节流要从一个Class文件中获取, 准确地说是根本没有指明要从哪里获取、 怎样获取。虚拟机设计团队在加载阶段搭建了一个相当开放的、 广阔的“舞台”, Java发展历程中, 充满创造力的开发人员则在这个“舞台”上玩出了各种花样, 许多举足轻重的Java技术都建立在这一基础之上, 例如:从ZIP包中读取, 这很常见, 最终成为日后JAR、 EAR、 WAR格式的基础。从网络中获取, 这种场景最典型的应用就是Applet。

 验证

  Java语言本身是相对安全的语言( 依然是相对于C/C++来说),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、 将一个对象转型为它并未实现的类型、 跳转到不存在的代码行之类的事情, 如果这样做了, 编译器将拒绝编译。 但前面已经说过, Class文件并不一定要求用Java源码编译而来, 可以使用任何途径产生, 甚至包括用十六进制编辑器直接编写来产生Class文件。 在字节码语言层面上, 上述Java代码无法做到的事情都是可以实现的,至少语义上是可以表达出来的。 虚拟机如果不检查输入的字节流, 对其完全信任的话, 很可能会因为载入了有害的字节流而导致系统崩溃, 所以验证是虚拟机对自身保护的一项重要工作。

验证阶段大致上会完成下面4个阶段的检验动作:

文件格式验证:

  验证字节流是否符合Class文件格式的规范

元数据验证:

  对类的元数据信息进行语义校验 

字节码验证:

  主要目的是通过数据流和控制流分析,确定程序语义是合法的、 符合逻辑的。 

符号引用验证:

  符号引用验证可以看做是对类自身以外( 常量池中的各种符号引用) 的信息进行匹配性校验, 通常需要校验下列内容:符号引用中通过字符串描述的全限定名是否能找到对应的类。在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。符号引用中的类、 字段、 方法的访问性( private、 protected、 public、 default) 是否可被当前类访问。 

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段, 这些变量所使用的内存都将在方法区中进行分配。 

public static int value=123;   验证阶段value=零值
#因为这时候尚未开始执行任何Java方法, 而把value赋值为123的putstatic指令是程序被编译后, 存放于类构造器< clinit> ( ) 方法之中, 所以把value赋值为123的动作将在初始化阶段才会执行。
public static final int value=123; 验证阶段value=123
#如果类字段的字段属性表中存在ConstantValue属性, 那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值。编译时Javac将会为value生成ConstantValue属性, 在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 

  • 符号引用( Symbolic References):

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可。 符号引用与虚拟机实现的内存布局无关, 引用的目标并不一定已经加载到内存中。 各种虚拟机实现的内存布局可以各不相同, 但是它们能接受的符号引用必须都是一致的, 因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。(编译的时候,不知道存储地址,用符号)

  • 直接引用( Direct References):

直接引用可以是直接指向目标的指针、 相对偏移量或是一个能间接定位到目标的句柄。 直接引用是和虚拟机实现的内存布局相关的, 同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。 如果有了直接引用, 那引用的目标必定已经在内存中存在。(加载后,知道在内存中的地址,吧之前在class中的符号替换为指针、或句柄)

  解析动作主要针对类或接口、 字段、 类方法、 接口方法、 方法类型、 方法句柄和调用点限定符7类符号引用进行 
关于理解“将常量池内的符号引用替换为直接引用”  参见:  https://blog.csdn.net/qq_34402394/article/details/72793119

初始化

前面的动作完全由虚拟机主导和控制。 到了初始化阶段, 才真正开始执行类中定义的Java程序代码( 或者说是字节码) 。

public class Test{
    static{
        i=0; //给变量赋值可以正常编译通过
        System.out.print( i) ; //这句编译器会提示"非法向前引用"
    }
    static int i=1;
}

 <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

<clinit>() 方法类和接口区别:

虚拟机会保证在子类的<clinit>() 方法执行之前,父类的<clinit>()方法已经执行完毕。也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作

  • 接口

接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。

  虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。同一个类加载器下,一个类型只会初始化一次。

ps:可以想一下,为什么mysql使用jdbc的驱动是需要 Class.forName("com.mysql.jdbc.Driver");

为什么不直接new  com.mysql.jdbc.Driver(),对象而是要去DriverManager注册。

 参考《深入理解java虚拟机》

原文地址:https://www.cnblogs.com/yanghaolie/p/11656912.html