首页 > 代码库 > 《深入Java虚拟机学习笔记》- 第1章 Java体系结构
《深入Java虚拟机学习笔记》- 第1章 Java体系结构
一、体系结构组成
当编写并运行一个Java程序时,就同时体验了这四种技术。用Java语言编写源代码,编译成Java Class文件,然后再在Java虚拟机上运行class文件。当编写程序时,通过调用类中的方法来访问系统资源。当程序运行的时候,它通过调用class文件中的方法来满足程序的Java API调用。
- Java程序设计语言
- Java Class文件格式
- Java应用编程接口
- Java虚拟机
二、Java虚拟机
- Java虚拟机的主要任务是装载class文件并执行其中的字节码。JVM包含一个类装载器,它可以从程序和API中装class文件。Java API中只有程序执行时需要的那些类才会被装载。
- 当JVM是由主机操作系统上的软件实现的时候,Java程序通过调用本地方法(native方法)和主机交互。Java中有两种方法:
- Java方法:由Java语言编写的,编译成字节码,存储在class文件中,是平台无关的;
- 本地方法:由其它语言(C,C++或汇编语言)编写的,编译成和处理器相关的机器代码,保存在动态链接库中,格式是各个平台专有的,是平台相关的;当编写一个平台独立的Java程序时,必须遵守的一条最重要的原则就是:不要直接或间接调用不属于Java API的本地方法;一个平台无关的Java程序如图:
- 运行中的程序调用本地方法时,JVM装载包含这个本地方法的动态库,并调用这个方法;
三、类装载器
- 一个Java应用程序可以使用两种类装载器:"启动"(bootstrap)类装载器和用户自定义的类装载器。启动类装载器(这是系统中唯一的)是JVM的一部分。
- Java应用程序能够在运行时安装用户定义的类装载器,这种类装载器能够使用自定义的方式来装载类,例如从网络下载class文件。
- 用户定义的类装载器使得在运行时扩展Java应用程序成为可能。当它运行时,应用程序能够决定它需要哪些额外的类,能够决定是使用一个或是更多的用户定义的类装载器来装载。由于类装载器是使用Java编写的,所以能用任何在Java中可以表述的风格来进行类的装载。这些类可以通过网络下载,可以从某些数据库中获取,甚至可以动态生成。
- 每一个类被装载的时候,JVM都监视这个类,看它到底是被启动类装载器还是被用户定义类装载器装载。当被装载的类引用了另外一个类时,JVM就会使用装载第一个类的类装载器装载被引用的类。这样,两个类就动态地建立起了联系。
- 由于JVM采取这种方式进行类的装载,所以被装载的类默认情况下只能看到被同一个类装载器装载的别的类。被不同的类装载器装载的类存放在不同的命名空间中,它们不能互相访问,除非应用程序显式地允许这么做。通过这种方法,就能够使用Java类装载器的体系结构来控制任何从不同源文件中装载的代码之间的相互影响,特别是能够阻止恶意代码获取访问和破坏善意代码的权限。
- Java类装载器体系结构如图:
四、Java独有的特性
- 在Java中没有通过使用强制转换指针类型或者通过进行指针运算直接访问内存的方法;
- Java避免无意间破坏内存的另一个办法是自动垃圾收集;在Java中,只需要停止对一个对象的引用,一段时间后,垃圾收集器会自动回收这个对象所占用的内存;
- Java在运行时保护内存完整性的第三个办法是数组边界检查;
- 最后一个关于Java确保程序健壮性的例子是对对象引用的检查,每次使用引用的时候,Java都会确保这些引用不为空值;
jvm全称是Java Virtual Machine(java虚拟机)。它之所以被称之为是“虚拟”的,就是因为它仅仅是由一个规范来定义的抽象计算机。我们平时经常使用的Sun HotSpot虚拟机只是其中一个具体的实现(另外还有BEA JRockit、IBM J9等等虚拟机)。在实际的计算机上通过软件来实现一个虚拟计算机。与VMWare等类似软件不同,你是看不到jvm的,它存在于内存。
当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
一、jvm体系结构
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图1所示。
图1 Java虚拟机的内部体系结构
下面先对图中各部分做个简单的说明:
1.class文件:虚拟机并不关心Class的来源是什么语言,只要它符合Java class文件格式就可以在Java虚拟机中运行。使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的 编译器一样可以把程序代码编译成Class文件。
2.类装载器子系统:负责查找并装载Class 文件到内存,最终形成可以被虚拟机直接使用的Java类型。
3.方法区:在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这 些信息存储到方法区。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。由于所有线程都共享方法区,因此它们对方法 区数据的访问必须被设计为是线程安全的。
4.堆:存储Java程序创建的类实例。所有线程共享,因此设计程序时也要考虑到多线程访问对象(堆数据)的同步问题。
5.Java栈:Java栈是线程私有的。每当启动一个新线程时,Java虚拟机都会为它分配一 个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈或出栈。当线程调用java方法时, 虚拟机压入一个新的栈帧到该线程的java栈中。当方法返回时,这个栈帧被从java栈中弹出并抛弃。一个栈帧包含一个java方法的调用状态,它存储有 局部变量表、操作栈、动态链接、方法出口等信息。
6.程序计数器:一个运行中的Java程序,每当启动一个新线程时,都会为这个新线程创建一个 自己的PC(程序计数器)寄存器。程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取 下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个 计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
7.本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态链接并直接调用指定的本地方法。如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。
8.执行引擎: 负责执行字节码。方法的字节码是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。执行引擎执行字节码 时,首先取得一个操作码,如果操作码有操作数,取得它的操作数。它执行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在 线程完成前将一直持续。
下面详细说明下具体构成:
1、数据类型
Java语言中所有的基本类型同样也都是Java虚拟机中的基本类型。但boolean有点特别,指令集对boolean只有很有限的支持。当编译器把Java源码编译为字节码的时,它会用int或byte来表示boolean。Boolean数组是当byte数组来访问的。
returnAddress是Java虚拟机内部使用的基本类型,这个类型被用来实现Java程序中的finally子句。
2、类装载器子系统
负责查找并装载的那部分被称为类装载器子系统。
分为启动类装载器和用户自定义类装载器
由不同的类装载器装载的类将放在虚拟机内部的不同命名空间中。
用户自定义的类装载器以及Class类的实例都放在内存的堆区,而装载的类型信息则都位于方法区。
装载顺序:
1)装载——查找并装载类型的二进制数据
2)连接——执行验证(确保被导入类型的正确性),准备(为类变量分配内存,并将其初始化为默认值),以及解析(把类变量中的符号引用转换为正确的初始值)
3)初始化——把类变量初始化为正确的初始值
3方法区
在java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。
所有线程都共享方法区。
类型信息:
这个类型的全限定名
这个类型的直接超类的全限定名
这个类型是类类型还是接口类型
这个类型的访问修饰符
任何直接超接口的全限定名的有序列表
该类型的常量池
字段信息
方法信息
除了常量以外的所有类(静态)变量
一个到类ClassLoader的引用
一个到Class类的引用
其中字段信息包括
字段名
字段类型
字段的修饰符
方法信息包括
方法名
方法的返回类型
方法参数的数量和类型
方法的修饰符
如果方法不是抽象的和本地的还须有
方法的字节码
操作数栈和该方法的栈帧中的局部变量的大小
异常表
4 堆
Java程序在运行时所创建的所有类实例或数组都放在同一个堆中。
Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能快速的定位对象实例的数据,另外,它必须能通过该对象引用访问相应的类数据,因此对象中通常有一个指向方法区的指针。
一种可能的堆空间设计就是,把堆分为两部分:一个句柄池,一个对象池。
这种设计的好处是有利于堆碎片的整理,缺点是每次访问对象的实例变量都需要经过两次指针传递。
另一种设计方式是使对象直接指向一组数据,而数据包括对象实例数据以及指向方法区类数据的指针。这种设计方式的优点是只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。
堆中其他数据:
1、对象锁,用于协调多个线程访问一个对象时的同步。
2、等待集合
3、与垃圾收集器有关的数据。
4、方法表:加快了调用实例方法时的效率。
方法表指向的实例方法数据包括以下信息:
此方法的操作数栈和局部变量区的大小
此方法的字节码
异常表
这些信息足够虚拟机去调用一个方法了,方法表包含有方法指针——指向类活或超类声明的方法的数据
对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器),在线程启动时创建。大小是一个字长。当线程执行某个Java方法时,PC的内容总是下一条将被指向指令的“地址”。如果该线程正在执行一个本地方法,那么此时PC的值为”undefined”。
6、Java栈
每当启动一个线程时,Java虚拟机都会为它分配一个Java栈,Java栈以帧为单位保存线程的运行状态,虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。
某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池,在线程执行一个方法时,它会跟踪当前类和当前常量池。
每当线程调用一个方法时,虚拟机都会在该线程的Java栈中压入一个新帧,而这个新栈自然就成为当前帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等数据。
Java栈上的所有数据都是数据都是此线程私有的。
7、栈帧
栈帧由三部分组成:局部变量区、操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,编译器在编译器时就确定的确定了这些值并放在class文件中。帧数据区的大小依赖于具体的实现。
当虚拟机调用一个方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。
局部变量区:Java栈帧的局部变量区被组织为以一个字长为单位、从0开始计数的数组。字节码指令通过从0开始的索引来使用其中的数据。
局部变量区对应方法的参数和局部变量。编译器首先按声明的顺序把这些参数放入局部变量数组。
在java中,所有的对象都按引用传递,并且都存储在堆中,永远都不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象的引用。
操作数栈:操作数栈也是被组织为一个字长为单位的数组。但它不是通过索引来访问,而是通过标准的栈操作——压栈和出栈来访问的。
帧数据区:支持解析常量池解析、正常方法返回以及异常派发机制。每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。常量池中对类型、字段和方法的引用在开始时都是符号。当虚拟机在常量池中搜索时,如果遇到类、接口、字段或者方法的入口,假若它们仍然是符号,虚拟机那时候才会进行解析。
8、执行引擎
指令集:方法的字节码流是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码本身就已经规定了它是否需要跟随操作数,以及如果有操作数它是什么形式的。当虚拟机执行一条指令的时候,可能使用当前常量池中的项、当前帧的局部变量中的值,或者当前帧操作数栈顶端的值。
执行技术:解释、即时编译、字适应优化、芯片级直接执行。
Hotspot虚拟机就采用了自适应优化。自适应优化虚拟机开始的时候对所有的代码都是解释执行,但是它会监视代码的执行情况。大多数程序花费80%-90%的时间来执行10%-20%的代码。虚拟机可以意识到那些方法是程序的热区——就是那10%-20%的代码,他们占整个执行时间的80%-90%。当自适应优化的虚拟机判断出某个特定的方法是瓶颈的时候,它启动一个后台线程,把字节码编译成本地代码,非常仔细的优化这些本地代码。