什么是类加载
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
数组类通过JAVA虚拟机直接创建,不通过类加载器创建
JVM中类(类+接口)的加载和连接过程都是在程序运行期间第一次使用时动态加载的,而不是一次性加载所有类,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特性实现的。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。
类加载的过程
如上图,类加载的过程分为三个步骤(五个阶段) :加载 -> 连接(验证、准备、解析)-> 初始化。加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段可以在初始化阶段之后发生,也称为动态绑定或晚期绑定。
加载
加载:查找并加载类的二进制数据的过程
- 通过类的全限定名定位.class文件,并获取其二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
如何获取二进制数据:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP),数据库 网络等
验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证形式包括文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
这个阶段中需要注意的两点:
- 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆内存中。
- 这里所设置的初始值”通常情况”下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=10 ,那么 value 变量在准备阶段的初始值就是 0 而不是10(初始化阶段才会复制)。特殊情况(常量):比如给 value 变量加上了 fianl 关键字public static final int value=10 ,那么准备阶段 value 的值就被复制为 10。
解析
解析阶段将常量池中的符号引用替换为直接引用。在字节码文件中,类、接口、字段、方法等类型都是由一组符号来表示,其形式由Java虚拟机规范中的Class文件格式定义。在虚拟机执行特定指令之前,需要将符号引用转化为目标的指针、相对偏移量或者句柄,这样可以通过此类直接引用在内存中定位调用的具体位置.
初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器
在准备阶段,变量已经被赋过一次变量默认的初始值,而在初始化阶段,则会根据程序中编写的具体值去初始化类变量和其他资源
类构造器
区别于构造函数 ,构造函数可以由编程人员实现,而类构造器则由编译器自动生成, 方法由编译器自动收集类的赋值动作和静态语句块(static{}块)中的语句合并生成的。(执行顺序=定义顺序)
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(初始化的时机):
- 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时
- 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化
- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类
- 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化