JVM 工做原理

    JVM对Java程序来讲就至关于操做系统,JVM包括一套指令、一组寄存器、栈、堆以及垃圾回收等,全部的Java程序都运行在JVM中,经过JVM能够实现各类计算机上的功能,JVM的好处在于它屏蔽了底层实现方式,Java程序能够运行在任何平台上,不须要写多份代码。

1. JVM体系结构
java

    首先,Java程序通过Compiler编译生成Class文件,启动程序后就产生JVM实例,类加载器负责将Class字节码加载到JVM中,将类的静态信息、方法信息等加载到方法区中,对象分配在Java对上,程序执行过程和局部变量则存放到Java栈中,而后执行引擎基于栈执行程序,寄存器记录程序执行的位置,程序执行的同时垃圾回收器回收不被引用的对象。

2. JVM生命周期数组

  1. 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class均可以做为JVM实例运行的起点;
  2. 运行。main()做为该程序初始线程的起点,任何其余线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程一般由JVM本身使用,java程序也能够标明本身建立的线程是守护线程;
  3. 消亡。当程序中的全部非守护线程都终止时,JVM才退出;若安全管理器容许,程序也可使用Runtime类或者System.exit()来退出。


3. 类加载器:
    类加载器的做用就是将Class文件加载到JVM中。
    ClassLoader两种装载Class的方式:缓存

  • 隐式:运行过程当中,碰到new方式生成对象时,隐式调用ClassLoader到JVM
  • 显式:经过Class.forName()动态加载

    类加载器结构:


    类的加载过程采用双亲委派模型(Parent Delegation Model),这种机制能更好的保证 Java 平台的安全。该模型要求除了顶层的Bootstrap class loader启动类加载器外,其他的类加载器都应当有本身的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是经过组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工做过程为:安全

  • 当前ClassLoader首先从本身已经加载的类中查询是否此类已经加载,若是已经加载则直接返回原来已经加载的类。每一个类加载器都有本身的加载缓存,当一个类被加载了之后就会放入缓存,等下次加载的时候就能够直接返回了。
  • 当前ClassLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用一样的策略,首先查看本身的缓存,而后委托父类的父类去加载,一直到Bootstrap ClassLoader。
  • 当全部的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它本身的缓存中,以便下次有加载请求的时候直接返回。

    使用这种模型来组织类加载器之间的关系主要是为了安全性,避免用户本身编写的类动态替换Java的一些核心类,好比String,同时也避免了重复加载,由于JVM中区分不一样类,不只仅是根据类名,相同的Class文件被不一样的ClassLoader加载就是不一样的两个类,若是相互转型的话会抛java.lang.ClassCaseException。

4. 运行时数据:
     Class文件加载到JVM后就会产生如下几种数据:数据结构

  • PC寄存器
    PC寄存器是用于存储每一个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。
     
  • Java栈
    Java栈是线程私有的,每一个线程建立的同时都会建立Java栈,Java栈中存放的为当前线程栈帧(Stack Frame),栈帧适用于支持虚拟机对方法调用和方法执行的数据结构,栈帧随着方法的调用而建立,随着方法的结束而销毁,栈帧中包括局部变量表(八中基本类型和非基本类型引用),操做数栈,动态连接,返回地址和额外信息。
     
  • Java堆
    它是用来存储对象实例以及数组值的区域,能够认为Java中全部经过new建立的对象的内存都在此分配,Heap中的对象的内存须要等待GC进行回收。
    堆是JVM中全部线程共享的,所以在其上进行对象内存的分配均须要进行加锁,这也致使了new对象的开销是比较大的;
    Sun Hotspot JVM为了提高对象内存分配的效率,对于所建立的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的状况计算而得,在TLAB上分配对象时不须要加锁,所以JVM在给线程的对象分配内存时会尽可能的在TLAB上分配,在这种状况下JVM中分配对象内存的性能和C基本是同样高效的,但若是对象过大的话则仍然是直接使用堆空间分配,TLAB仅做用于新生代的Eden Space。
     
  • 方法区域:
    在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中经过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在必定的条件下它也会被GC,当方法区域须要使用的内存超过其容许的大小时,会抛出OutOfMemory的错误信息。
     
  • 运行时常量池:
    存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
     
  • 本地方法栈:
    JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每一个native方法调用的状态。


5. 执行引擎:
    执行引擎是JVM的核心部分,ClassLoader将Class加载到JVM后,执行引擎的做用就是解析JVM字节码指令,获得执行结果。Sun HotSpot是基于栈的执行引擎,执行的过程就是栈帧压栈和出栈的过程,计数器则用于记录指令执行到的位置。
并发

  • 局部变量表
    一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中肯定了改方法所需分配的最大局部变量表的容器。 包含类型:boolean、byte、char、short、int、float、reference或returnAddress类型八种类型。
     
  • 操做栈
    一个后入先出栈。同局部变量表同样,操做数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中,操做数栈的深度都不会超过在max_stacks数据项中设定的最大值。当一个方法刚刚开始执行的时候,这个方法的操做数栈是空的,在方法的执行过程当中,会有各类字节码指令向操做数栈中写入和提取内容,也就是入栈出栈操做。 操做数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。
  • 动态连接
    每一个栈帧都包含着一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用的是为了支持方法调用过程当中的动态链接。 在Class文件中存在着大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分在类加载阶段第一次使用阶段的时候转换为直接引用,这种转换称为静态解析。另一部分将在每次的运行期间转化为直接引用,这部分称为动态转换。
     
  • 返回地址
    当一个方法被执行后,有两种方式能够退出这个方法:
    a) 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
    b) 另一种退出方式是:在方法执行过程当中遇到异常,而且这个异常没有在方法体内获得处理,不管是JVM内部产生的异常,仍是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出。这种方式被称为异常退出出口。此方式不会给上层调用者产生任何返回值。

    不管采用哪种退出方式,在方法退出后,都会返回到方法被调用的位置,程序才能继续执行。方法返回时可能要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。通常来讲,方法正常退出以后,调用者的PC计数器的值就能够做为返回地址。栈帧中极可能会保存这个计数器值,而方法异常退出后,返回地址就要经过异常处理器表来肯定,栈帧通常不保存这部分信息。
     
  • 方法调用
    a) 解析调用时是静态的过程,在编译期间就彻底肯定目标方法。
    在Class文件中,全部方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就可以肯定惟一的目标方法,这类方法的调用成为解析调用。此类方法主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,所以决定了他们都不可能经过继承或者别的方式重写该方法,符合这两类的方法主要有如下几种:静态方法、私有方法、实例构造器、父类方法。虚拟机中提供了如下几条方法调用指令:
    invokestatic:调用静态方法,解析阶段肯定惟一方法版本
    invokespecial:调用<init>方法、私有及父类方法,解析阶段肯定惟一方法版本
    invokevirtual:调用全部虚方法
    invokeinterface:调用接口方法
    invokedynamic:动态解析出须要调用的方法,而后执行

    b) 分派调用则便可能是静态,也多是动态的,根据分派标准能够分为单分派和多分派。两两组合有造成了静态单分派、静态多分派、动态单分派、动态多分派。分派调用更多的体如今多态上。
    静态分派:全部依赖静态类型来定位方法执行版本的分派成为静态分派,发生在编译阶段,典型应用是方法重载。
    动态分派:在运行期间根据实际类型来肯定方法执行版本的分派成为动态分派,发生在程序运行期间,典型的应用是方法的重写。
    单分派:根据一个参数对目标方法进行选择。
    多分派:根据多于一个参数对目标方法进行选择。


    执行引擎工做过程:函数

public class Main {
    public static void main(String[] args) {
        int a = 0;
        int b = 1;
        int c = a + b;
    }
}


public class piggy.Main {
  public piggy.Main();
    Code:
       0: aload_0      
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return       

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0      // 常量0入栈
       1: istore_1      // 变量a复制0加入变量表
       2: iconst_1      // 常量1入栈
       3: istore_2      // 变量b复制1加入变量表
       4: iload_1       // 变量a入栈
       5: iload_2       // 变量b入栈
       6: iadd          // 栈顶a,b相加
       7: istore_3      // 将a,b和复制c加入变量表 
       8: return       
​​​​​​​}


6. 垃圾回收:
    Class被ClassLoader加载到JVM后会产生各类数据,有些数据通过一段时间后就再也不会被使用,GC将内存中再也不被使用的对象进行回收,GC中用于回收的方法称为收集器,因为GC须要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽量的缩短GC对应用形成的暂停。
垃圾回收按照如下三个方面进行分类:性能

  • 回收策略
    a) 引用计数
    b) 标记清除
    c) 复制
    d) 标记整理
     
  • 分区方法
    a) 增量收集
    b) 分代收集
     
  • 系统线程
    a) 串行收集
    b) 并行收集
    c) 并发收集

     JVM堆内存采用分代方法存储对象,分为年轻带、年老代和永久代,不一样代的回收方式也不相同,年轻代通常采用并行方式收集称为Young GC,年老代采用并发的方式收集称为Full GC,其中主要耗时的是Full GC,由于Full GC会暂停全部线程,因此要经过对JVM进行一些设置,从而减小Full GC的频率和事件。操作系统

相关文章
相关标签/搜索