JAVA JVM

JVM

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

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

运行过程:算法

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

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

每一种平台的解释器是不一样的,可是实现的虚拟机是相同的,这也就是Java为何可以跨平台的缘由了 。安全

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

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

JVM生命周期:函数

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

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

守护线程是JVM本身使用的线程,好比垃圾回收(GC)就是一个守护线程。

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

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

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

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

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

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

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

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

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

2、类加载子系统

Class文件由Java编译器生成,咱们建立的.Java文件在通过编译器后,会变成.Class的文件,而后被类加载系统加载后,在JVM上运行。

类加载机制

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

类加载过程

JVM将类的加载分为3个步骤:一、装载(Load)二、连接(Link)三、初始化(Initialize)

 

装载(Load)

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

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

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

连接(Link)

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

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

验证阶段大体会完成4个阶段的检验动做:

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

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

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

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

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

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

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

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

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

初始化(Initialize)

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

1)若是类存在直接的父类而且这个类尚未被初始化,那么就先初始化父类;2)若是类中存在初始化语句,就依次执行这些初始化语句。

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

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

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

类加载器类型

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

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

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

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

3、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(内存不足错误)的区域。

4、JVM执行引擎

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

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

字节码能够经过如下两种方式转换成合适的语言:

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

5、垃圾回收算法

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

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

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

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

相关文章
相关标签/搜索