首先我们来探讨一个问题,关于Java语言的特点,无关性的基石:一次编写,到处运行。
关于这个问题,首先先来了解一下Java语言。
1.什么是Java?
Java是一门面向对象的高级编程语言
2.那什么是编程语言呢?
说白了就是让人类能够和计算机沟通,计算机能够懂得语言。
3.什么是机器语言?
cpu能够直接认识的数据指令。
4.什么是汇编语言?
低级语言,通过汇编器翻译成机器语言。
5.什么是高级语言?
C、C++、Java、Python、Go语言等
6.编译型和解释型
编译型:使用专门的编译器,针对特定的平台,将高级语言源代码一次性编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行程序的格式。C、C++、Go
特点:执行速度快,效率高;但需要依靠编译器、跨平台性差。
把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。
解释型:使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。
是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。Python、Javascript
特点:执行速度慢、效率低;依靠解释器、跨平台性好。
把做好的源程序翻译一句,然后执行一句,直到结束。
问题来了,Java属于哪种类型的语言呢?
Java属于编译型+解释型的高级语言。
为什么这么讲呢?
其实不是因为javac将Java源码编译成class文件才说Java属于编译+解释语言,因为这个编译器编译之后,生成的类文件不能直接在对应的平台上运行。
那为什么说Java是编译型+解释型语言呢?
因为class文件最终是通过JVM来翻译才能在对应的平台上运行,而这个翻译大多时候是解释过程,但是也会有编译,称之为运行时编译,即JIT。
综上所述,Java是一门编译型+解释型的高级语言。
7.什么是JVM?
Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现(Windos,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。这个也是Java可以款平台的一个关键因素。
8.Java程序从源代码到运行的过程
.java文件(源代码)通过JDK中的javac编译成.class文件(JVM可理解的Java字节)再通过JVM解析成机器可执行的二进制 机器码。
9.那什么又是JDK呢?
JDK及Java Development Kit,它是功能齐全的Java SDK。它拥有JRE拥有的一切,还有编译器(javac)和工具(javadoc和jdb)。它能够创建和编译程序。
10.什么是JRE?
JRE即Java运行时环境。它是运行已编译Java程序的所有内容集合,包括Java虚拟机(JVM),Java类库,java命令和其他的一些基础构建。但是,他不能用于创建新程序。
回归正题。铺垫了那么多的知识点,更多的还是有助于JVM的理解。
11.Class类文件的结构
11.1 魔数与Class文件版本
每个Class文件前四个字节叫魔数,唯一作用是确定这个文件是否为一个能够被虚拟机接受的Class文件。(咖啡宝贝)
紧接着魔数的4个字节存储的是Class文件的版本号:第5和6个字节是次版本号,第7和8个字节是主版本号。
高版本的JDK可以兼容低版本的Class文件,但不能运行以后版本的Class文件。
11.2 常量池
紧接着主次版本号是常量池入口。常量池主要存放两大类常量:字面量和符合引用。
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
符号引用属于编译原理方面的概念,主要包括下面几类常量:被模块导出或者开放的包,类和接口的全限定名,字段的名称和描述符,方法的名称和描述符,方法句柄和方法类型,动态调用点和动态常量。
11.3 访问标志
常量池结束后紧接着两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Classs是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否会被声明为final;等等。
11.4 类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,Class文件中由这三项数据来确定该类型的继承关系。
11.5 字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段可以包括的修饰符有字段的作用域、是实例变量还是类变量、可变性、并发可见性、可否被序列化、字段数据类型、字段名称。
11.6 方法表集合
方法表的结构如同字段表一样,依次包括访问标志、名称索引、描述符索引、属性表集合。
11.7 属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
11.7.1 Code属性
Java程序方法体里面的代码经过Javac编译处理之后,最终变为字节码指令存储在Code属性内。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
11.7.2 Exceptions属性
Exceptions属性是在方法表中与Code属性平级的一项属性。作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。
11.7.3 LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,但如果选择不生成LineNumberTable属性,对程序运行产生的最主要影响是抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
11.7.4 LocalVariableTable及LocalVariableTypeTable属性
LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
11.7.5 SourceFile及SourceDebugExtension属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。SourceDebugExtension属性用于存储额外的代码调试信息。
11.7.6 ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。
11.7.7 InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
11.7.8 Deprecated及Synthetic属性
Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用“@deprecated”注解进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
11.7.9 StackMapTable属性
它是一个相当复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
StackMapTable属性中包含零至多个栈映射帧,每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。
11.7.10 Signature属性
它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。记录泛型签名信息。因为Java语言的泛型采用的是擦除法实现的伪泛型,字节码(Code属性)中所有的泛型信息编译(类型变量、参数化类型)在编译之后都通通被擦除掉。
11.7.11 BootstrapMethods属性
它是一个复杂的变长属性,位于类文件的属性表中。用于保存invokedynamic指令引用的引导方法限定符。
11.7.12 MethodParameters属性
它是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息。
11.7.13 模块化相关属性
Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。
Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requires、exports、opens、uses和provides定义的全部内容。
ModulePackages是另一个用于支持Java模块化的变长属性,它用于描述该模块中所有的包,不论是不是被export或者open的。
ModuleMainClass属性是一个定长属性,用于确定该模块的主类。
11.7.14 运行时注解相关属性
为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameter-Annotations四个属性。
12.关于字节码
简介:JVM指令由一个字节长度的、代表着某种特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作的参数构成。
12.1 字节码与数据类型
在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务。
12.2 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。存储数据的操作数栈和局部变量表主要由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。
12.3 运算指令
算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。
·加法指令:iadd、ladd、fadd、dadd
·减法指令:isub、lsub、fsub、dsub
·乘法指令:imul、lmul、fmul、dmul
·除法指令:idiv、ldiv、fdiv、ddiv
·求余指令:irem、lrem、frem、drem
·取反指令:ineg、lneg、fneg、dneg
·位移指令:ishl、ishr、iushr、lshl、lshr、lushr
·按位或指令:ior、lor
·按位与指令:iand、land
·按位异或指令:ixor、lxor
·局部变量自增指令:iinc
·比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
12.4 类型转换指令
类型转换指令可以将两种不同的数值类型相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
·int类型到long、float或者double类型
·long类型到float、double类型
·float类型到double类型
12.5 对象创建与访问指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
·创建类实例的指令:new
·创建数组的指令:newarray、anewarray、multianewarray
·访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic
·把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
·将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
·取数组长度的指令:arraylength
·检查类实例类型的指令:instanceof、checkcast
12.6 操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令。
·将操作数栈的栈顶一个或两个元素出栈:pop、pop2
·复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
·将栈最顶端的两个数值互换:swap
12.7 控制转移指令
控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。
·条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
·复合条件分支:tableswitch、lookupswitch
·无条件分支:goto、goto_w、jsr、jsr_w、ret
12.8 方法调用和指令返回
·invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
·invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
·invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
·invokestatic指令:用于调用类静态方法(static方法)。
·invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
12.9 异常处理指令
在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成。
12.10 同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。