• Java虚拟机--JVM


    Java源代码是怎么被机器识别并执行的呢?答案是Java虚拟机。

    一、字节码

    0和1是计算机仅能识别的信号,经过0和1的不同组合产生了数字之上的操作。另外通过不同的组合亦产生了各种字符。同样可以通过不同的组合产生不同的机器指令。

    机器码是离CPU指令集最近的编码,是CPU可以直接解读的指令,因此机器码肯定是与底层硬件系统耦合的。

    在代码执行的过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖;JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态地编译为机器码,提高执行效率。

    字节码主要指令如下:

    (1)加载和存储指令

    在某个栈帧中,通过指令操作数据在虚拟机栈的局部变量表与操作栈之间来回传输,常见指令如下:

    • 将局部变量加载到操作栈中。如ILOAD(将int类型的局部变量压入栈和ALOAD(将对象引用的局部变量压入栈)等。
    • 从操作栈顶存储到局部变量表。如ISTORE、ASTORE等。
    • 将常量加载到操作栈顶,这是极为高频使用的指令。如 ICONST、BIPUSH、SIPUSH、LDC等。

    (2)运算指令

    对两个操作栈帧上的值进行运算,并把结果写入操作栈顶,如IADD、IMUL等。

    (3) 类型转换指令

    显示转换两种不同的数值类型。如I2L、D2F等。

    (4)对象创建与访问指令

    根据类进行对象的创建、初始化、方法调用相关指令,常见指令如下:

    • 创建对象指令
    • 访问属性指令
    • 检查实例类型指令

    (5)操作栈管理指令

    JVM提供了直接控制操作栈的指令,常见指令如下:

    • 出栈操作。如POP即一个元素,POP2即两个元素。
    • 复制栈顶元素并压入栈。如DUP。

    (6)方法调用与返回指令

    • INVOKEVIRTUAL 指令: 调用对象的实例方法
    • INVOKESPECIAL 指令: 调用实例初始化方法、私有方法、父类方法等
    • INVOKESTATIC 指令: 调用类静态方法
    • RETURN 指令:返回VOID类型

    (7)同步指令

    JVM 使用方法结构中的ACC SYN HRONIZ 标志同步方法 指令集中有MONITORENTER MONJTOREXIT 支持 synchroni ze 语义。

    我们编写好的.java文件是源代码文件,并不能交给机器直接执行,需要将其编译称为字节码甚至是机器码文件。静态编译器将源码转字节码流程:

          词法解析是通过空格分隔出单词,操作符,控制符等信息,将其形成token信息流,传递给语法解析器;在语法解析时,把词法解析得到的token信息流按照JAVA语法规则组成以可语法树,在语义分析阶段,需要检查关键字的使用是否合理、类型是否匹配、作用域是否正确等;当语义分析完成之后,即可生成字节码。

    字节码必须通过类加载过程加载到JVM环境后,才可以运行。字节码必须通过类加载过程加载到JVM 环境后,才可以执行。执行有三种模式第一,解释执行,第二, JIT 编译执行第三, JIT 编译与解释混合执行(主流 JVM默认执行模式)。混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进 JVM通过热点代码统计分析 识另 高频的方法调用、循环体、公共模块等,基于强大的JlT 动态编译技术,将热点代码转换成机器码,直接交给 PU执行。 JIT 作用是将Java 字节码动态地编译成可以直接发送给处理器指令执行的机器码。简要流程如图 所示。

    二、类加载过程

    任何程序都需要加载到内存才能与CPU进行交流。字节码.class文件同样需要加载到内存中,才可以实例化类。ClassLoader的使命就是提前加载.classl类文件到内存中。在加载类时,使用的是“Parent Delegation Model” 译为双亲委派模型。

    Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的Load、Link、Init,即加载、链接、初始化。

    • 第一步,Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
    • 第二步,Link 阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。
    • 第三步,Init 阶段执行类构造器<clinit>方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

                                                                                    类加载过程图

           类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。在这个过程中,JVM会初始化继承树上还没有被初始化的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。

          类加载器类似于原始部落结构,存在权力等级制度。

    (1)最高的一层是家族中威望最高的Bootstrap,它是在JVM启动时创建的,通常由与操作系统相关的本地代码实现,是最根本的类加载器,负责装载最核心的Java类,比如Object、System、String等。

    (2)第二层是在JDK9版本中,称为Platform ClassLoader,即平台类加载器,用以加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等,JDK9之前的加载器是Extension ClassLoader。

    (3)第三层是Application ClassLoader的应用类加载器,主要是加载用户定义的CLASSPATH路径下的类。

    第二、第三类加载器为Java语言实现,用户也可以自定义类加载器。

    什么情况下需要自定义类加载器?

    (1)隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如,***某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。

    (2)修改类加载方式。类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。

    (3)扩展加载源。比如从数据库、网络,甚至是电视机机顶盒进行加载。

    (4)防止源码泄露。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。

    三、内存布局

            内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。

    -------------------------------------------------------------------------------------------------------------------------------------------

                                                                                     JVM内存布局图

    Heap(堆区)

    Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用。通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗所有的空间。堆的内存空间既可以固定大小,也可以在运行时动态地调整,可以通过相应的参数进行设置。通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成一样大小,避免在GC后调整堆大小时带来的额外压力。

    JVM Stack(虚拟机栈)

    栈(Stack)是一个先进后出的数据结构,就像子弹的弹夹,最后压入的子弹先发射,压在底部的子弹最后发射,撞针只能访问位于顶部的那一颗子弹。

    相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境。栈结构移植性更好,可控性更强。JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。而StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。JVM能横扫千军,虚拟机栈就是它的心腹大将,当前方法的栈帧,都是正在战斗的战场,其中的操作栈是参与战斗的士兵。

    虚拟机栈通过压栈与出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等。

    Native Method Stack(本地方法栈)

    本地方法栈(Native Method Stack)在JVM内存布局中,也是线程对象私有的,但是虚拟机栈“主内”,而本地方法栈“主外”。

    Program Counter Register (程序计数寄存器)

    在程序计数寄存器()中,Register的命名源于CPU的寄存器,CPU只有把数据装载到寄存器才能够运行。寄存器存储指令相关的现场信息,由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

    这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等。线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。

    最后,从线程共享的角度来看,堆和元空间是所有线程共享的,而虚拟机栈、本地方法栈、程序计数器是线程内部私有的,从这个角度看一下Java内存结构。

    四、垃圾回收 (Garbage Collection, GC)

    Java会对内存进行自动分配与回收管理,使上层业务更加安全,方便地使用内存实现程序逻辑。在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的。

    垃圾回收的主要目的是清楚不再使用的对象,自动释放内存。

    。。。。。。。。

  • 相关阅读:
    STL中的map
    HDU 4027 Can you answer these queries?
    HDU 2199 Can you solve this equation?
    USACO section1.2 Name That Number 命名那个数字
    HDU 3790 最短路径问题 (双重权值)
    [笔记]CiscoPT配置RIP
    [笔记]Cisco PT VLANTrunk配置
    iptables感悟Ubuntu
    CentOS网络配置
    Discuz X2 数据库备份功能分析
  • 原文地址:https://www.cnblogs.com/nastu/p/15180918.html
Copyright © 2020-2023  润新知