Java 虚拟机类加载机制和字节码执行引擎

澳门新葡亰3522平台游戏 2

引言

我们知道java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢?

前言

一 类文件结构

第一部分 走近Java

无关性基石

java有一个口号叫做一次编写,到处运行。实现这个口号的就是可以运行在不同平台上的虚拟机和与平台无关的字节码。这里要注意的是,虚拟机也是中立的,只要是符合规范的字节码,都可以被虚拟机接受,例如Groovy,JRuby等语言,都会生成符合规范的字节码,然后被虚拟机所运行,虚拟机不关心字节码由哪种语言生成。

第1章 走近Java

类文件结构

class类文件是一组以8位字节为基础的二进制流,它包含以下几个部分:

魔数和class文件版本:类文件开头的四个字节被定义为CAFEBABE,只有开头为CAFEBABE的文件才可以被虚拟机接受,接下来四个字节为class文件的版本号,高版本JDK可以兼容以前版本的class文件,但不能运行以后版本的class文件。

常量池:可以理解为class文件中的资源仓库,它包含两大类常量:字面量和符号引用,字面量包含文本字符串,声明为final的常量值等,符号引用包含类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

访问标志:常量池结束后,紧接着两个字节表示访问标志,用于识别一些类或接口层次的访问信息,例如是否是public,是否是static等。

类索引,父类索引,和接口索引集合:类索引用来确定这个类的全限定名,父类为父类的全限定名,接口索引集合为接口的全限定名。

字段表集合:用于描述接口或者类中声明的变量,但不包含方法中的变量。

方法表集合:用于表述接口或者类中的方法。

属性表集合:class文件,字段表,方法表中的属性都源自这里。

1.1 概述

二  类加载机制

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换分析和初始化,最终形成可以被虚拟节直接使用的JAVA类型,这就是虚拟机的类加载机制。

类从被加载到虚拟机内存到卸载出内存的生命周期包括:加载->连接(验证->准备->解析)->初始化->使用->卸载。

1.2 Java技术体系

初始化的5种情况:

  1. 使用new关键字实例化对象时,读取或设置一个类的静态字段,除被final修饰经编译结果放在常量池的静态字段,调用类的静态方法时。
  2. 使用java.lang.reflect包方法对类进行反射调用时。(Class.forName())。
  3. 初始化子类时,如果父类没有初始化。
  4. 虚拟机启动时main方法所在的类。
  5. 当使用JDK1.7动态语言支持时,java.lang.invoke.MethodHandle实例解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且对应类没有进行初始化。

1.3 Java发展史

类加载过程

加载

加载是类加载的第一个阶段,虚拟机要完成以下三个过程:1)通过类的全限定名获取定义此类的二进制字节流。2)将字节流的存储结构转化为方法区的运行时结构。3)在内存中生成一个代表该类的Class对象,作为方法区各种数据的访问入口。

验证

目的是确保class文件字节流信息符合虚拟机的要求。

准备

为static修饰的变量赋初值,例如int型默认为0,boolean默认为false。

解析

虚拟机将常量池内的符号引用替换成直接引用。

初始化

初始化是类加载的最后一个阶段,将执行类构造器<init>()方法,注意这里的方法不是构造方法。该方法将会显式调用父类构造器,接下来按照java语句顺序为类变量和静态语句块赋值。

1.4 Java虚拟机发展史

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。举一个例子:

package com.sinaapp.gavinzhang.bean;
import java.io.InputStream;

public class App 
{
    public static void main( String[] args )
    {
        ClassLoader myClassLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try{
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null)
                    {
                        System.out.println(fileName+ "is not find");
                        return super.loadClass(name);
                    }
                    System.out.println("fileName: "+fileName);
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                }catch (Exception E)
                {
                    throw new ClassCastException(name);
                }

            }
        };
        try {
            Object obj = myClassLoader.loadClass("com.sinaapp.gavinzhang.bean.Resource").newInstance();
            Object obj1  = Class.forName("com.sinaapp.gavinzhang.bean.Resource").newInstance();
            System.out.println(obj instanceof com.sinaapp.gavinzhang.wesound.bean.Resource);
            System.out.println(obj1 instanceof com.sinaapp.gavinzhang.wesound.bean.Resource);
        }catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

结果为:

澳门新葡亰3522平台游戏 1

可以看到,由自定义的加载类只能获取同包下的class,而系统的class不能被加载,而且由Class.forName()获取的类与自定义加载类得到的类不是同一个类。

根据五种初始化的条件,父类也会被初始化,但是,上边的代码运行结果显示,父类和接口都没有被初始化,这又是怎么回事呢?

系统提供了三种类加载器,分别是:启动类加载器(Bootstrap
ClassLoader),该加载器会将<JAVA_HOME>/lib目录下能被虚拟机识别的类加载到内存中。扩展类加载器(Extension
ClassLoader),该加载器会将<JAVA_HOME>/lib/ext目录下的类库加载到内存。应用程序类加载器(Application
ClassLoader),该加载器负责加载用户路径上所指定的类库。

我们自定义的ClassLoader继承自应用程序类加载器,当自定义类加载器找不到所加在的类时,会使用启动类加载器进行加载,当启动类加载器加载不到时,由扩展类加载,扩展类加载不到时有应用程序类加载。这也是为什么上边的代码能够成功运行的原因。

1.4.1 Sun Classic Exact VM

三  字节码执行引擎

1.4.2 Sun HotSpot VM

运行时栈帧结构

 
中讲到虚拟机栈是线程私有的,线程中会为运行的方法创建栈帧。

澳门新葡亰3522平台游戏 2

栈帧是虚拟机栈的栈元素,栈帧存储了局部变量表,操作数栈,动态连接,返回地址等信息。每一个方法的调用都对应着一个栈帧在虚拟机栈中的入栈和出栈。

局部变量表由方法参数,方法内定义的局部变量组成,容量以变量槽(Slot)为最小单位。如果该方法不是static方法,则局部变量表的第一个索引为该对象的引用,用this可以取到。

操作数栈最开始为空,由字节码指令往栈中存数据和取数据,方法的返回值也会存到上一个方法的操作数栈中。

动态连接含有一个指向常量池中该栈帧所属方法的引用,持有该引用是为了进行动态分派。

方法返回地址存放的是调用该方法的pc计数器值,当方法正常返回时,就会把返回值传递到上层方法调用者。当方法中发生没有可被捕获的异常,也会返回,但是不会向上层传递返回值。

1.4.3 Sun Mobile-Embedded VM Meta-Circular VM

方法调用

java是一门面向对象的语言,它具有多态性。那么虚拟机又是如何知道运行时该调用哪一个方法?

静态分派是在编译期就决定了该调用哪一个方法而不是由虚拟机来确定,方法重载就是典型的静态分派。

动态分派是在虚拟机运行阶段才能决定调用哪一个方法,方法重写就是典型的动态分派。

动态分派的实现:当调用一个对象的方法时,会将该对象的引用压栈到操作数栈,然后字节码指令invokevirtual会去寻找该引用实际类型。如果在实际类型中找对应的方法,且访问权限足够,则直接返回该方法引用,否则会依照继承关系对父类进行查找。实际上,如果子类没有重写父类方法,则子类方法的引用会直接指向父类方法。

1.4.4 BEA JRockit IBM J9 VM

基于栈的字节码执行引擎

不管是解释型语言还是编译型语言,机器都无法理解非二进制语言。高级语言转化成机器语言都遵循现代经典编译原理。即执行前对程序源码进行词法和语法分析,构建抽象语法树。C语言等编译型语言会由单独的执行引擎做这些工作,而Java语言等解释型语言语法抽象树由jvm完成。jvm可以选择通过解释器来解释字节码执行还是通过优化器生成机器代码来执行。

常用的两套指令集架构分别是基于栈的指令集和基于寄存器的指令集。

基于栈的指令集更多的通过入栈出栈来实现计算功能,例如1+1

    iconst_1  ;将1入栈
    iconst_1  ;将1入栈
    iadd      ;将栈顶两个元素取出相加并将结果入栈

基于寄存器的指令集更多的是使用寄存器来进行操作,例如1+1

mov eax,1 ;向eax中存1
 add eax,1 ;eax<-eax+1

总体来说,基于栈的指令集会慢一些,但是它与寄存器无关,更容易实现到处运行的目标。

1.4.5 Azul VM BEA Liquid VM

总结

又到了该总结的时候了,类加载机制面试中很容易被问到,不幸的是,当时我并没有看这方面的知识。

class类文件结构的每一个部分都可以再深入下去,类文件结构是采用结构体的方式存储的,那么怎么知道集合的长度,各个属性又是怎么被标记的。

类加载机制中有且仅有的五种触发初始化的情况。类加载器的分类。

栈帧的结构,以及方法调用。

java语言的方法调用分为静态多分派,动态单分派。

1.4.6 Apache Harmony Google Android Dalvik VM

1.4.7 Microsoft JVM及其他

1.5 展望Java技术的未来

1.5.1 模块化

1.5.2 混合语言

1.5.3 多核并行

1.5.4 进一步丰富语法

1.5.5 64位虚拟机

1.6 实战:自己编译JDK

1.6.1 获取JDK源码

1.6.2 系统需求

1.6.3 构建编译环境

1.6.4 进行编译

1.6.5 在IDE工具中进行源码调试

1.7 本章小结

第二部分 自动内存管理机制

第2章 Java内存区域与内存溢出异常

2.1 概述

2.2 运行时数据区域

  • 线程共享:
    • 堆(Heap):OutOfMemoryError
    • 方法区(Method Area):OutOfMemoryError
  • 线程私有:
    • 程序计数器(Program Counter
      Register):唯一没有规定OutOfMemoryError的区域
    • 虚拟机栈(VM Stack,java方法): OutOfMemoryError,
      StackOverflowError
    • 本地方法栈(Native Method
      Stack,本地方法):OutOfMemoryError,StackOverflowError

OutOfMemoryError(内存泄漏):虚拟机可以动态扩展,如果扩展是无法申请到足够的内存时;
StackOverflowError(栈溢出):线程请求的栈深度大于虚拟机所允许的深度时;

2.2.1 程序计数器

程序计数器(Program Counter
Register):是一块很小的内存空间,可以看作是当前线程做执行的字节码行号指示器
字节码指示器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖于这个计数器来完成的。)。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在确定的时刻,一个处理器都只会处理的一个线程中的指令。
因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,互不影响,独立存储,这类的内存区域称为“线程私有”的内存。

2.2.2 Java虚拟机栈

Java虚拟机栈也是线程私有的,生命周期与线程相同。
描述的是Java方法执行的内存模型

  • 每个方法在执行时都会创建一个栈帧(Stack
    Frame)用于存储局部变量表操作数栈动态链接方法出口
    等信息。
  • 每一个方法从调用直至完成的过程,就对应一个栈帧在虚拟机栈中的入栈到出栈的过程。

2.2.3 本地方法栈

与Java虚拟机栈所发挥的作用是非常相似的,区别在于:

虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,
而,本地方法栈则为:虚拟机所用到的Native方法服务。

2.2.4 Java堆

所有线程共享的一块内存区域。唯一目的就是:存放对象实例
Java堆是垃圾回收器管理的主要区域,又称“GC堆”:(Garbage Collected
Heap);

  • 内存回收角度:
    • 分代回收算法
    • 粗分:新生代、老年代
    • 细分:Eden空间、From Survivor空间、To Survivor空间等你。
  • 内存分配角度:线程共享的Java堆可能划分出多个线程私有的分配缓冲区(Thread
    Local Allocation Buffer,TLAB)

2.2.5 方法区

字节码中方法表;
方法区是线程共享的,用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码的数据。
HotSpot虚拟机的GC分代收集器扩展至当方法区,使用永久代来实现方法区——所以又称为“永久代”(Premanent
Generation)。这个区域的内存回收目标主要是:针对常量池的回收和对类型的卸载。

方法区虽然称为 非堆 ,但实际在内存之中,也是一种特殊的

堆是专门用来存放对象的,而方法区(一种特殊的堆)是用来
存在描述方法信息对象

方法区存放着类的运行时数据:

  1. 静态变量(静态域)
  2. 静态方法
  3. 常量池
  4. 类的代码

<-
存放着对象:

  • java.lang.class:代表着方法区的类(java中万物皆对象:Class也是对象。)

<-
main方法的栈帧:

2.2.6 运行时常量池

运行时常量池(Runtime Constant
Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(Constant
Pool
Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

2.2.7 直接内存

直接内存(Direct
Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库来直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。显著提高性能,因为避免了Java堆和Native堆中来回复制数据。

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建

2.3.2 对象的内存布局

2.3.3 对象的访问定位

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出

2.4.2 虚拟机栈和本地方法栈溢出

2.4.3 方法区和运行时常量池溢出

2.4.4 本机直接内存溢出

2.5 本章小结

第3章 垃圾收集器与内存分配策略

3.1 概述

3.2 对象已死吗

3.2.1 引用计数算法

3.2.2 可达性分析算法

3.2.3 再谈引用

3.2.4 生存还是死亡

3.2.5 回收方法区

3.3 垃圾收集算法

3.3.1 标记-清除算法

3.3.2 复制算法

3.3.3 标记-整理算法

3.3.4 分代收集算法

3.4 HotSpot的算法实现

3.4.1 枚举根节点

3.4.2 安全点

3.4.3 安全区域

3.5 垃圾收集器

3.5.1 Serial收集器

3.5.2 ParNew收集器

3.5.3 Parallel Scavenge收集器

3.5.4 Serial Old收集器

3.5.5 Parallel Old收集器

3.5.6 CMS收集器

3.5.7 G1收集器

3.5.8 理解GC日志

3.5.9 垃圾收集器参数总结

3.6 内存分配与回收策略

3.6.1 对象优先在Eden分配

3.6.2 大对象直接进入老年代

3.6.3 长期存活的对象将进入老年代

3.6.4 动态对象年龄判定

3.6.5 空间分配担保

3.7 本章小结

第4章 虚拟机性能监控与故障处理工具

4.1 概述

4.2 JDK的命令行工具

4.2.1 jps:虚拟机进程状况工具

4.2.2 jstat:虚拟机统计信息监视工具

4.2.3 jinfo:Java配置信息工具

4.2.4 jmap:Java内存映像工具

4.2.5 jhat:虚拟机堆转储快照分析工具

4.2.6 jstack:Java堆栈跟踪工具

4.2.7 HSDIS:JIT生成代码反汇编

4.3 JDK的可视化工具

4.3.1 JConsole:Java监视与管理控制台

4.3.2 VisualVM:多合一故障处理工具

4.4 本章小结

第5章 调优案例分析与实战

5.1 概述

5.2 案例分析

5.2.1 高性能硬件上的程序部署策略

5.2.2 集群间同步导致的内存溢出

5.2.3 堆外内存导致的溢出错误

5.2.4 外部命令导致系统缓慢

5.2.5 服务器JVM进程崩溃

5.2.6 不恰当数据结构导致内存占用过大

5.2.7 由Windows虚拟内存导致的长时间停顿

5.3 实战:Eclipse运行速度调优

5.3.1 调优前的程序运行状态

5.3.2 升级JDK 1.6的性能变化及兼容问题

5.3.3 编译时间和类加载时间的优化

5.3.4 调整内存设置控制垃圾收集频率

5.3.5 选择收集器降低延迟

5.4 本章小结

第三部分 虚拟机执行子系统

第6章 类文件结构

6.1 概述

6.2 无关性的基石

澳门新葡亰3522平台游戏,6.3 Class类文件的结构

6.3.1 魔数与Class文件的版本

使用魔数来进行身份识别,值为:0xCAFFBABE(咖啡宝贝)。虚拟机也必须拒绝执行超过其版本号的Class文件。

6.3.2 常量池

常量池可以理解为Class文件之中的资源仓库。

它是Class文件结构中也其他项目关联最多的数据类型;
它也是占用Class文件空间最大的数据项目之一;
同时它还是在Class文件中第一个出现表类型数据项目。

常量池主要存放两大类常亮:字面量(Literal)和符号引用(Symbolic
Reference)。

  • 字面量:比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值;
  • 符号引用:属于编译原理方面的概念,包括下面三类常量:
  • [ ] 类和接口的全限定名
  • [ ] 字段的名称和描述符
  • [ ] 方法的名称和描述符

字段和方法的符号引用,在运行期转换为真正的内存入口地址。虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

6.3.3 访问标志

常量池之后,就是访问标志(access_flags),这个标志用于识别一下类或者接口层次的访问信息,包括:

  • 这个Class是类还是接口;
  • 是否定义为public类型;
  • 是否定义为abstract类型;
  • 如果是类的话,是否被声明为final等。

6.3.4 类索引、父类索引与接口索引集合

类索引和父索引是一个u2类型的数据(一个类是只能有一个父类。);而接口索引集合是一组u2类型的数据的集合(Java的多继承就是通过接口来实现,一个类可以实现多个接口。)。入口的第一项数据为:接口计数器。这三项数据确定继承关系:类、父类、接口。
类索引、父索引->类或接口的符号引用->具体的UTF-8编码的字符串(常量中的全限定名字符串)。

6.3.5 字段表集合

字段表:用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。可以包括的信息有:

  • 字段的作用域(public、private、protect修饰符)
  • 是实例变量还是类变量(static修饰符)
  • 可变性(final)
  • 并发可见性(volatile修饰符,是否强制从主内存读写)
  • 是否被序列化(transient修饰符)
  • 字段数据类型(基本类型、对象、数组)
  • 字段名称

各个修饰符都是布尔值,是用标志位来表示。
而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。(具体变量的数据类型,引用常量池中的常量)

6.3.6 方法表集合

6.3.7 属性表集合