java虚拟机
Java虚拟机(1)
- Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
- JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
- Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
JVM的架构模型
Java编译器输入的指令流主要有两种架构:
- 基于栈的指令集架构
- 基于寄存器的指令集架构
具体来说:这两种架构之间的区别:
基于栈式架构的特点
- 设计和实现更简单,适用于资源受限的系统
- 避开了寄存器的分配难题:使用零地址指令方式分配
- 指令流中的指令大部分是零地址指令,其执行过程依赖操作栈。指令集更小,编译器容易实现
- 不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点
- 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主
总结
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
java虚拟机的体系结构
- 运行时数据区
当Java虚拟机运行一个程序时,需要内存在存储许多东西。比如字节码,程序创建的对象,传递的方法参数,返回值,局部变量等等。JVM会把这些东西都组织到几个“运行时数据区”中便于管理。
- 方法区
当JVM使用类装载器定位Class文件,并将其输入到内存中时。会提取Class文件的类型信息,并将这些信息存储到方法区中。同时放入方法取中的还有该类型中的类静态变量。下面我们具体看看需要存储哪些信息?
●该类型的全限定名。如java.io.FileOutputStream
●该类型的直接超类的全限定名。如java.io.OutputStream
●该类型是类类型还是接口类型。
●该类型的访问修饰符(public、abstract、final)。
●任何直接超接口的全限定名的有序列表。如java.io.Closeable, java.io.Flushable。
●该类型的常量池。比如所有类型、方法和字段的符号。基本数据类型的直接数值等。
●字段信息。对类型中声明的每个字段,方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺寻也必须保存。字段名
字段的类型
字段的修饰符(public, private, protected, static, final, volatile,
transient的某个子集)●方法信息。和字段一样保存方法的相关信息。
方法名
方法的返回类型
方法的参数的数量和类型
方法的修饰符
方法的字节码
操作数栈和栈帧中局部变量的大小 (见下面Java栈的内容)
异常表
●类静态变量。这里要注意:类中的静态变量时存放在方法区中的。并不是存放在堆中某一个该类型的对象中的。
也就是我们常说的“类静态变量属于类,而不属于对象”这句话的由来了。
●指向ClassLoader类的引用。任何类都需要被类装载器装入内存。如果是被用户自定义类装载器装载的,那么JVM必须在类型信息中存储对该装载器对象的引用。
●指向Class类的 引用。对于每一个被装载的类型,虚拟机都会相应的为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方 法区中的类型信息关联起来。 这就使得我们可以在程序运行时查看某个加载进内存的类的当前状态信息。也就是反射机制实现的根本。
●方法表。 为了能快速定位到类型中的某个方法。JVM对每个装载的类型都会建立一个方法表,用于存储该类型对象可以调用的方法的直接引用,这些方法就包括从超类中继 承来的。而这张表与Java动态绑定机制( 参见《java动态绑定机制实现多态 》 )的实现是密切相关的。
●方法区是多线程共享的。也就是当虚拟机实例开始运行程序时,边运行边加载 进Class文件。不同的Class文件都会提取出不同类型信息存放在方法区中。同样,方法区中不再需要运行的类型信息会被垃圾回收线程丢弃掉。
●运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。Java虚拟机规范没有对这部分做任何细节的要求。
运行时常量池相对于Class文件常量池的一个重要特性是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,比如说String的intern()方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
- 堆
Java程序在运行时创建的所有类型对象和数组(数组在Java虚拟机中是一个真正的对象)都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。但是堆中开辟对象的空间并没有任何人工 指令可以回收,而是通过JVM的垃圾回收器负责回收。
- 堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享,所以对它的访问需要注意同步问题,方法和对应的属性都需要保证一致性。
- Java堆是垃圾收集器管理的主要区域。因此也叫“GC堆”。由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代(Eden空间、From
Survivor和To Survivor空间)和老年代。- Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样。 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
- PC寄存器(程序计数器)
- PC寄存器(Program Counter Register)严格来说是一个数据结构,它用于保存当前正常执行的程序的内存地址。线程私有。
- 每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。
- 每个线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的时正在执行的虚拟机字节码指令的地址;如果正在执行的时Native方法,这个计数器值则为空。
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- 虚拟机栈
- 栈 与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。这也就是为什么多线程编程时,两个相同线程执行同一方法时,对方法内的局部变量时不需要数据同步的原因。
- 在这个Java栈中又会含有多个栈帧,这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
- 局部变量表
- 局部变量表(Local VariableTable)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,并在Java编译为Class文件时,就已确定该方法所需分配的局部变量表的最大容量
- 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,对象引用(reference类型)和
returnAddress类型(它指向了一条字节码指令的地址)- 线程私有,不允许跨线程访问,随方法调用创建,方法退出销毁
- 编译期间长度已经确定,局部变量元数据存储在字节码中
- 局部变量表是栈帧最主要的存储空间,决定了栈的深度
- 操作数栈
- 保存中间计算的临时结果,字节码指令在执行过程中的中间计算结果存储在操作数栈
- 操作数栈(Operand Stack)也常被成为操作栈,是一个后入先出栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。其最大深度在编译时就被写到了Code属性的max_stacks中
- 操作数栈在方法的执行过程中,根据字节码指令往栈中写入数据或提取数据,即入栈和出栈操作。虽然栈是用数组实现的,但根据栈的特性,对栈中数据访问不能通过索引,而是只能通过标准的入栈和出栈操作来完成一次数据访问
- 动态连接
- 将符号引用转为直接引用
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
- 持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
- 在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析
- 另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接
方法出口
- 存放调用方法的程序计数器
- 当一个方法开始执行后,有2种方式可以退出这个方法
- 方法返回指令 : 执行引擎遇到一个方法返回的字节码指令(return),这时候可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口
- 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值,而方法异常退出时,返回地址要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息
- 本地方法栈
我们知道,Java属于上层语言,在对操作系统的控制层面上相比c,c++逊色不少,但是为了实现某些功能需要调用操作系统提供的相关函数,而Java中的native方法的职责就为这一功能的实现提供了一个类似转换的接口
- 在native方法中,只提供了接口,没有具体的实现,实现部分由C或C++去实现,总结来说:
- 一个native方法就是一个Java调用非Java代码的接口 定义一个native方法时,并不提供具体的实现
- native的方法可以调用其他语言接口实现对操作系统更加底层的操作,比如windows下对dll文件的调用