JVM相关知识

什么是JVM

JVM是Java Virtual Machine缩写,也就是java虚拟机,了解JVM之前需要先了解下JDK和JRE这两个东西。jdk是Java Development Kit缩写,即java语言软件的开发套件,它是真实存在的,是java开发程序的工具、运行时环境以及JVM的集合;jre是Java Runtime Environment缩写,即java运行时环境,它也是真实存在的,包含了JVM和java api,提供java程序运行需要的环境;而JVM并不是物理存在的,它是一种软件实现,用来执行编译后生成的java字节码文件,即运行java程序。

JVM用来做什么

  • 执行java代码:执行编译后的class代码。

  • 内存管理:包括内存的分配回收和状况分析。

  • 线程资源的同步和交互。

JVM的生命周期

  • 启动:拥有main方法的的class作为JVM运行实例的起点。

  • 运行:main方法为起点,程序中的其他线程均由它启动,包括daemon守护线程和non-daemon普通线程。daemon是JVM自己使用的线程,比如GC线程,main方法的初始线程是non-daemon。

  • 销毁:所有线程终止运行时,JVM实例生命结束。

JVM整体结构

JVM整体结构

ClassLoader:

类加载器是JVM中负责加载程序中的类。jdk默认提供了三种类加载器。

  • BootStrp ClassLoader:根类加载器,它负责加载虚拟机的核心类库,如Java.lang.*等,它使用C++编写,它的实现依赖于底层操作系统,它并没有继承java.lang.ClassLoader类。

  • Extension ClassLoader:它的父加载器为根类加载器,它从jre\lib\ext子目录下加载类库,它使用Java代码实现,是java.lang.ClassLoader类的子类。

  • Application ClassLoader: 它的父加载器为扩展类加载器,它从环境变量classpath中加载类,它是用户自定义的类加载器的默认父加载器。它使用java实现,是java.lang.ClassLoader类的子类。

其中BootStrp ClassLoader是JVM启动后初始化的,然后它负责加载Extension ClassLoader,并将其父加载器设置为BootStrp ClassLoader,这之后BootStrp ClassLoader就开始加载Application ClassLoader,并将Application ClassLoader的父加载器设置为Extension ClassLoader。

注意:父子加载器并非继承关系,也就是说子加载器不一定是继承了父加载器,他们其实是一种包装关系。

父类委托机制

采用父类委托机制加载类的时候采用如下的几个步骤:

1、当前ClassLoader首先从自己已经加载的类(缓存)中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。

2、当前ClassLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到BootStrp ClassLoader。

3、当所有的父类加载器都没有加载的时候,再由当前的类加载器去加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

4、如果所有父类加载器和当前类加载器都不能加载,则抛出异常ClassNotFountException。

为什么要用父类委托机制

每个类装载器都有一个自己的命名空间用来保存已加载的类,命名空间由该加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。当一个类加载器加载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。由同一类加载器加载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能相互访问包可见的类和成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。父委托机制的优点是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。

类加载器的可见性机制

子类加载器可以查找父加载器中的类,但是一个父加载器不能查找子加载器里的类。

类加载器的单一性机制

如果父类加载器已经加载了这个类,这个类会被直接使用,子类加载器不会加载第二次,所有父类加载器都没有加载过,当前类加载器才会请求加载这个类。

Java提供了动态加载特性;他会在运行时的第一次引用到一个class的时候对它进行加载(Loading)、链接(Linking)和初始化(Initialization),而不是在编译时进行。

加载

加载过程虚拟机会做:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

相比与加载过程的其他几个阶段,加载阶段可控性最强,因为类的加载器可以用系统的,也可以用自己写的,开发者可以用自己的方式写加载器来控制字节流的获取。获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在java堆中实例化一个java.lang.Class对象与堆中的数据关联起来。

链接

链接包含三步:验证,准备,解析。

验证的目的:确保class文件的字节流信息符合jvm的口味,不会让jvm感到不舒服。假如class文件是由纯粹的java代码编译过来的,自然不会出现类似于数组越界、跳转到不存在的代码块等不健康的问题,因为一旦出现这种现象,编译器就会拒绝编译了。但是,跟之前说的一样,Class文件流不一定是从java源码编译过来的,也可能是从网络或者其他地方过来的,甚至你可以自己用16进制写,假如jvm不对这些数据进行校验的话,可能一些有害的字节流会让jvm完全崩溃。

验证主要经历几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证

  • 文件格式验证:验证字节流是否符合Class文件格式的规范并 验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流就可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。

  • 元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。

  • 字节码检验:最复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。

  • 符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)

验证是类加载中最复杂的过程,并且花费的时间也是最长的。任务是确保导入类型的准确性,验证阶段做的检查,运行时不需要再做,虽然减慢加载速度,但是避免了多次检查。

接着就上面步骤完成后,就会进入准备阶段了:

这阶段会为类变量(指那些静态变量)分配内存并设置内存初始值的阶段,这些内存在方法区中进行分配。这里要说明一下,这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。这里的给类变量设初始值跟类变量的赋值有点不同,比如下面:

public static int value = 123;

在这一阶段,value的值将会是0,而不是123,因为这个时候还没开始执行任何java代码,123还是不可见的,而我们所看到的把123赋值给value的putstatic指令是程序被编译后存在于<clinit>(),所以,给value赋值为123是在初始化的时候才会执行的。

这里也有个例外:

public static final int value = 123;

这里在准备阶段value的值就会初始化为123了。这个是说,在编译期,javac会为这个特殊的value生成一个ConstantValue属性,并在准备阶段jm就会根据这个ConstantValue的值来为value赋值了。

解析是把这个类的常量池中的所有的符号引用改变成直接引用。如果不执行,符号解析要等到字节码指令使用这个引用时才会进行。

初始化

把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。

运行时数据区

运行时数据区结构如图:

运行时数据区结构

由上面的总体结构图可以看到,Runtime Data Area包括五大块:Method Area、JVM Stack、Native Method Area、Heap、Program Counter Register。

PC寄存器

也就是Program Counter Register,也叫程序计数器,是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。每一个JVM线程都有自己的PC寄存器,在任意时刻,一个JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method),如果该方法是 java 方法,那PC寄存器保存 JVM 正在执行的字节码指令的地址,如果该方法是native,那PC寄存器的值是undefined。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

JVM Stack

与PC寄存器一样,JVM栈也是线程私有的。每一个JVM线程都有自己的java虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同,用来保存栈帧。JVM 只会在JVM栈上进行push和pop的操作。JVM栈可以被实现成固定大小,也可以根据计算动态扩展。如果采用固定大小的JVM Stack设计,那么每一条线程的JVM Stack容量应该在线程创建时独立地选定。JVM实现应该提供调节JVM Stack初始容量的手段。如果采用动态扩展和收缩的JVM Stack方式,应该提供调节最大、最小容量的手段。

  • 栈帧随着方法调用而创建,随着方法结束而销毁—无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在JVM Stack之中,每一个栈帧都有自己的局部变量表(Local Variables)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用。

    • 局部变量数组

    每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定。 局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。 JVM使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从 0 开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第 0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的“this”关键字)。后续的其他参数将会传递至从 1 开始的连续的局部变量表位置上。

    • 操作数栈

    每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出LIFO栈。栈帧中操作数栈的长度由编译期决定。 操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。JVM提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

    • 动态链接

    每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。 C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。 当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。

    • 方法正常调用完成

    在这种场景下,当前栈帧承担着回复调用者状态的责任,其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等。使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常地执行。

    • 方法异常调用完成

    方法异常调用完成是指在方法的执行过程中,某些指令导致了 Java 虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者在执行过程中遇到了 athrow 字节码指令显式地抛出异常,并且在该方法内部没有把异常捕获住。如果方法异常调用完成,那一定不会有方法返回值返回给它的调用者。

本地方法栈

Native Method Area缩写,Java虚拟机可能会使用到传统的栈来支持native方法(使用Java语言以外的其它语言编写的方法,如C/C++)的执行,这个栈就是本地方法栈(Native Method Stack)。如果JVM不支持native方法,也不依赖与传统方法栈的话,可以无需支持本地方法栈。如果支持本地方法栈,则这个栈一般会在线程创建的时候按线程分配,即线程私有。

方法区

在Java虚拟机中,被加载类的信息都保存在方法区中。包括类型信息(Type Information)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。如果两个线程都去加载同一个类时,那只能由一个线程被容许去加载这个类,另一个必须等待。方法区是在JVM启动的时候创建的。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。

方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。是否对方法区进行垃圾回收对JVM的实现是可选的。

运行时常量池

Runtime constant pool缩写,运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM通过运行时常量区来查找方法或者变量在内存里的实际地址。运行时常量池是方法区的一部分。每一个运行时常量池都分配在JVM的方法区中,在类和接口被加载到JVM后,对应的运行时常量池就被创建。

在JVM中,堆(heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。堆在JVM启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显式地被销毁。Java堆的容量可以是固定大小,也可以随着需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。JVM实现应当提供给程序员调节Java 堆初始容量的手段,对于可动态扩展和收缩的堆来说,则应当提供调节其最大和最小容量的手段。

执行引擎

Execution Engine,通过类加载器加载并被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取 Java 字节码。它就像一个 CPU 一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。不过 Java 字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被 JVM 执行的语言。字节码可以通过以下两种方式转换成合适的语言。

  • 解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。

  • 即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

转载于:https://my.oschina.net/shenhuniurou/blog/847188

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值