zoukankan      html  css  js  c++  java
  • JAVA JVM

    JVM

    JVM是可运行Java代码的虚拟计算机 ,包括一套字节码指令集、一组寄存器、栈、堆、存储方法域和垃圾回收

    JVM是运行在操作系统之上的,它与硬件没有直接的交互。

    运行过程:

    ① Java源文件—->编译器—->字节码文件

    ② 字节码文件—->JVM—->机器码

    每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了 。

    当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。

    程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

    JVM生命周期:

    JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。

    JVM中的线程分为两种:守护线程普通线程

    守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。

    普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。

    权限足够的话,可以调用exit()方法终止程序。

    一、字节码校验器(class文件校验器)

    在字节码被类加载器执行之前对文件进行校验,保证class文件内容有正确的内部结构。

    class文件校验器分成四部分:

    1.文件格式验证: 校验class文件的结构的合法性

    2.元数据验证: 扫描发生在方法区中,主要对于,语义,词法和语法的分析,也就是检查这个类是否能够顺利的编译

    3.字节码验证: 字节码的校验过程校验的就是字节码流的合法过程,也就是校验操作数+操作码的合法性(字节码流=操作码+操作数)。

    4.符号引用验证: 解析符号引用和直接引用时进行的,这次校验确认被引用的类,字段以及方法确实存在

    二、类加载子系统

    Class文件由Java编译器生成,我们创建的.Java文件在经过编译器后,会变成.Class的文件,然后被类加载系统加载后,在JVM上运行。

    类加载机制

    类的加载指的是将类的.class文件中的二进制数据读入到内存中,通常是创建一个字节数组读入.class文件,将其放在运行时数据区的方法区内,然后在创建Class对象,用来封装类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

    类加载过程

    JVM将类的加载分为3个步骤:1、装载(Load)2、链接(Link)3、初始化(Initialize)

    装载(Load)

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

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

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

    链接(Link)

    1、验证:确保被加载的类的正确性

    验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    验证阶段大致会完成4个阶段的检验动作:

    文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

    元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

    字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

    符号引用验证:确保解析动作能正确执行。

    2、准备为类的静态变量分配内存,并将其初始化为默认值

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段(内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中),这些内存都将在方法区中分配

    3、解析把类中的符号引用转换为直接引用 

    符号引用:就是一组符号来描述目标,可以是任何字面量。(对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7类符号引用)

    直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

    初始化(Initialize)

    初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化,

    1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。

    在Java中对类变量进行初始值设定有两种方式:

    ①声明类变量是指定初始值。

    ②使用静态代码块为类变量指定初始值。

    类加载器类型

    1、BootStrap ClassLoader :启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等。

    2、Extension ClassLoader:扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。

    3、App ClassLoader:系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。

    4、Custom ClassLoader:自定义类加载器,通过java.lang.ClassLoader的子类自定义加载class。

    三、JVM结构

    JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

    JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack)程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:

    方法区(线程共享)

    静态变量、常量、类信息、运行时常量池存在方法区中。

    方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。

    简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。

    访问方法区的信息必须确保线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许去加载这个类,另一个必须等待。

    栈(先进后出,线程私有)

    栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。

    Java栈中只保存基础数据类型变量自定义对象的引用注意只是对象的引用而不是对象本身,对象是保存在堆区中的

    堆(先进先出,线程共享)

    堆这块区域是JVM中最大的,应用的对象数据都是存在这个区域,这块区域也是线程共享的,也是 GC 主要的回收区。

    一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。

    堆内存分为三部分:

    新生区:

    新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1去也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

    如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。

    原因有二:

    a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

    b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

    养老区:

    养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。

    永久区:

    永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

    如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。

    原因有二:

    a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。

    b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满。

    本地方法栈(先进后出)

    本地方法栈是在程序调用或JVM调用本地方法接口(Native)时候启用,用于存储本地方法的局部变量表,本地方法的操作数栈等信息。

    栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。

    每一个线程都包含一个栈区,每个栈中的数据都是线程私有的,其他栈不能访问。

    程序计数器

    在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

    分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器

    程序计数器仅占很小的一块内存空间。

    当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。

    程序计数器这个内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError(内存不足错误)的区域。

    四、JVM执行引擎

    类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码

    在JVM规范中制定了虚拟机字节码执行引擎的概念模型。执行引擎必须把字节码转换成可以直接被JVM执行的语言。

    字节码可以通过以下两种方式转换成合适的语言:

    • 解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
    • 即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。用即时编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。

    五、垃圾回收算法

    1.标记-清除: 这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。 

    2.复制算法: 为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。 于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)

    3. 标记-整理 该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。 

    4.分代收集 现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

  • 相关阅读:
    解决ORA14098分区交换索引不匹配错误
    Oracle等待事件kfk:async disk IO
    Advanced Diagnostic using oradebug dumpvar
    shellscript 06 命令执行顺序
    Linux 备份策略
    shellscript 05 shell输入与输出
    Java video 02
    软件安装:原始码与 Tarball
    shellscript 07 正则表达式介绍
    shellscript 03 后台执行命令
  • 原文地址:https://www.cnblogs.com/willpan-z/p/9502873.html
Copyright © 2011-2022 走看看