《深入理解Java虚拟机(第2版)》

澳门新浦京娱乐游戏 3

引言

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

前言

JVM 是一种抽象的计算机,基于堆栈架构,它有自己的指令集和内存管理,是
Java 跨平台的依据,JVM解释执行字节码,或将字节码编译成本地代码执行。Java
虚拟机体系结构如下:

一 类文件结构

第一部分 走近Java

澳门新浦京娱乐游戏 1

无关性基石

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

第1章 走近Java

Class File

Class File
是平台无关的二进制文件,包含着能被JVM执行的字节码,其中多字节采用大端序,字符使用一种改进的UTF-8编码。Class文件精确的描述了一个类或接口的信息,其中包括:

  • 常量池:数值和字符串字面常量,元数据如类名、方法名称、参数,以及各种符号引用
  • 方法的字节码指令,参数个数,局部变量,最大操作数栈深度,异常等信息

类文件结构

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

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

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

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

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

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

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

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

1.1 概述

Class Loader

类加载器,JVM在类首次使用时动态的加载、链接和初始化。JVM默认的加载模型是双亲委派模型,类加载器之间存在父子关系的层次结构,内部使用组合实现。此外还有其他的加载方式,比如Servlet加载,它先尝试自己加载,不成功再委派上层加载器,类隔离;OSGI加载器之间是一种网状的依赖关系,没有上下层的区分,比较灵活。

二  类加载机制

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

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

1.2 Java技术体系

加载

加载就是将Class文件表示的类或接口,在JVM方法区中创建一个与之对应的java.lang.Class对象,像Class.forName()、ClassLoader.loadClass()、反射都能触发类加载。当触发一个类加载时,详细的过程如下:

  • 检查类是否已经被加载
  • 将加载请求委派给上层类加载器
  • 自己尝试搜索类并加载

当ClassLoader在classpath中未找到类文件,会抛出ClassNotFoundException;当类A引用类B,类A已经成功加载,但是加载B时未找到类文件,会抛出NoClassDefFoundError。JVM有以下几种类加载器:

  • Bootstrap ClassLoader,启动类加载器,加载
    <java_home>jrelib 中 Java 核心类库
  • Extension ClassLoader,扩展类加载器,加载
    <java_home>jrelibext 中的类
  • System ClassLoader,系统类加载器,也叫应用程序类加载器(Application
    class loader),加载 CLASSPATH 环境变量中的类

初始化的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发展史

链接

  • 验证:确保class文件的正确性。
  • 准备:为类静态字段分配内存并初始化为默认值,不会执行任何字节码指令。
  • 解析:将符号引用转为方法区(运行时常量池)直接引用

类加载过程

加载

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

验证

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

澳门新浦京娱乐游戏 ,准备

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

解析

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

初始化

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

1.4 Java虚拟机发展史

初始化

执行类初始化方法,即赋值静态字段,执行静态块,顺序按照其定义的先后。父类的静态域会先于子类静态域初始化。
至此,一个类或接口被加载到了内存中,JVM会保证整个过程是线程安全的。需要注意的是整个过程没有涉及到任何实例对象。

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在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();
        }
    }
}

结果为:

澳门新浦京娱乐游戏 2

可以看到,由自定义的加载类只能获取同包下的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

运行时数据区

  • Method
    Area
    线程共享,存储运行时常量池、类字段和方法信息、静态变量和方法的字节码,是堆的逻辑组成部分,这部分的垃圾回收是可选的。值得一提的是Hotspot
    JVM自JDK8之后,调整了这部分内存的内容,class
    meta-data的分配使用本地内存,interned
    String和类静态变量移动到了Java堆。
  • 运行时常量池:对于JVM来说具有核心作用,基本上涉及到方法或字段,JVM就会在运行时常量池中搜索其具体的内存地址。
  • Heap线程共享,存储实例对象,实例变量以及数组,是垃圾回收的主要区域。
  • JVM
    Stack
    线程私有,用于存储栈帧,当方法被调用时会创建一个栈帧入栈,栈帧由以下几部分组成:

    • 局部变量表:从0开始存储this、方法参数、局部变量。
    • 操作数栈:方法的工作区,在操作数栈和局部变量之间交换数据,存储中间结果,操作数栈深度在编译时就能确定。
    • 帧数据:方法返回值,异常分派,以及当前方法所在类运行时常量池的引用。
  • PC
    Register
    线程私有,保存当前指令地址,执行后指向下一条指令地址。
  • Native Method Stack线程私有,存储本地方法信息,C或C++栈。

三  字节码执行引擎

1.4.2 Sun HotSpot VM

执行引擎

读取、翻译、执行字节码。JVM基于栈架构,这个栈就是操作数栈,字节码指令就是通过它进行各种运算。此外还有基于寄存器的虚拟机。

  • Interpreter,翻译:解释字节码比较快,执行慢,缺点是每次方法调用都要重新翻译解释一遍。
  • JIT
    Compiler,即时编译:找出程序中频繁调用的热点方法,将字节码编译成本地代码,提高性能。
  • Garbage
    Collector,垃圾收集器:回收无效对象,判断对象是否可回收,可采用不同的垃圾回收算法。

运行时栈帧结构

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

澳门新浦京娱乐游戏 3

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

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

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

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

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

1.4.3 Sun Mobile-Embedded VM Meta-Circular VM

本地方法接口和库

JNI,调用本地方法,c/c++库;执行引擎所需的本地方法库。

方法调用

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

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

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

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

1.4.4 BEA JRockit IBM J9 VM

小结

主流JVM的实现有Oracle的Hotspot
JVM、JRockit以及IBM的JVM。说到JVM调优,默认指的就是Hotspot
VM,足见其流行程度。如今搞Java不去了解JVM就显得有点low了-v-。
要想写出高质量代码,不仅要了解JVM,像调优,问题排查等都需要完备的计算机基础知识,其实无论用什么语言开发,都是一个构建和完善自身计算机知识体系的过程。

基于栈的字节码执行引擎

不管是解释型语言还是编译型语言,机器都无法理解非二进制语言。高级语言转化成机器语言都遵循现代经典编译原理。即执行前对程序源码进行词法和语法分析,构建抽象语法树。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 无关性的基石

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 属性表集合

6.4 字节码指令简介

6.4.1 字节码与数据类型

6.4.2 加载和存储指令

6.4.3 运算指令

6.4.4 类型转换指令

6.4.5 对象创建与访问指令

6.4.6 操作数栈管理指令

6.4.7 控制转移指令

6.4.8 方法调用和返回指令

6.4.9 异常处理指令

6.4.10 同步指令

6.5 公有设计和私有实现

6.6 Class文件结构的发展

6.7 本章小结

第7章 虚拟机类加载机制

7.1 概述

虚拟机如何加载这些class文件?

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验转换解析初始化,最终形成可以被虚拟机直接使用的Java类型。——虚拟机的类加载机制。
Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,所以,给Java带来了动态扩展的语言特征——都是依赖于运行期动态加载动态连接(迟绑定)的特点。

7.2 类加载的时机

类的生命周期:

加载->
(连接)验证->准备->解析->
初始化->
使用->
卸载。

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或者晚期绑定)。

什么情况下需要开始类加载过程的第一阶段:加载?

Java虚拟机规范中并没有进行强制约束;但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new(实例化对象)、getstatic(读取类的静态字段)、putstatic(设置类的静态字段)或者invokestatic(调用类的静态方法)这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
    (被final修饰、已在编译器把结果放入常量池的静态字段除外。)
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发器初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发器父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机就会先初始化这个主类。
  5. JDK
    1.7语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄多对应的类没有进行过初始化,则需要先触发其初始化。

这五种场景中的行为称为对一个类进行了主动引用;除此之外的,
所有引用类的方法都不会触发初始化,称为被动引用。

7.3 类加载的过程

7.3.1 加载

7.3.2 验证

7.3.3 准备

7.3.4 解析

7.3.5 初始化

7.4 类加载器

7.4.1 类与类加载器

将class文件字节码->内存->class对象作运行时数据的入口。

7.4.2 双亲委派模型

只有两种不用的类加载器:

  • 一种是启动类加载器(Bootstrap
    ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;

  • 另一种就是所有其他的类加载器,这些类加载器都有Java语言实现的,独立于虚拟机外部,并且都继承抽象类java.lang.ClassLoader(加载字节码,找到定义的java类)。

  • [ ] 引导类加载器(Bootstrap ClassLoader):JAVA_HOMElib;

  • [ ] 扩展类加载器(Extension
    ClassLoader):JAVA_HOMElibext、java.ext.dirs指定的所有类库;

  • [ ] 应用程序类加载器(Application
    ClassLoader):用户类路径(ClassPath)所指定的类库;

  • [ ] 自定义类加载器:文件加载器、网络加载器。

代理模式:交给其他类加载器,加载指定类。
双亲委派机制:优先交给父加载器来加载。(为了安全,例如有人自定义了java.lang.Object,java.lang.String,避免核心类的自定义。)
同一类被不同加载器加载,JVM会认为是不同的类。
Tomcat服务器也是使用了代理模式:先尝试加载这个类,找不到再代理父类加载器,
与一般加载器顺序相反

7.4.3 破坏双亲委派模型

双亲委派模型出现过三次较大规模的“被破坏”情况:

  1. JDK 1.2之前,用户都是去继承java.lang.ClassLoader的唯一目的就是为了
    重写loadClass()方法
    因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()
    JDK
    1.2之后才引入了双亲委派模型,而类加载器和抽象类java.lang.ClassLoader则在JDK
    1.0时代就存在了。为了向前兼容,JDK
    1.2之后的java.lang.ClassLoader添加了一个新的protect方法findClass()
  2. 双亲委派模式自身的缺陷导致的——双亲委派模型很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载):基础类总是作为被用户代码调用的API。

但是基础类又要调用回用户的代码,那该怎么办?
典型的例子就是:JNDI服务——它的代码由启动类加载器去加载(在JDK
1.3是放进去的rt.jar),
但是JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service
Provider
Interface)的代码,但启动类加载器不可能“认识”这些代码啊!那该怎么办?

双亲委派模型行不通,为了解决这个问题,引入了一个不太优雅的设计:线程上下文类加载器(Thread
Context
ClassLoader)。这个类加载器通过java.lang.Thread类的setContextLoader()方法进行设置。

  1. 由于用户对程序动态性的追求而导致——代码热替换(HotSwap)、模块热部署(Hot
    Deployment)等。

ODGi(Open Service Gateway Initivate):面向java的动态模块系统。
A、B、C三个组件各自有自己的加载器(每个组件都有自己的加载器)。

7.5 本章小结

第8章 虚拟机字节码执行引擎

8.1 概述

8.2 运行时栈帧结构

8.2.1 局部变量表

8.2.2 操作数栈

8.2.3 动态连接

8.2.4 方法返回地址

8.2.5 附加信息

8.3 方法调用

8.3.1 解析

8.3.2 分派

8.3.3 动态类型语言支持

8.4 基于栈的字节码解释执行引擎

8.4.1 解释执行

8.4.2 基于栈的指令集与基于寄存器的指令集

8.4.3 基于栈的解释器执行过程

8.5 本章小结

第9章 类加载及执行子系统的案例与实战

9.1 概述

9.2 案例分析

9.2.1 Tomcat:正统的类加载器架构

9.2.2 OSGi:灵活的类加载器架构

9.2.3 字节码生成技术与动态代理的实现

9.2.4 Retrotranslator:跨越JDK版本

9.3 实战:自己动手实现远程执行功能

9.3.1 目标

9.3.2 思路

9.3.3 实现

9.3.4 验证

9.4 本章小结

第四部分 程序编译与代码优化

第10章 早期(编译期)优化

10.1 概述

10.2 Javac编译器

10.2.1 Javac的源码与调试

10.2.2 解析与填充符号表

10.2.3 注解处理器

10.2.4 语义分析与字节码生成

10.3 Java语法糖的味道

10.3.1 泛型与类型擦除

10.3.2 自动装箱、拆箱与遍历循环

10.3.3 条件编译

10.4 实战:插入式注解处理器

10.4.1 实战目标

10.4.2 代码实现

10.4.3 运行与测试

10.4.4 其他应用案例

10.5 本章小结

第11章 晚期(运行期)优化

11.1 概述

11.2 HotSpot虚拟机内的即时编译器

11.2.1 解释器与编译器

11.2.2 编译对象与触发条件

11.2.3 编译过程

11.2.4 查看及分析即时编译结果

11.3 编译优化技术

11.3.1 优化技术概览

11.3.2 公共子表达式消除

11.3.3 数组边界检查消除

11.3.4 方法内联

11.3.5 逃逸分析

11.4 Java与CC++的编译器对比

11.5 本章小结

第五部分 高效并发

第12章 Java内存模型与线程

12.1 概述

12.2 硬件的效率与一致性

12.3 Java内存模型

12.3.1 主内存与工作内存

12.3.2 内存间交互操作

12.3.3 对于volatile型变量的特殊规则

12.3.4 对于long和double型变量的特殊规则

12.3.5 原子性、可见性与有序性

12.3.6 先行发生原则

12.4 Java与线程

12.4.1 线程的实现

12.4.2 Java线程调度

12.4.3 状态转换

12.5 本章小结

第13章 线程安全与锁优化

13.1 概述

13.2 线程安全

13.2.1 Java语言中的线程安全

13.2.2 线程安全的实现方法

13.3 锁优化

13.3.1 自旋锁与自适应自旋

13.3.2 锁消除

13.3.3 锁粗化

13.3.4 轻量级锁

13.3.5 偏向锁

13.4 本章小结

附  录

附录A 编译Windows版的OpenJDK

附录B 虚拟机字节码指令表

附录C HotSpot虚拟机主要参数表

附录D 对象查询语言(OQL)简介

附录E JDK历史版本轨迹

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图