java虚拟机规范-加载、链接与初始化
前言
java虚拟机是java跨平台的基石,本文的描述以jdk7.0为准,其他版本可能会有一些微调。java代码本身并不能为jvm识别,实际上在jvm中的表现形式为Class对象,一个java类从字节码到能够在jvm中正常运行,需要经过加载-》链接-》初始化三个步骤。
引用
虚拟机的启动
- java虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类来完成,这个类是由虚拟机的具体实现指定。紧接着,JAVA虚拟机链接这个初始类,初始化并调用它的main方法。之后整个执行过程都是由对此方法的调用开始。
- 启动过程如图所示:
加载
类加载器层次结构图
- 在java中,所有的类都是对其第一次使用时,动态加载到JVM中。当程序创建第一个对类的静态成员(方法、变量)的引用时,就会加载这个类。这个证明了构造器也是类的静态方法,即使在构造器之前并没有使用static关键字,使用new操作符创建类的新对象也会被当做对类的静态成员的引用。
定义
- 注意
加载
只是类加载中的一个阶段,在加载阶段虚拟机主要做以下三件事情:- 通过一个类的全限定名来获取此类的二进制字节流,(例如class文件中的部分数据、zip包、applet等,执行该步骤的模块称为
类加载器
) - 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 通过一个类的全限定名来获取此类的二进制字节流,(例如class文件中的部分数据、zip包、applet等,执行该步骤的模块称为
- 在加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在
方法区
中,然后在内存中实例化一个java.lang.class对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。 - java类的加载是由类加载器来完成的。类加载器分为两类:
- 启动类加载器(bootstrap),JVM原生提供,使用C++实现(注意这里特指hotspot虚拟机,有一些不是)
- 用户自定义类加载器(user-defined),用户自定义实现,继承自java.lang.ClassLoader类。
- 类的加载方式分为两种:显式加载和隐式加载,这两种方式都是调用classloader类中的loadClass方法来完成类的实际加载工作的。直接调用Classloader中的loadClass方法是另外一种不常用的显式加载类的技术。
- 显式加载:使用Class.forname的方式就是显式加载
- 隐式加载:使用new创建实例就是隐式加载。
- 类加载器有很多用途,例如java热替换技术,jvm中相同类的隔离等。
链接
- 链接类或接口包括验证、准备、解析。其中解析是可选的部分。
- java虚拟机规范允许灵活的选择链接发生的时机,但是必须符合以下规范:
- 在类或者接口被链接之前,它必须被成功的加载过
- 在类或者接口初始化之前,它必须被成功的验证及准备过
- 程序的直接或者间接行为可能会导致链接发生,链接过程中检查到的错误应该在请求链接的程序处被抛出。
验证
- 验证(verification)阶段用于确保类或者接口的二进制表示结构是正确的。验证过程中可能会导致某些额外的类或者接口被加载进来,但是不应该导致它们也需要验证或者准备。
准备
- 准备(preparation)阶段的任务是为类或者接口的静态字段分配空间,并用默认值初始化这些字段,这个阶段不会执行任何的虚拟机字节码指令。(注意在初始化阶段会有显式的初始化器来初始化这些静态字段,所以准备阶段不做这些事情),注意以下几点:
- 准备阶段进行内存分配的仅包括类变量(被static修饰),不包括实例变量。实例变量会在对象实例化时随着对象一起分配到java堆中。
- 示例:
- 这里的初始值“通常情况下”是数据类型的零值,例如
public static int val=23;
,那么在准备阶段过后,val的值将设置为0,而不是23。而把val值赋值为23的putstatic指令是程序被编译后,存放于类的构造器<cinit()>方法之中,所以把val值赋值为23的动作将在初始化阶段才会执行,但是如果类的字段属性表中存在ConstantValue属性(final修饰符),那么在准备阶段就会初始化完成,例如public static final int val=23
,那么在准备阶段的val值就为23.
- 这里的初始值“通常情况下”是数据类型的零值,例如
- 具体的代码和字节码参加下图:
解析
解析(Resolution)是根据运行时常量池的符号引用来动态决定具体值的过程
,java虚拟机指令(anewarray,checkcast,getfield,getstatic,instanceof,putstatic,new,invokespecial,invokevirutal)等将符号引用指向运行时常量池。执行上述任何一条指令都需要对它的符号引用进行解析。- 本步骤是符号引用,它与直接引用的区别在于:
- 符号引用只能告诉你怎么无歧义的定位到目标,该值明确规定在class文件中
- 直接引用是直接指向目标,(指针、偏移量、句柄)。直接引用和虚拟机实现的内存布局相关,
同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般都不相同
。
初始化
- 初始化是类加载的最后一步,在前面的类加载过程中,基本上动作都是由虚拟机主导和控制(除了用户自定义类加载器)。到了初始化阶段才开始真正的执行类定义中的JAVA程序代码(或者说是字节码)。
初始化阶段可以看做是执行类构造器<cinit()>方法的过程
:- 该方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static)合并而成,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,
静态语句块中只能访问到之前定义的变量,但是可以赋值。
比如下面的这段代码: - 虚拟机会保证父类的<cinit()>方法一定在子类之前执行,因此第一个执行该方法的类一定是java.lang.Object。因此
父类的赋值操作一定是在子类之前,即必须把父类中的所有赋值操作执行完毕后才会执行子类的赋值操作
。 - 如果一个接口/类中没有静态语句块,也没有对变量的赋值操作,那么编译器就不会为该类生成<cinit()>方法。
- 多个线程去初始化同一个类,那么只有一个线程去执行该类的<cinit()>方法,其他线程都需要阻塞等待,注意同一个类加载器中,一个类型只会被初始化一次。
- 该方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static)合并而成,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,
案例
案例一-关于非法向前引用
- 仍然以上面的代码为例,
非法向前引用
这种编译检查,是未了防止静态字段在被初始化之前就被独走默认值,这会导致问题难以诊断。可能有细心的读者会发现在方法体中不会出现该问题,比如下面的这段代码: - 这段代码不会抛错,最后执行结果为0.
为什么方法和类可以消除向前引用,而变量不可以呢?
这个是因为java运行时为了实现向前引用,在初始化所有字段之前会把所有的字段添加到符号表中,以便可以调用这些字段。不过由于还没有初始化这些字段,所以符号表中所有字段都使用默认的值。上图中的代码最后返回的额结果也是0.
案例二
- 上述这段代码其实是有问题的,真正运行的时候会抛NPE:
- 在java虚拟机启动的过程前期,加载和链接过程都是没有问题的。但是在初始化的步骤(执行<cinit()>方法)中,由于编译器收集顺序是语句在源文件中出现的顺序,而res变量的赋值操作在add静态语句块之后。因此在执行add方法的时候,res变量只有一个默认值
null
。因此会导致该段代码抛NPE
- 在java虚拟机启动的过程前期,加载和链接过程都是没有问题的。但是在初始化的步骤(执行<cinit()>方法)中,由于编译器收集顺序是语句在源文件中出现的顺序,而res变量的赋值操作在add静态语句块之后。因此在执行add方法的时候,res变量只有一个默认值
java虚拟机规范-加载、链接与初始化的更多相关文章
- Java类的加载 链接 初始化
原文地址 Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码到能够 ...
- JVM详解之:类的加载链接和初始化
目录 简介 加载 运行时常量池 类加载器 链接 验证 准备 解析 初始化 总结 简介 有了java class文件之后,为了让class文件转换成为JVM可以真正运行的结构,需要经历加载,链接和初始化 ...
- Java类的加载、链接和初始化
一.Java的类加载机制回顾与总结: 我们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java字节代码转换成JVM中的java.lang.Class类的对象.这样 ...
- java 类的加载,链接,初始化
本篇的话题,讨论Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码 ...
- java类从加载、连接到初始化过程
类加载器 在了解Java的机制之前,需要先了解类在JVM(Java虚拟机)中是如何加载的,这对后面理解java其它机制将有重要作用. 每个类编译后产生一个Class对象,存储在.class文件中,JV ...
- JAVA类的加载、连接与初始化
JAVA类的加载.连接与初始化 类的声明周期总共分为5个步骤1.加载2.连接3.初始化4.使用5.卸载 当java程序需要某个类的时候,java虚拟机会确保这个类已经被加载.连接和初始化,而连接这个类 ...
- Java中类的加载、连接和初始化
Java中类的加载.连接和初始化 类的加载.连接和初始化 先介绍一下JVM和类 JVM和类: 当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多 ...
- 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】
目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...
- jvm系列(一):java类的加载机制
java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装 ...
随机推荐
- 深入.NET平台和C#编程的错题
29)有如下C# 代码,则下面选项中说法正确的是(BC).public class A { } Person public class B : A { } StudentA a = new A( ...
- Eureka 源码分析之 Eureka Server
文章首发于公众号<程序员果果> 地址 : https://mp.weixin.qq.com/s/FfJrAGQuHyVrsedtbr0Ihw 简介 上一篇文章<Eureka 源码分析 ...
- linux工具之screen
screen官方网址:http://www.gnu.org/software/screen/ 参考文章:http://man.linuxde.net/screen 简介 Screen是一款由GNU计划 ...
- Cyclic Nacklace HDU 3746 KMP 循环节
Cyclic Nacklace HDU 3746 KMP 循环节 题意 给你一个字符串,然后在字符串的末尾添加最少的字符,使这个字符串经过首尾链接后是一个由循环节构成的环. 解题思路 next[len ...
- python学习-第四天补充-面向对象
python学习-第四天补充-面向对象 python 私有 --name mangling(名字修改.名字) 在命名时,通过使用两个下划线作为开头,可以使得这个变量或者函数编程私有的,但是这个其实的p ...
- js分页加载数据
<script type="text/javascript"> $(function(){ $.ajax({ type: "post", dataT ...
- 列表、元组和range
小知识点 s = " 5 " print(int(s)) print(s.replace(" ","")) 结果: 5 5 id()#获取对 ...
- 数学: HDU1098 Ignatius's puzzle
Ignatius's puzzle Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others ...
- k3 cloud的单据存储在业务对象表中
k3 cloud的单据存储在业务对象表中,表名为T_META_OBJECTTYPE,查询表名和对应的表: select FNAME,FBASEOBJECTID from T_META_OBJECTTY ...
- python之路之——操作系统的发展历史
阅读目录 手工操作 —— 穿孔卡片 批处理 —— 磁带存储和批处理系统 多道程序系统 分时系统 实时系统 通用操作系统 操作系统的进一步发展 操作系统的作用 手工操作 —— 穿孔卡片 1946年第一台 ...