== 这篇博客是在阅读本书过程中做的笔记,以后遇到相关知识点或问题会加以补充完善==
Java面向网路的核心就是Java虚拟机,它支持Java面向网络体系结构三大支柱的所有方面,平台无关性、安全性和网络移动性。
一个Java程序可以使用两种类装载器:’启动‘类装载器和用户自定义的类装载器。启动类装载器也叫做系统类装载器,原始类装载器或者默认类装载器,启动类装载器通常使用默认的方式从本地磁盘中装载类,包括JavaAP
I类。启动类装载器是Java虚拟机实现的本值部分,但是用户自定义的类装载器也能够Java编写,编译被虚拟机加载,这里不做太多记录,后边遇到了再加以理解和补充。
从上边的虚拟机结构图可以看出,Java虚拟机的主要任务是装载class文件并且执行其中的字节码。图中可以看到,Java虚拟机包含一个类装载器( class loader ),它可以从程序和API中装载class文件。Java API中只有程序执行时需要的那些类才会被装载。字节码由执行引擎来执行。
不同的Java虚拟中,执行引擎的实现方式可能不同,目前来说由软件实现的虚拟机中,执行引擎的实现方式有三种:
Java虚拟机操作本机资源,是通过本地方法来完成的,本地方法就是链接Java虚拟机和本机操作系统的桥梁。当然这种桥梁也是需要通行证的,Java有完备的本地方法库的api,调用前都会有权限的监控,这种权限控制当然是为了让安全性更高,其实就是一个封闭的沙箱环境。
对平台无关性的支持就像安全性和网络移动性的支持一样,是分布在整个Java体系结构中的,所有的组成部分,语言、class文件、api以及虚拟机,都在对平台无关性的支持扮演者重要的角色。
Java平台由Java API和Java虚拟机组成,Java平台扮演者一个运行时Java程序于其下硬件和操作系统之间的缓冲角色。Java程序被编译成 可运行在Java虚拟机中的二进制程序,API则给予Java程序访问底层计算机资源的能力。所以说,无论Java程序被部署在何处,他只需要更Java平台交互。
Java语言的基本类型和行为都是Java语言自己定义的,所以不管在什么系统上运行,这一点在Java虚拟机和class文件中都是一致的,。通过确保基本类型在所在平台上的以执行来支持平台无关性。
class文件定义了一个的定义Java虚拟机的二进制格式,这种二进制文件就是编译原理中的中间码,因为它的格式有严格的约定,同一calss文件在不同的Java平台上运行时会被翻译成适用于当前系统可执行的机器码。从而很好的支持平台无关。
可伸缩性是指,Java平台可以在不同的计算机上实现。
做了解不做笔记,以后遇到了再做研究记录。
组成Java沙箱的基本组件
- 类装载器结构
- class文件检验器
- 内置Java虚拟机的安全特性
- 安全管理器及javaAPI
类装载器在Java安全特性中的作用
- 防止恶意代码干涉善意代码(命名空间)
- 守护被行人的类库边界
- 将代码归入某类,该类确定了代码可以进行那些操作
Class文件校验器对代码的四次检查
- 第一次检查:检查是否符合Java文件的基本结构;检查文件的完整性,是否又被删节,尾部是否有带其他的字节;检查版本号;
- 第二次检查:数据类型的语义检查,不需要查看字节码,也不需要装在和查看其他类型,查看每个组成部分,确认他们是否是其所属类型的实例,结构是否正确。
- 第三次检查:字节码验证
- 第四次检查:符号引用的验证
要理解Java虚拟机,你首先必须意识到,当你说“Java虚拟机”时,可能指的是如下三种不同的东西:
每个Java程序都运行于某个具体的Java虚拟机实现的实例上。在本书中,术语“Java虚拟机”可能表示上述三种情形之一,当无法联系上下文来确定其准确意思的时候,我们会在文中指明究竟是“抽象规范”,“一个具体的实现”,还是“一个运行中的虚拟机实例”。
- 某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有。每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的。
- 当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息。然后,它把这些类型信息放到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。
- 当每一个新线程被创建时、它都将得到它自己的PC寄存器(程序计数器)以及一个lava栈。如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下–条将被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态—-包括它的局部变量,被调用时传进来的参数,它的返回值.以及运算的中间结果等等。而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。
- 方法区和堆是由所有线程所共享的运行时数据区。
- Java栈是由许多栈帧( stack framc)或者说帧( frame )组成的,一个栈帧包含一个Java方法调用的状态。可以不是很形象的说,Java方法是一系列操作的指令集,一个方法就是一个栈帧,执行方法会有具体的栈帧,会将方法中的变量放到栈帧中的局部变量中,后边会详细讲到。
- 当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中;当该方法返回时、这个栈帧被从Java栈中弹出并抛弃。
- Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。
boolean
类型有点特别,虽然Java虚拟机也将boolean
看作是基本类型,但是指令集(指令集:指挥机器工作的指令和命令)对boolean的支持是有限的。当编译器将Java源码编译成字节码时,他会用int
或byte
来表示boolea
。false
用零(0)来表示,所有的非零数都表示true
,涉及boolea
的操作会使用int
。boolean
数组是当作byte数组来访问的,但是在“堆”区,他可以被表示为位域。returnAddress
是一个旨在Java虚拟机内部使用的基本类型。Java虚拟机中的以用类型:
Java虚拟机中,最基本的数据单元就是字( word ),它的大小是由每个虚拟机实现的设计者来决定的。字长必须足够大,至少是一个字单元就足以持有byte、short、int、char、float、returnAddress或者reference类型的值,而两个字单元就足以持有long或者double类型的值。因此,虚拟机实现的设计者至少得选择32位作为字长,或者选择更为高效的字长大小。通常根据底层主机平台的指针长度来选择字长。
在Java虚拟机规范中,关于运行时数据区的大部分内容,都是基于“字”这个抽象概念的。比如,关于栈帧的两个部分―局部变量和操作数栈-—都是按照“字”来定义的。这些内存区域能够容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操作数栈中时,它将占用一个或两个字单元。
在运行时,Java程序无法侦测到底层虚拟机的字长大小;同样,虚拟机的字长大小也不会影响程序的行为——它仅仅是虚拟机实现的内部属性。
在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件——一个线性二进制数据流,然后将它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的类(静态)变量同样也是存储在方法区中。
由于所有线程都共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。比如,假设同时有两个线程都企图访问一个名为Lava的类,而这个类还没有被装入虚拟机,那么,这时只应该有一个线程去装载它,而另一个线程则只能等待。
方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。
方法区也可以被垃圾收集,当某个类不在被引用时,Java虚拟机就可以写在这个类(垃圾收集),从而使方法区占用的内存达到最小。
方法区中存储的类信息:
1、这个类型的全限定名。
2、这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)。·
3、这个类型是类类型还是接口类型。
4、这个类型的访问修饰符(( public、abstract或final的某个子集)。·
5、任何直接超接口的全限定名的有序列表。
6、该类型的常量池。详细内容第6章讲
常量池主要用于存放两大类常量:
- 字面量(Literal),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等。
- 符号引用量(Symbolic References),符号引用则属于编译原理方面的概念,包括了如下三种类型的 >常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符
7、字段信息。
- 字段名
- 字段类型
- 字段的修饰符( public、private、protected、static、final、volatile、transient的某个子集))
8、方法信息。
- 方法名
- 方法的返回类型(或void)
- 方法参数的数量和类型(按声明顺序)
- 字段的修饰符( public、private、protected、static、final、volatile、transient的某个子集))
- 如果某个方法不是抽象方法和本地方法它还需要保存方法的字节码bytecodes
- 操作数栈和该方法的栈帧中的局部变量区的大小(这些在本章的后面会详细描述)。
- 异常表 (参见17章)
9、除了常量以外的所有类(静态)变量。
类(静态)变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关—―而非类的实例,因此它们总是作为类型信息的一部分而存储在方法区*。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类(静态)变量分配空间。
10、一个到类ClassLoader的引用。
- 指向ClassLoader类的引用﹐每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。
- 虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。关于动态连接和命名空间的细节请参见第8章。
11、一个到类Class的引用。
指向Class类的引用﹐对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。
// on CD-ROM in file jvm/ex2/Lava. java
class Lava {
private int speed = 5; // 5 kilometers per hour
voia flow() {
}
}
// on CD-ROM in file jvn/ex2 /volcano.java
class volcano {
public static void main (string []args) {
Lava Lava = new Lava( );
1ava.f1ow();
}
}
- 要运行Volcano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“Volcano”这个名字。之后,虚拟机将找到并读入相应的class文件“Volcano.class”。
- 然后它会从导入的class文件里的二进制数据中提取类型信息并放到方法区中。
- 通过执行保存在方法区中的字节码,虚拟机开始执行main( )方法,在执行时,它会一直持有指向当前类( Volcano类)的常量池(方法区中的一个数据结构)的指针。
- main ( )的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机使用指向Volcano常量池的指针找到第一项(引用变量Lavan),发现它是一个对Lava类的符号引用,然后它就检致方法区,看Lava类是否已经被装载了。
- 当虚拟机发现还没有装载过名为“Lava”的类时,它就开始查找并装载文件“Lava.class",并把从读人的二进制数据中提取的类型信息放在方法区中。
- 紧接着,虚拟机以一个直接指向方法区Lava类数据的指针来替换常量池第一项(就是那个字符串“Lava")—-以后就可以用这个指针来快速地访问Lava类了。这个替换过程称为常量池解析,即把常量池中的符号引用替换为直接引用。这是通过在方法区中搜索被引用的元素实现的,在这期间可能又需要装载其他类。在这里,我们替换掉符号引用的“直接引用”是一个本地指针。·
- Java虚拟机总能够通过存储于方法区的类型信息来确定个对象需要多少内存,当Java虚拟机确定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量speed初始化为默认初始值0。假如Lava类的超类Object也有实例变量,则也会在此时被初始化为相应的默认值。
- 当把新生成的Lava对象的引用压到栈中,main( )方法的第一条指令也完成了。接下来的指令通过这个引用调用Java代码(该代码把speed变量初始化为正确初始值5 )。另外一条指令将用这个引用调用Lava对象引用的flow ()方法。
注意,虚拟机开始执行Volcano类中main ( )方法的字节码的时候,尽管Lava类还没被装载,但是和大多数(也许所有)虚拟机实现一样,它不会等到把程序中用到的所有类都装载后才开始运行程序。恰好相反,它只在需要时才装载相应的类。
上图堆空间设计就是,把堆分为两部分:一个句柄池,一个对象池。而一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分:一个指向对象实例变量的指针,一个指向方法区类型数据的指针。
- 好处是有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需要更改一下指针指向对象的新地址就可以了一就是在句柄池中的那个指针。
- 缺点是每次访问对象的实例变量都要经过两次指针传递。
上图堆空间设计就是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。这样设计的优缺点正好与前面的方法相反,它只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减少内存碎片而移动对象的时候,它必须在整个运行时数据区中更新指向被移动对象的引用。
方法表
不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有–个方法表,因为方法表加快了调用实例方法时的效率,从而对Java虚拟机实现的整体性能起着非常重要的正面作用。但是Java虚拟机规范并未要求必须使用方法表,所以并不是所有实现中都会使用它。比如那些具有严格内存资源限制的实现,或许它们根本不可能有足够的额外内存资源来存储方法表。如果一个实现使用方法表,那么仅仅使用一个指向对象的引用,就可以很快地访问到对象的方法表。
这个方法表结构可以包括两部分:
方法表指向的实例方法数据信息:
对象锁:
虚拟机中,堆中每个对象都有一个对象锁,用于协调多线程时对象访问的同步。除了实现锁所需要的数据外,每个Java对象逻辑上还与实现等待集合( wait set)的数据相关联。锁是用来实现多个线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成一个共同目标而协调工作的。
对于一个运行中的Java程序而言.其中的每一个线程都有它自己的PC(程序计数器)寄存器,它是在该线程启动时创建的。PC寄存器的大小是一个字长,因此它既能够持有一个本地指针,也能够持有一个returnAddress。当线程执行某个Java方法时,PC寄存器的内容总是下一条将被执行指令的“地址",这里的“地址”可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是“undefined”。
理解内容1:
在5.3.5中有说到,一个Java程序独占一个Java虚拟机实例,也就是一个Java虚拟机实例就是一个Java程序。那如果是程序他就可以产生多个线程。比如说你的Java程序在启动时需要初始化很多东西,就像大量的图片和图片的文字性说明,这是你就可以创建两个线程T1、T2,一个用于加载图片,一个用于加载图片说明。这时候Java虚拟机就会给这T1、T2两个线程分配Java栈。
是不是可以理解为Java栈就是线程的活动空间?
理解内容2:
在【理解内容1】中提到了两个线程T1、T2,这两个线程在加载图片或说明时肯定需要执行加载图片的方法methodT()1和加载说明的方法methodT2()。
以methodT1()为例,T1线程在调用methodT1()方法时,为T1线程创建的栈会先创建一个栈帧,在他执行的时候将方法的局部变量、参数等数据放入栈帧中,方法执行结束后再将这个栈帧释放掉。
这样的话又可以理解为:每一个方法都可以看做一个栈帧。每当有方法被调用执行,就把该方法的栈帧入栈,方法执行完毕时出栈。
方法的调用和执行:
方法总是先调用再执行,方法调用的过程分为解析和分派两个过程。在Java程序中我们通过方法名调用特定方法,为什么通过方法名就能找到具体方法呢?在class文件中每个方法名只是一个字面量而已,把这个字面量变成对具体方法的引用就是解析的过程。通过解析,我们可以通过方法名得到恰当的具体方法,具有了方法调用的前提。接下来要做的事就是选择要调用的方法,把不同的调用分派到不同的方法。分派又分为静态分派和动态分派两种。
动态分派:
动态分派强调的是从覆盖的方法中分派合适的方法,并且因为这个分配的结果是在实际运行时得出的,所以成为动态分派。(重载方法)
静态分派:
静态分派强调的是从重载的方法中分派合适的方法。JVM根据方法调用时根据传入参数不同分派给类中的重载方法就是静态分配的过程。只是这个分派的过程在编译期根据源代码就能确定下来,所以称为静态分派。
单分派和多分派:
单分派和多分派,根据方法分派时依据的宗量不同,分派又分为单分派和多分派。
动态分派时只需根据实例类型这一因素选择分派即可,所以它只有一个宗量,是单分派。
静态分派时需要根据引用类型和参数表两个因素才能确定分派的方法,所有它有两个宗量,是多分派。
Java是一种静态多分派,动态单分派语言。
执行,通过调用获得了恰当方法,然后才是执行方法的过程。
栈帧由三部分组成:局部变量区,操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,它们是按字长计算的。编译器在编译时就确定了这些值并放在class文件中。而帧数据区的大小依赖于具体的实现。
当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。
局部变量:
操作数栈:
帧数据区:
除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些信息都保存在Java栈帧的帧数据区中。
演示代码:
public class Add {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.print(c);
}
}
编译后的字节码
CA FE BA BE :这四位表示这是一个class文件,虚拟机只识别这四位开头的文件。
00 00 00 34:这四位是版本号,后两位是主版本号,接着就是常量池的大小和常量池数据,这里不做说明。
常量池:
反汇编码:
- 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
将输出类的静态域压栈
- 12: invokevirtual #3 // Method java/io/PrintStream.print:(I)V
执行接口方法
- 15: return
结束出栈
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。可以把这看做是虚拟机利用本地方法来动态扩展自己。就如同Java虚拟机的实现在按照其中运行的Java程序的吩咐,调用属于虚拟机内部的另-一个(动态连接的)方法。
执行引擎:
这个术语也可以有三种理解:
运行中Java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。
从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过即时编译器执行编译过的本地代码。Java虚拟机的实现可能用一些对用户程序不可见的线程,比如垃圾收集器。这样的线程不需要是实现的执行引擎的实例。所有属于用户运行程序的线程,都是在实际工作的执行引擎。
指令集:
含操作数
指令0: iconst_1,iconst表示操作码,1表示操作数,这个指令的意思就是将int类型的1
压栈。
不含操作数
例如指令iadd,后边没有操作数,他表示将操作数栈种的值相加,将结果压栈。
代码示例
代码:
public class Add { public static void main(String[] args) { Add add = new Add(); add.addMethod(); } public void addMethod(){ int a = 1; int b = 2; int c = a + b; System.out.print(c); } }
反汇编:
public class com.github.erbudu.prj.test.Add { public com.github.erbudu.prj.test.Add(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."
":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class com/github/erbudu/prj/test/Add 3: dup 4: invokespecial #3 // Method " ":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method addMethod:()V 12: return public void addMethod(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: istore_3 8: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 12: invokevirtual #6 // Method java/io/PrintStream.print:(I)V 15: return } 指令集序列(方法的字节码流):
public static void main(java.lang.String[]); Code: 0: new #2 // class com/github/erbudu/prj/test/Add 3: dup 4: invokespecial #3 // Method "
":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method addMethod:()V 12: return ——————————————————————————————————————————— public void addMethod(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: istore_3 8: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 12: invokevirtual #6 // Method java/io/PrintStream.print:(I)V 15: return 说明:
含操作数的指令
- 例如指令0: iconst_1,
iconst
表示操作码,1
表示操作数,这个指令的意思就是将int类型的1
压栈。不含操作数的指令
- 例如指令iadd,后边没有操作数,他表示将操作数栈种的值相加,将结果压栈。
执行引擎实例执行指令集:
线程 ==》 执行引擎实例;
方法 ==》指令集序列
Java class文件是Java程序二进制文件的精确定义。每一个class文件都对Java类或者Java接口做出了全面的描述。一个Java文件中只能包含一个类或接口。正是对Java文件的明确定义,使得所有Java虚拟机能够正确的读取和解释所有Java class文件。
以下观点为个人理解,慎重参考:
1.windows有windows的可执行文件,linux有linux的可执行文件,所以,Java虚拟机的可执行文件就是class文件。
2.class文件除了能被虚拟机识别外,在其他地方可能一无是处。虚拟机可以识别解析class文件的数据结构和指令集。
3.解释执行:Java方法就是一系列的指令集,当然这些指令集只有虚拟机可以识别。执行的时候将指令集解释为本地操作系统可执行的机器码再执行。
4.编译执行:也可直接将class文件翻译为计算机可执行文件直接执行,各有优缺。
class文件结构的数据类型:
无符号类型(基本类型)字节占位符,用来描述数字、索引引用、数量值或UTF-8编码构成的字符串值。
类型名称 | 字节数 |
---|---|
u1 | 1 |
u2 | 2 |
u4 | 4 |
u8 | 8 |
表
表是由多个无符号数或其他表作为数据项构成的复合数据类型,一般以"_info"结尾; 表用来描述有层次关系的复合结构的数据; 表中的项长度不固定;
class文件基本类型
占位符 | 字节长度 | 符号类型 |
---|---|---|
u1 | 1 | 无符号类型 |
u2 | 2 | 无符号类型 |
u4 | 3 | 无符号类型 |
u8 | 8 | 无符号类型 |
ClassFile表的格式
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u4 | magic | 1 | 魔数,Java文件的前四位,固定的四位CA FE BA BE ,用于区分该文件是否是Java文件,虚拟机只识别以正确魔数开头的文件。 |
u2 | minor_version | 1 | 次版本号,魔数后的两位。 |
u2 | major_version | 1 | 主版本号,次版本号后边的两位。和次版本号共同确定了特定的class文件格式,通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取class文件。如果class文件的版本号超出了Java虚拟机所能处理的有效范围,Java虚拟机将不会处理该class文件。 |
u2 | constant_pool_count | 1 | 常量池计数,主版本号的后两位。常量池列表中没有索引为0的入口,但是被constant_pool_count计数在内,所以计数比实际的常量池中要多1 |
cp_info | constant_pool | constant_pool_count-l | 常量池,常量池计数后的是常量池,文件中的常量内容。常量池入口大多都指向了常量池其他表的入口。 每个常量池人口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型。一旦Java虚拟机获取并解析这个标志,Java虚拟机就会知道在标志后的常量类型是什么。 |
u2 | access_flags | 1 | 紧接常量池后的两个字节称为access_flags,它展示了文件中定义的类或接口的几段信息。例如,访问标志指明文件中定义的是类还是接口;访问标志还定义了在类或接口的声明中,使用了哪种修饰符﹔类和接口是抽象的,还是公共的;类的类型可以为final,而final类不可能是抽象的;接口不能为final类型。这些标志位的定义如表6-4所示。 |
u2 | this_class | 1 | 对常量池的索引 |
u2 | super_class | 1 | 常量池索引 |
u2 | interfaces_count | 1 | 紧接着super_class的是interfaces_count。此项的含义为:在文件中由该类直接实现或者由接口所扩展的父接口的数量。 |
u2 | interfaces | interfaces_count | 在接口计数的后面,是名为interfaces的数组,它包含了对每个由该类或者接口直接实现的父接口的常量池索引。每个父接口都使用一个常量池中的CONSTANT_Class_info人口来描述,该CONSTANT_Class_info人口指向接口的全限定名。这个数组只容纳那些直接出现在类声明的implements子句或者接口声明的extends子句中的父接口。超类按照在implements子句和extends子句中出现的顺序(从左到右)在这个数组中显现。 |
u2 | fields_count | 1 | 变量计数,它是类变量和实例变量的字段的数量总和。 |
field_info | fields | fields_count | 同长度的field_info表的序列( fields_count指出了序列中有多少个field_info表)。只有在文件中由类或者接口声明了的字段才能在fields列表中列出。在fields列表中,不列出从超类或者父接口继承而来的字段。另一方面,fields列表可能会包含在对应的Java源文件中没有叙述的字段,这是因为Java编译器可能会在编译时向类或者接口添加字段。例如,对于一个内部类的fields列表来说,为了保持对外围类实例的引用,Java编译器会为每个外围类实例添加实例变量。源代码中并没有叙述任何在fields列表中的字段,它们是被Java编译器在编译时添加进去的,这些字段使用Synthetic属性标识。 每个field_info表都展示了一个字段的信息。此表包含了字段的名字、描述符和修饰符。如果该字段被声明为final,field_info表还会展示其常量值。这样的信息有些放在field_info表中,有些则放在由field_info表所指向的常量池中。 |
u2 | methods_count | 1 | 方法计数 |
method_info | methods | methods_count | method_info表中包含了与方法相关的一些信息,包括方法名和描述符(方法的返回值类型和参数类型)。如果方法既不是抽象的,又不是本地的,那么method_info表就包含方法局部变量所需的栈空间长度、为方法所捕获的异常表、字节码序列以及可选的行数和局部变量表。如果方法能够抛出任何已验证的异常,那么method_info表就会包括一个关于这些已验证异常的列表。 |
u2 | attributes_count | 1 | 属性计数,attribute _info表的数量总和。 |
attribute_info | attribute | attributes_count | 每个attribute _info的第一项是指向常量池中CONSTANT_Utf8_info表的索引、该表给出了属性的名称。 |
类或接口的全名,例如:在class文件中,java.util.Hashtable的全限定名表示为java/util/Hashtable。
字段名和方法名以简单名称的形式出现在常量池入口中。例如,一个指向类java.lang.Object所属方法String toString( )的常量池入口有一个形如“toString”的方法名。
字段的描述符给出了字段的类型;方法描述符给出了方法的返回值和方法参数的数量、类型以及顺序。
术语解释:
字面量:文本字符串、被声明的静态常量、基本数据类型、其他
符号应用:类和结构的完全限定名、字段名称和描述符、方法名称和描述符
每个常量池入口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型。一旦Java虚拟机获取并解析这个标志,Java虚拟机就会知道在标志后的常量类型是什么。
常量池是一个可变长度cp_info表的有序序列。表现如下
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u1 | info | tag决定 |
个人理解:
常量池就像是一个归类的资源仓库,将代码中的常量分类放在同一类型的序列表中。
示例:
i
是一个int类型的常量,在常量池中用Integer
标志表示,Integer
标志对应的info表为CONSTANT_lnteger_info
,
他的表结构只有两项 tag和byte,tag就是类型标志,byte是值。因为i
是基本类型的常量,所以,他的直接量就是1,不再指向别的地址。
反例,比如Methodref
对应方法类型的标志,他的表结构为
tag = 类型标志
class_index = 被引用方法的类的CONSTANT_Class_info人口的索引。所以这一项指向的一定是常量池中某个Class类型的入口
name_and_type_index = 方法描述,指向CONSTANT_NameAndType_info入口的索引,该入口提供了方法的简单名称以及描述符。
所以方法类型的常量入对应的表中第二项和第三项会分别指向class类型和ameAndType的类型入口。下图为例。
总的来讲,常量池中的类型表之间是纵横交错的相互指向,下边会对每个常量池类型表的结构一一说明。
CONSTANT_Utf8
的标志值为1
(点击跳转常量池标志表)bytes
的长度,字节数。可变长度的CONSTANT_Utf8_info表使用一种UTF-8格式的变体来存储一个常量字符串。这种类型的表可以存储多种字符串,包括以下:
CONSTANT_lnteger_info
说明:
该表值存int类型的值,不存储符号引用。
CONSTANT_lnteger
的标志值为3
(点击跳转常量池标志表)说明:
该表只存储float类型的值,不存储符号引用。
CONSTANT_Float
的标志值为4
(点击跳转常量池标志表) CONSTANT_Class_info
说明:
固定长度的CONSTANT_Class_info表使用符号引用来表述类或者接口。无论指向类、接口、字段,还是方法,所有的符号引用都包含一个CONSTANT_Class_info表。
CONSTANT_Class
的标志值为7
(点击跳转常量池标志表)name_index给出的是指向utf8的一个索引,因为类或接口的完全限定名是在utf8表中,所以这个索引最终会指向常量表中的完全限定名。
固定长度的CONSTANT_String_info表用来存储文字字符串值,该值亦可表示为类java.lang.String的实例。该表只存储文字字符串值,不存储符号引用。
CONSTANT_String_info
说明:
CONSTANT_String
的标志值为8
(点击跳转常量池标志表)不存储符号引用,string_index给出的是指向utf8表的索引,因为string类型的是在utf8表中,索引最终指向的肯定是一个字符串。
CONSTANT_Fieldref_info
说明:
固定长度的CONSTANT_Fieldref_info表描述了指向字段的符号引用。
CONSTANT_Fieldref
的标志值为9
(点击跳转常量池标志表)固定长度的CONSTANT_Methodref_info表使用符号引用来表述类中声明的方法(不包括接口中的方法)。
CONSTANT_Methodref_info
说明:
CONSTANT_Methodref
的标志值为10
(点击跳转常量池标志表) CONSTANT_NameAndType_info
说明:
固定长度的CONSTANT_NameAndType_info表构成指向字段或者方法的符号引用的一部分。该表提供了所引用字段或者方法的简单名称和描述符的常量池入口。
CONSTANT_NameAndType
的标志值为12
(点击跳转常量池标志表)字段信息对应field_info表,不会有同名字段。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
constant_utf8_info
入口的索引。constant_utf8_info
入口的索引。attribute_info
表的数量。attributes_info
表组成。在class文件中,方法由一个长度可变的method_info表来描述。
constant_utf8_info
入口的索引。constant_utf8_info
入口的索引。attribute_info
表的数量。attributes_info
表组成。名称 | 使用者 | 描述 |
---|---|---|
Code | method_info | 方法的字节码和其他数据 |
Constant Value | field_info | final变量的值 |
Deprecated | field_info、method_info | 字段或方法被禁用的指示符 |
Exception | method_info | 方法被抛出的可能被检测的异常 |
Inner Class | Class File | 内部、外部类的列表 |
Line Number Table | Code_attribute | 方法行号和字节码的映射 |
LocalVariableTable | Code_attribute | 方法的局部变量的描述 |
SourceFile | ClassFile | 源文件名 |
Synthetic | field_info、method_info | 编译器产生的字段或者方法的指示符 |
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
Java虚拟机通过加载、连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。
连接分为三个子步骤:
- 验证:去报Java类型正确可以被Java虚拟机使用。
- 准备:分配内存
- 解析:将常量池中的符号引用转换为直接引用。这个过程也可以初始化之后进行
类和接口的初始化时机
除了上边说的6种情况外,其他使用Java类型的方式都是被动使用,他们不会导致Java类型的初始化。
使用某个类型前要求所有的超类都要被初始化,而一个接口的初始化并不要求他的祖先接口预先初始化。
装载阶段由三个基本动作组成,要装载一个类型,Java虚拟机必须:
验证主要是对字节码完整性的验证,第三章有详细的讲到。
准备阶段主要是为类变量分配内存,设置默认初始值。在准备阶段是不会执行Java代码的。
验证、准备、解析都是连接的过程,解析就是在类型的常量池中寻找类、接口、字段、方法的符号引用,把这些符号引用替换成直接引用的过程。
初始化就是为类变量赋予正确的初始值。注意区分不是类型的默认值,是初始值。
初始化永远先初始化这个类的超类。
一个类一旦被装载、连接、初始化之后就可以使用了。程序可以访问他的静态字段、静态方法以及创建他的实例。
三种创建类实例的方法:
以上为显示实例化类的方法,其他隐式初始化类的方式不做赘述。
如果某些动态装载的类型只是临时需要,当他不再被使用时,占据的内存空间可以使用卸载的方式使其被释放。