JVM

本文引用了部分网络材料,仅为学习,如果冒犯请见谅。

1.1.1  概念

JVM:Java Virtual Mechinal(JAVA虚拟机)。JVMJRE的一部分它是一个虚构出来的计算机本身就是一个计算机体系结构。是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用。Java语言是跨平台运行的,其实就是不同的操作系统,使用不同的JVM映射规则,让其与操作系统无关,完成了跨平台性。JVM 对上层的 Java 源文件是不关心的,它关注的只是由源文件生成的类文件( class file)。类文件的组成包括 JVM 指令集,符号表以及一些补助信息。JVM本身是一个规范,所以可以有多种实现,除了Hotspot外,还有诸如OracleJRockitIBMJ9也都是非常有名的JVM

1.1.2  体系结构

JVM的体系结构如下图所示。


可以看出,JVM主要由类加载器子系统运行时数据区(内存空间)执行引擎以及与本地方法接口等组成。其中运行时数据区又由方法区、堆、Java栈、PC寄存器、本地方法栈组成。从上图中还可以看出,在内存空间中,方法区和堆是所有Java线程共享的,而Java栈、本地方法栈、PC寄存器则由每个线程私有。

l  ClassLoader 是负责加载class文件 , class文件在文件开头有特定的文件标示 , 并且ClassLoader只负责class文件的加载 , 至于它是否可以运行,则由ExecutionEngine决定。

l  Native Interface 是负责调用本地接口的。他的作用是调用不同语言的接口给JAVA用 , 他会在Native Method Stack中记录对应的本地方法 , 然后调用该方法时就通过Execution Engine加载对应的本地lib。原本多于用一些专业领域, 如JAVA驱动 , 地图制作引擎等 , 现在关于这种本地方法接口的调用已经被类似于Socket通信 , WebService等方式取代。

l  Execution Engine 是执行引擎 , 也叫Interpreter。Class文件被加载后 , 会把指令和数据信息放入内存中,ExecutionEngine则负责把这些命令解释给操作系统。

l  Runtime Data Area 则是存放数据的, 分为五部分:Stack,Heap,Method Area,PC Register,Native Method Stack。

Java语言支持通过JNI(JavaNative Interface)来实现本地方法的调用。但是需要注意到,如果你在Java程序用调用了本地方法,那么你的程序就很可能不再具有跨平台性,即本地方法会破坏平台无关性。

JavaHotSpot Client VM和Java HotSpot Server VM是JDK关于JVM的两种不同的实现,前者可以减少启动时间和内存占用,而后者则提供更加优秀的程序运行速度

 

1.1.3  运行过程

1.1.4  Class Loader

简单的说,类加载器的作用就是解析.class文件,并在jvm内存的“方法区”中为其分配内存空间,并形成与该类相关的数据结构(静态变量、成员变量、成员方法、构造函数)。

1.1.4.1  类加载方式

Java基础类和扩展类都是预先加载的,而用户程序类是按需加载的。

1 按加载时间区分

按加载按照加载时机,是否自动加载分为两种:预先加载和按需加载。

l  预先加载的类是JVM启动之后,应用程序运行之前。至少包含rt.jar中的所有类。

l  按需加载则是在程序运行过程中,JVM遇到一个还未被装载的类,这时由ClassLoader把该类载入内存。

2  按加载方式区分

类加载按照方式来分,也是两种:隐式加载和显式加载。

l  隐式加载是通过new的方式,在类初始化时由JVM根据相应的Class Loader将类载入。

l  显式加载则是程序员在代码中显式利用某个ClassLoader将类载入。有两种方式:

[1].  通过Class.forName()方法动态加载

[2].  通过ClassLoader.loadClass()方法动态加载

 

1.1.4.2  分类

Java 的class loader分为4类:Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader及User Defined ClassLoader 。

这四类加载器存在父子关系。BootstrapClassLoader是ExtensionClassLoader的parent,Extension ClassLoader是App ClassLoader的parent。但是这并不是继承关系,只是语义上的定义。基本上,每一个ClassLoader实现,都有一个Parent ClassLoader。详细内容如下:

l  启动类加载器(BootStrap ClassLoader):负责加载rt.jar文件中所有的Java类,即Java的核心类(以java.*开头的包)都是由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。该类是用特定于操作系统的本地代码实现的,属于JAVA虚拟机的内核,Bootstrap类不用专门的类装载器去进行装载。加载路径为:sun.boot.class.path

l  扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包(以javax.*开头的包以及ext下的包)。Java语言编写的java类。

l  系统类加载器(System Class Loader):负责加载启动参数中指定的Classpath中的jar包及目录。通常我们自己写的Java类也是由该ClassLoader加载,即当使用java命令去启动执行一个类时,JAVA虚拟机使用AppClassLoader加载这个类。在Sun JDK中,系统类加载器的名字叫AppClassLoader。它是Java语言编写的java类。

l  用户自定义类加载器(User Defined Class Loader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

1.1.4.3  工作原理

类加载分为装载、链接、初始化三步。如下图所示:


在加载阶段主要用到的是JVM内存中的“方法区”

方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。

如果把方法的代码看作它的“静态”部分,而把一次方法调用需要记录的临时数据看做它的“动态”部分,那么每个方法的代码是只有一份的,存储于JVM的方法区中;每次某方法被调用,则在该调用所在的线程的的Java栈上新分配一个栈帧,用于存放临时数据,在方法返回时栈帧自动撤销。

1.1.4.3.1  装载

装载是将指定的java字节码(.class文件)以二进制的方式加载至JVM内存中,然后将二进制数据流按照字节码规范解析成jvm内部的运行时的数据结构。java只对字节码进行了规范,并没有对内部运行时数据结构进行规定,不同的jvm实现可以采用不同的数据结构。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。在内存中ClassLoader实例和类的实例都位于堆中它们的类信息都位于方法区

当一个类的二进制解析完毕后,jvm最终会在堆上生成一个java.lang.Class类型的实例对象,该对象是方法区内这些数据的访问入口,通过这个对象可以访问到该类在方法区的内容。

双亲委派模型:装载过程采用了一种被称为“双亲委派模型(Parent Delegation Model”的方式,当一个ClassLoader要加载类时,它会先请求它的父ClassLoader加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader无法加载指定的类时,它才会自己加载类。

双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存在。这样即使有恶意的类冒充自己在核心包(例如java.lang)下,由于它无法被启动类加载器加载,也造成不了危害。

由此也可见,如果用户自定义了类加载器,那就必须自己保障类加载过程中的安全。

全盘委托机制:当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有 Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入。

流程参见下图。


1.1.4.3.2  链接

链接的任务是把二进制的类型信息合并到JVM运行时状态中去。链接分为以下三步:

1)       验证:校验.class文件的正确性,保证加载的字节码符合java语言的规范,并且不会给虚拟机带来危害。比如验证这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。按照验证的内容不同又可以细分为4个阶段:文件格式验证(这一步会与装载阶段交叉进行),元数据验证,字节码验证,符号引用验证(这个阶段的验证往往会与解析阶段交叉进行)。

2)       准备:为类的静态变量分配内存,并设置jvm默认的初始值。对于非静态的变量,则不会为它们分配内存。

在jvm中各类型的初始值如下:

int,byte,char,long,float,double默认初始值为0

boolean 为false(在jvm内部用int表示boolean,因此初始值为0)

reference类型为null

对于final static基本类型或者String类型,则直接采用常量值(这实际上是在编译阶段就已经处理好了)。

3)       解析(可选):主要是把类的常量池中的符号引用解析为直接引用,这一步可以在用到相应的引用时再解析。

l  解析过程主要针对于常量池中的

CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量。

l  jvm规范并没有规定解析阶段发生的时间,只是规定了在执行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic这13个指令应用于符号指令时,先对它们进行解析,获取它们的直接引用。

l   jvm对于每个加载的类都会有在内部创建一个运行时常量池(参考上面图示),在解析之前是以字符串的方式将符号引用保存在运行时常量池中,在程序运行过程中当需要使用某个符号引用时,就会促发解析的过程,解析过程就是通过符号引用查找对应的类实体,然后用直接引用替换符号引用。由于符号引用已经被替换成直接引用,因此后面再次访问时,无需再次解析,直接返回直接引用。

1.1.4.3.3  初始化(仅在满足条件时才进行此步骤

初始化阶段是根据用户程序中的初始化语句为类的静态变量赋予正确的初始值。这里初始化执行逻辑最终会体现在类构造器方法<clinit>()方中。该方法由编译器在编译阶段自动生成,它封装了两部分内容:静态变量的初始化语句和静态语句块。

JVM规范严格定义了何时需要对类进行初始化:

a、通过new关键字、反射、clone、反序列化机制实例化对象时。

b、调用类的静态方法时。

c、使用类的静态字段或对其赋值时。

d、通过反射调用类的方法时。

e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。

f、JVM启动时被标记为启动类的类(简单理解为具有main方法的类)

之后,执行引擎将JAVA的字节码转换为host system的二进制码,然后交给操作系统执行,也就是从main方法开始执行。

1.1.5  Run time area(内存结构)

运行数据区(runtime data area)是jvm管理的内存空间。编译后的程序都被加载到这里,之后才开始运行。其结构图如下所示:


结合垃圾回收机制,将堆和虚拟机栈进行细化:

 

 

表1‑1 Runtime data area

序号

名词

解释

1.         

PROGEAM COUNTER REGISTER

(程序计数器)

每一个用户线程对应一个程序计数器,用来指示当前线程所执行字节码的行号(.class文件中的位置)。由程序计数器给文字码解释器提供下一条要执行的字节码的的位置。根据jvm规范,在这个区域中不会抛出OutOfMemoryError的内存异常。

2.         

JAVA STACK

java虚拟机栈)

线程私有(线程安全)分为三部分:局部变量区、操作数栈、帧数据区,可能连续也可能不连续.

最典型的应用是方法调用。Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。前面已经提到Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区

3.         

HEAP

(堆)

线程共享

所有的对象实例以及数组都要在堆上分配

回收器主要管理的对象

4.         

MEATHOD AREA

(方法区)

线程共享的内存区域

非堆主要区域

存储类信息、常量、静态变量,即编译器编译后的代码

5.         

NATIVE METHOD STACK

(本地方法栈)

为虚拟机使用到的Native 方法服务

几点说明:

方法区和堆是线程共享的,所有的运行在jvm上的程序都能访问这两个区域。堆,方法区和虚拟机的生命周期一样,随着虚拟机的启动而存在,而栈和程序计数器依赖用户线程的启动和结束而建立和销毁,即每个线程都有自己的栈、程序计数器和本地方法栈


JVM是基于栈执行的,每个线程会jvm stack中建立一个每个栈又包含了若干个栈帧(每个方法的执行都会创建一个栈,每个栈帧包含了局部变量、操作数栈、动态连接、方法的返回地址信息等每一个方法被调用直至执行完成的过程,就对应着一个栈帧在当前线程所在栈中从入栈到出栈的过程。

 

除了pc计数器区,其他区都有可能产生oom,栈区还有可能stackoverflow

 

1.1.6  执行引擎(excution engine)

 

执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。

JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例

JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。注意这里是“通常”情况,之后会讲到由于优化导致的例外。

程序的执行可以直接解释为是对方法的递归调用,通过一连串的方法链来最终得出执行结果,亦即是说虚拟机对程序的执行,根本上是对方法的调用和执行。

 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表(最小单位为变量槽VariableSlot)、操作数栈、动态连接和方法返回地址等信息,每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。栈帧的内容在编译时就已经完成确定,不受程序运行期变量数据的影响,仅取决于具体的虚拟机实现。
发布了18 篇原创文章 · 获赞 1 · 访问量 2万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章