Java 类加载与运行

Java 类加载与运行

类从加载到内存直至被卸载,整个生命周期包括:加载、验证、准备、解析(绑定)、初始化、使用和卸载7个阶段,对于不一样的 JVM 实现,这 7 个阶段可能各有重叠,但大体过程相同java

graph TB A[Class 加载] style A fill:#f90 B[Files<br/>network] C[<b>准备</b><br/>方法区<br/>static 0值] D[<b>类首次</b>初始化<br/><b>clinit</b><br/>5个初始化时机] E[加载器名字空间] O[运行时栈] P[<b>Slots</b><br/>0 this<br/>args<br/>locals] Q[Slot 复用] R[操做数栈] S[动态连接<br/>栈帧] T[动态特性] U[invoke<br/>反射] A --> B A --> C C --> D B --> E O --> P P --> Q O --> R O --> S O --> T T --> U

[TOC]数组

从类到对象

粗略来看,类的加载通常须要通过下面三个过程tomcat

  1. 经过一个类的全限定名来获取定义此类的二进制字节流 ,例如使用文件(Jar、war)、网络(Applet) 等方式
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构
  3. 生成一个表明这个类的 java.lang.Class 对象(方法区或者堆),做为方法区这个类的各类数据的访问入口

下面介绍部分细节安全

验证

class 文件不必定由 Java 编写,也可能由 C 或其余语言生成,因此为了安全,载入 class 文件时 JVM 会对 class 文件进行验证。验证 阶段大体上会完成下面 4 个阶段的检验动做:网络

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

准备阶段主要初始化方法区内存数据结构

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有 两个容易产生混淆的概念须要强调一下,首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量, 实例变量将会在对象实例化时随着对象一块儿分配在 Java 堆中。其次,这里所说的初始值“ 一般状况” 下是数据类型的零值,由于这时候还没有开始执行任何 Java 方法,成员变量的赋值编译后常置于构造器 <clinit>() 中。可是,若是类字段为 const 类型,那么在编译与准备时变量的值就已经肯定了,不须要写进构造器中多线程

public static int value       = 123; // 变量 value 在准备阶段事后的初始值为 0 而不是 123
public static final int value = 123; // 变量 value 在准备阶段的初始值为 123

解析

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

类初始化

注意,这里的初始化指的不是类对象的初始化,而是类或接口首次使用(建立对象)前所作的准备工做ide

类初始化阶段是类加载过程的最后一步,前面的类加载过程当中,除了在加载阶段用户应用程序能够经过自定义类加载器参与以外, 其他动做彻底由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)。从另一个角度来表达:初始化阶段是执行类构造器 <clinit>() 方法的过程,调用<clinit>()是一个类或接口被首次使用前的最后一项工做函数

<clinit>() 方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量, 在前面的静态语句块能够赋值,可是不能访问

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

<clinit>()方法对于类或接口来讲并不是必需的,若是一个类中没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生成 <clinit>()方法

<clinit>() 方法与类的构造函数(或者说实例构造器 <init>() 方法)不一样,它不须要显式地调用父类构造器,虚拟机会 保证在子类的 <clinit>() 方法执行以前,父类的 <clinit>()方法已经执行完毕。虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会 有一个线程去执行这个类的<clinit>()

初始化时机

JVM 规范对类初始化的时机作了明确的规定,有且仅有下面 5 种状况必须对类进行初始化,且下面 5 种方式被称为主动引用

  1. 碰见 new、getstatic、putstatic 和 invokestatic 这 4 条字节码命令时,若是类未初始化过,则须要初始化
    • new,调用构造函数初始化对象堆内存,建立对象须要知道对象的类型,而 java.lang.Class是在类初始化时载入的
    • 类中静态初始化块只会在类第一次初始化时调用,后三个静态成员访问指令须要触发首次类初始化
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时,若是类未进行过初始化,则需先触发初始化
    • 参考这里,反射须要维护对象的一个 Class 对象,而 Class 对象由类初始化载入
  3. 初始化一些类时,若是发现其父类未被初始化,则需先触发父类的初始化
    • 对于接口而言,只有用到的时候才会初始化父接口
  4. 程序的入口类(包含 main 函数的类),必定会被初始化
  5. JDK 7 中若是一个 java.lang.invoke.MethodHandle 最后解析结果 REF_getstatic 等方法句柄,且这个方法句柄对应的类没有进行过初始化,则须要先触发初始化

除上述 5 种方式(主动引用)外其余对类的应用方式均不会触发类的初始化,这些引用被称为被动引用。下面有几个被动引用的示例

  1. 经过子类引用父类的静态字段,不会致使子类的初始化
  2. 经过数组定义引用类,不会触发此类的初始化:classA a = new classA[10],不会触发 classA的初始化
  3. 常量在编译阶段会存入调用类的常量池中,本质上并无直接引用到定义常量的类,所以不会触发定义常量的类的初始化

类加载器

虚拟机设计团队把类加载阶段中的“经过一个类的全限定名来获取描述此类的二进制字节流” 这个动做放到 Java 虚拟机外部去实现,以便让应用程序本身决定如何去获取所须要的类

类加载器在类层次划分、OSGi、热部署、代码加密等领域十分重要,是 Java 技术体系中一块重要的基石

若是两个类由不一样的类加载器加载,那么即便类源自同一个 class 文件,这两个类也是不一样的(包括 equals、isAssignableFrom、isInstance ),由于每个类加载器都有本身的名字空间

双亲委派模型

开发中经常使用的 Java 类加载器有三类,这三类加载器分层,能够由底层加载器委派上层加载器去载入数据,后续再详细看

Tomcat & OSGi

若想详细学习类加载技术,能够看一看 tomcat 和 OSGi(Open Service Gateway Initiative) 的实现

运行时栈

一个栈帧中包含的内容以下:局部变量表、操做栈、动态链接、返回地址等数据,下面一一介绍

C/C++ 的栈是动态变化的,变量只有执行到指定位置才会在栈中为之分配内存,且内存大小和变量类型息息相关

局部变量表(Slots)

存储方法内部定义的局部变量和方法参数

JVM 规定局部变量的容量以变量槽(Variable Slot)做为最小单位,一个槽能够存放一个 32 位之内的数据类型。JVM 使用索引定位的方式使用局部变量表,若是是 64 位数据就同时使用相邻的两个 Slot,JVM 不容许访问 64 位数据其中一个 Slot

局部变量槽第 0 个Slot 保存 this 指针,其他变量从 1 开始依次占用槽位,先是函数的参数,再是方法内部的局部变量

JVM 并不会像初始化类变量那样初始化局部变量,因此须要手动初始化

为失效变量赋 null

JVM 栈帧中的变量有时是复用的,下面两段代码,前者触发了垃圾回收,后者未触发,就是由于 Slot 的复用

这条原则不必体如今代码中,JIT 会自动优化这类问题,此处提出这个概念是为了加深对 Slot 的理解

// 即便调用强制系统进行 GC,p 对应的内存也没有被回收
// 由于在调用 gc 时 p 依旧保持在栈中(Slots),做为 GC Root 指向堆内存
public static void main(String[] args)
{
    {
		byte[] p = new byte[64*1024*1024];
        // 部分书籍推荐对不使用的引用赋 null 值,避免回收失效
        // 部分 JIT 会优化掉下面的语句,因此具体状况具体对待
        // p = null; 
    }
	System.gc();
}

// 不少实现复用了不用的 Slot,因此下面这段代码能够触发垃圾回收
public static void main(String[] args)
{
    {
		byte[] p = new byte[64*1024*1024];
    }
    int a = 0; // a 复用了 p 的 Slot,p 对应的内存失去了引用,可被回收
	System.gc();
}

操做数栈

Java 中的指令面向操做数栈而非寄存器,故 Java 指令要操做的数据都须要保存在操做数栈中,与之相对应的是 x86 等 CPU 架构,指令的操做数通常都保存在 CPU 寄存器中

使用操做数栈的架构便于移植,不一样 CPU 有不一样的寄存器结构,移植时须要从新编译代码;固然使用操做数栈结构会下降性能

操做数栈的最大深度在编译时已写入 Code 属性的 max_stacks

动态连接

和 C/C++ 这类语言生成的可执行文件概念相似,有些代码在编译时即已写入可执行文件中(如静态连接),有些代码须要在运行时动态决断(如系统库的调用)

运行时栈都包含一个指向运行时常量池中当前栈帧所属方法的引用,这个引用的存在是为了支持方法调用过程的动态连接

返回地址

Java 中从一个函数返回有两种状况:正常 return 和异常返回

附加信息

例如调试信息等

方法调用

分派

  • 静态分派(相似 C++ 中的静态绑定)

    全部依赖静态类型来定位方法执行版本的分派动做称为静态分派。静态分派的典型应用是方法重载

  • 动态分派(相似 C++ 的动态绑定)

    • C++ 的动态绑定常见的实现方式是虚函数表和虚函数指针,虚函数指针保存在对象中,C++ 想实现多态,只能用指针(或引用)。Java 和 C++ 实现相似,不过 Java 的“虚函数指针”,在栈帧中有明确的存储位置
    • Java 在解析虚函数时会使用 Slots 槽中第 0 个位置的 this 指针获取对象的类型信息,而后查找对应的方法地址

字节码指令

  • invokestatic
  • invokespecial,调用示例构造<init>方法、私有方法和父类方法
  • invokevirtual
  • invokeinterface
  • invokedynamic,前 4 条语句函数的解析由 JVM 控制,当前指令函数解析由用户决定

Java 的动态语言特性

动态语言的特色是它的类型检测主体过程在运行期而不是编译期。以 C++ 和 Java 为例,假设 obj 为一个对象实例,则在 C++ 和 Java 中使用 obj.method() 的前提是 obj 的静态类型中声明的有 method 这个方法,不然没法经过编译。考虑语言的多态性,非动态语言的特色是:能够修改对象方法的行为,但不能调用对象中不存在的方法

动态语言就不同了,例如 JavaScript,你甚至能够在运行时为一个对象赋予全新的方法,简单来讲,动态语言在运行时查询对象信息,若是有对应方法就调用,没有就报运行时错误

Java 可使用反射和 invoke 包实现动态方法调用,示例以下

import java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

// 假设 ClassA 中实现了 println 方法
Object obj = System.currentTimeMillis()%2 == 0?System.out:new ClassA(); 
// 在 Java 中,下面用法是不合法的,编译器会提示找不到 println
// 编译器只能查询 obj 自己所包含的信息,不能查询 obj 所引用的对象的类型
// obj 指向的对象是包含 println 方法的,但 obj 的静态类型种没有 println 方法
// obj.println("Hello"); 

// JDK 7以后可使用 invoke 类库实现方法的动态调用,方法以下
getPrintlnMH(obj).invokeExact("Hello");

// 使用下面的方法得到方法并和指定的对象绑定
MethodHandle getPrintlnMH(Object receiver)
{
    // 得到方法类型,第一个参数为方法的返回值类型,后面为方法的参数类型
    MethodType mt = MethodType.methodType(void.class, String.class);
    // 从 receiver 中查找方面名为 println,形参类型为 mt 的方法,并与 receiver 绑定
    // 执行下面语句前,JVM 是不知道 receiver 有 println 这个成员函数的
    return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
}

和反射相比,反射比 invoke 中的方法更重量级。反射模拟的是代码层次的调用,MethodHandle 模拟字节码层次的调用;反射比 MethodHandle 包含了更多额外的信息,例如方法签名、属性等,后者仅仅包含方法调用信息

MethodHandle 面向全部语言,是 JVM 的特性;反射通常只用于 Java

invokedynamic

JVM 的 invokedynamic 指令和上面的 getPrintlnMH 函数所展现的功能相似,具体实现请参看其余资料

字节码执行引擎

JVM 在执行 Java 代码时有两种选择:解释执行编译执行,有时候这两种方法是同时存在的,例如包含JIT 的 JVM

先举个字节码执行过程的例子。使用 javac 命令编译下面代码,并使用 javap 查看 cal 函数的字节码:

class TestClass {
    public int cal()
    {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a+b)*c;
    }
}

上面代码对应的字节码以下,编译器可能会对字节码进行优化,因此下面过程只用于说明字节码执行过程

public int cal();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100 // 将 100 放入操做数栈
         2: istore_1          // 将栈顶数据写入局部变量第一个 Slot 中
         3: sipush        200 // 后面 4 条语句与前两条功能相同,即初始化变量 a,b,c 
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1       // 这两行把 Slot 中的值写进操做数栈
        12: iload_2
        13: iadd          // 弹出操做数栈栈顶的两个值求和并将结果入栈,此时栈顶为求和结果
        14: iload_3       // 将第三个 Slot 中的值,即 c 压入操做数栈
        15: imul          // 弹出操做数栈顶两个值求积并将结果入栈,此时栈顶为最终结果
        16: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 7
        line 12: 11
}

字节码生成与动态代理

动态代理的简单示例, 动态代理能够实现适配器(Adapter)或修饰器(Decorator) 等模式

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class TestClass {
    interface IHello {
        void sayHello();
        void sayGoodbye();
    }

    static class Hello implements IHello {
        @Override
        public void sayHello() { System.out.println(" hello world"); }
        @Override
        public void sayGoodbye() { System.out.println(" bye bye!!!"); }
    }

    static class DynamicProxy implements InvocationHandler {
        Object originalObj;

        Object bind(Object originalObj) {
            this.originalObj = originalObj;
            // 三个参数,Class Loader;须要实现的接口数组;this
            return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),\ 
                                          originalObj.getClass().getInterfaces(), \
                                          this);
        }

        // 全部原始成员函数都如下面的方式调用
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println(" welcome");
            return method.invoke(originalObj, args);
        }
    }

    public static void main(String[] args) {
        IHello hello = (IHello) new DynamicProxy().bind(new Hello());
        hello.sayHello();
        hello.sayGoodbye();
        /* // 上面两行代码编译后的结果相似于下面代码
        public final void sayHello() throws { 
            try { // m3 是绑定在 hello 上的实际方法,即原始的 sayHello 或 sayGoodbye
        		this.h.invoke(this, m3, null); return; 
        	} 
        	catch...
        	}
        */
    }
}

/* 执行结果
welcome
hello world
welcome
bye bye!!!
*/

Retrotranslator

同时执行不一样版本的 Java 代码,Retrotranslator 能够将 jdk5 的代码编译为 jdk 5之前的 class 文件

相关文章
相关标签/搜索