java类加载机制

 

    最近面试被问到类加载机制,一时不知道该怎么回答,平时学习走的太快,忽略了基本的知识的研究,虽然断断续续使用java快1年多了,但是有些基础知识还是不清楚,没有仔细研究过,吃过亏,查了写资料,总结一下自己的理解!

概述:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机类加载机制。在Java语言中,类的加载、连接和初始化过程都是在程序运行期间完成的,这是java作为动态语言的基础。

为了加深理解,找了几个问题:

1、java代码被编写后是怎么执行的?

2、什么是类加载?什么时候进行类加载?

3、什么是类初始化?什么时候进行类初始化?

5、什么时候会为变量分配内存?

6、什么时候会为变量赋默认初值?什么时候会为变量赋程序设定的初值?

7、类加载器是什么?

把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”

8、如何编写一个自定义的类加载器?

在Java中,类装载器把一个类装入JVM中,要经过以下步骤:

     (1) 装载:查找和导入Class文件;

     (2) 链接:把类的二进制数据合并到JRE中;(java run env)

        (a)校验:检查载入Class文件数据的正确性;

        (b)准备:给类的静态变量分配存储空间;

        (c)解析:将符号引用转成直接引用;

     (3) 初始化:对类的静态变量,静态代码块执行初始化操作

 

如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的动态绑定。 为什么???
在了解各个阶段的大概作用之前,还需要解决的一个问题就是虚拟机在何时会对类进行加载。实际上虚拟机规范没有对类加载的时机进行规定,而规定了类初始化的时机,并且规定有且只有下面5种情况需要立即对类进行初始化(在初始化之前自然需要进行加载、验证、准备几个阶段)。 
1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这四条指令的场景Java代码场景是:使用new关键词实例化类的对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法的时候。 
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行过初始化,则需要先进行初始化。 
3)当初始化一个类的时候,如果发现其父类还没有初始化,则需要先初始化其父类。 
4)当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类 
5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。 
上面这五种方式被称为一个类的主动引用,除此之外所有引用类的方法都不会触发初始化,称为被动引用(比较常见的就是对类常量的引用,另外通过子类来引用父类的静态变量也属于被动引用,不会触发子类的初始化)。对于接口和类的初始化规则还有一些不同,主要是对类进行初始化的时候要求其父类的初始化已经全部完成了,但对接口进行初始化的时候并不要求其父接口都完成了初始化,只要在真正使用到父接口(如引用父接口中静态变量)才会初始化。下面看下加载、验证、准备、解析、初始化这5个阶段各自的工作。 

 

一、加载

加载阶段主要完成下面三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。(包名.外部类名$内部类名
  • 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构(这里可以理解为字节流的格式是虚拟机规范规定的,而每个虚拟机中对Java类的数据结构有自己的规定,这一步将外部的二进制字节流转换为虚拟机需要的格式存储在方法区之中)。
  • 在内存中共生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。

二、验证

验证是连接阶段的第一步,这一步的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机自身的安全。其实如果纯粹是从Java源码编译得到的Class文件,自身是可以确保安全的,但是因为Class文件可以由任何途径产生(甚至可以由十六进制编辑器直接编写来产生Class文件),所以虚拟机很有必要对输入的字节流进行验证以维护自身的安全。 
验证阶段需要完成四个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。 
1)文件格式验证主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,这个阶段是基于二进制字节流进行的,验证的目的是为了保证输入的字节流符合Class文件规范能够正确的解析并存储于方法区内,通过这个阶段的校验之后字节流才能进入到内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

2)元数据验证和字节码验证主要是对字节码的语义分析和对数据流和控制流进行分析,做一些确保代码逻辑的验证工作;

3)最后的符号引用验证发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在链接的第三个阶段–解析阶段。符号引用验证可以看成是对类自身以外的信息进行匹配性校验的过程,简单的来说就是看下符号引用通过字符串形式描述的类是否存在并且类的定义是否规范。

三、准备

准备阶段是正式为类实例变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里的类变量指的是被static修饰的变量,不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中;另外这里的初始值不是代码中指的初始值而是变量数据类型对应的零值(int为0,String为null等)。

四、解析

解析阶段简单的来说就是虚拟机将常量池内的符号引用替换为直接引用的过程。具体的过程牵涉到Class文件格式中符号引用的规定,在这里就不展开了。

五、初始化

初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序通过自定义类加载器参与之外,其余动作完全由虚拟机主导。到了初始化过程,才会真正开始执行类中定义的Java程序代码。这一步主要就是执行静态变量的初始化,包括静态变量的赋值和静态初始化块的执行。这里需要引入一个类构造器<clinit>()方法,下面对其进行简单的介绍:

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态初始化块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态初始化块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量静态语句块中可以赋值但是不能访问(这里有一点需要注意,静态初始化块和静态变量赋值语句执行顺序是按定义顺序来的,并不是说初始化块一定会在静态赋值语句之后执行)。
  • <clinit>()方法与类的构造函数不同,它不需要显示的调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  • <clinit>()方法对于类或接口来说不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口中不能使用静态初始化块,但是仍有static变量的赋值操作,所以也会有<clinit>()方法,但是接口执行<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用到时,才会执行<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待。

六、使用

 

七、卸载

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值