JVM条理混乱?看完这篇你就理清楚了(言简意赅版)

文章内容概要:
在这里插入图片描述

一、什么是JVM

     JVM是Java Virtual Machine(Java虚拟机)的缩写,它是整个Java实现跨平台的最核心的部分
    所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
    JVM是Java平台的基础,和实际的机器一样,它也有自己的指令集,并且在运行时操作不同的内存区域。 JVM通过抽象操作系统和CPU结构,提供了一种与平台无关的代码执行方法,即与特殊的实现方法、主机硬件、主机操作系统无关。JVM的主要工作是解释自己的指令集(即字节码)到CPU的指令集或对应的系统调用,保护用户免被恶意程序骚扰。 JVM对上层的Java源文件是不关心的,它关注的只是由源文件生成的类文件(.class文件)。

几个重要概念
1、Java虚拟机的生命周期:声明周期起点是当一个java应用main函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才结束生命。

2、Java虚拟机与main方法的关系:main函数就是一个java应用的入口,main函数被执行时,java虚拟机就启动了。启动了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机。

3、JVM结构图各模块
在这里插入图片描述

二、JDK,JRE,JVM的关系

    JDK(Java Development Kit)是Java 语言的软件开发工具包(SDK)。在JDK的安装目录下有一个jre目录,
    JRE里面有两个文件夹bin和lib
    在这里可以认为bin里的就是JVMlib中则是JVM工作所需要的类库,而jvm和 lib合起来就称为jre。
    jdk,jre,JVM的关系图

在这里插入图片描述

三、JVM结构

在这里插入图片描述
我们总结出JVM内存包含两个子系统两个组件
两个子系统是:
Classloader子系统Executionengine(执行引擎)子系统
两个组件分别是:
Runtimedataarea(运行时数据区域)组件Nativeinterface(本地库接口)组件

(一)两大子系统

1、Classloader子系统
(1)作用
    类加载指的是将类的class文件读入到内存,并为之创建一个对应java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会之建立一个java.lang.Class对象(联合“反射”理解)。
(2)类加载器
     类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。
    jvm虚拟机位于操作系统的堆中,并且,程序员写好的类加载到虚拟机执行的过程是:当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class文件装载到jvm的方法区,方法区中的这个字节文件是用以被虚拟机拿来new A对象,并在堆内存生成了一个A.class对象(联合“反射”理解),然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader

在这里插入图片描述

(3)双亲委派机制
双亲委派机制:JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

采用双亲委派模式的是好处是Java类随着它的类加载器可以具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

在这里插入图片描述

例如:当jvm要加载Test.class的时候,
  (1)首先会到自定义加载器中查找(其实是看运行时数据区的方法区有没有加载),看是否已经加载过,如果已经加载过,则返回字节码。
  (2)如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。
  (3)如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。
  (4)如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。
  (5)依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException。

2、执行引擎(Executionengine子系统)

(1)概念
    负责执行来自类加载器子系统(class loader subsystem)中被加载类中在方法区包含的指令集(指令集存在于.class文件),通俗讲就是类加载器子系统把代码逻辑(什么时候该if,什么时候该相加,相减)都以指令的形式加载到了方法区,执行引擎就负责执行这些指令就行了。
用网上最流行的一张图表示就是:
在这里插入图片描述
(2)代码转化
    但是执行引擎拿到的方法区中的指令还是人能够看懂的,这里执行引擎的工作就是要把指令转成JVM执行的语言(也可以理解成操作系统的语言),最后操作系统语言再转成计算机机器码。

在这里插入图片描述
(二)两大组件
在这里插入图片描述
两大组件分别为Nativeinterface(本地库接口)组件Runtimedataarea(运行时数据区域)组件
从图中可以看出Runtimedataarea(运行时数据区域)组件包含5部分:
方法区,堆,虚拟机栈,本地方法栈,程序计数器

1、Nativeinterface(本地库接口)组件

  • 本地方法库接口:即操作系统所使用的编程语言的方法集,是归属于操作系统的。
  • 本地方法库:保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的。

2、Runtimedataarea(运行时数据区域)组件
从图中可以看出运行时数据区域包含5部分:
方法区,堆,虚拟机栈,本地方法栈,程序计数器

虚拟机栈、本地方法栈、程序计数器这三个模块是线程私有的,有多少线程就有多少个这三个模块,声明周期跟所属线程的声明周期一致。

(1)程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

(2)Java虚拟机栈(Java Virtual Machine Stack)
    ①Java虚拟机栈是线程私有的,它的生命周期与线程相同,每个线程都有一个。
    ②每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的为当前线程中局部变量,操作数帧,动态链接,方法出口与其他内容
    ③每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
    ④栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进…F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
    ⑤JAVA虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令,如下图:

在这里插入图片描述

  • 局部变量表
        存储方法里的局部变量,基础类型和引用类型。
    虚拟机通过索引定位的方式使用局部变量表。JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double;和引用变量
  • 操作数栈
        操作数栈是代码执行时存放操作数的栈,例如加法操作,先将两个参数从栈顶取出,计算得到结果以后,再将结果压入栈。操作数栈和局部变量表会有一部分重叠
  • 动态链接
        Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数,每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(运行时链接)。
        描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接调用
        与之对应:静态链接:当一个字节码文件被装进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变。这种情况下调用方法的符号引用转换
  • 方法返回地址
        方法的退出有两种方式,正常执行结束退出和异常退出。异常退出一般根据异常处理表来确定返回地址,栈帧中不会存储这一部分信息。方法的退出实际上就是将栈帧从栈里弹出,恢复上一层栈帧的本地变量表和操作数栈,并且将返回值压入上一栈帧的操作数栈。

(3)本地方法栈(Native Method Stack)
    作用同java虚拟机栈类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。是线程私有的,它的生命周期与线程相同,每个线程都有一个。
本地(native)方法讲解:
     ①本地方法就是带有native标识符修饰的方法
     ②有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节
     ③存在的意义:不方便用java语言写的代码,使用更为专业的语言写更合适;甚至有些JVM的实现就是用c编写的,所以只能使用c来写,

(4)Java 堆(Java Heap)
  ①在虚拟机启动的时候创建(虚拟机在main方法执行时创建)。
  ②是Java虚拟机所管理的内存中最大的一块。
  ③不同于上面3个,堆是jvm所有线程共享的
  ④唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
  ⑤Java堆是垃圾收集器管理的主要区域
  ⑥因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间
  ⑦java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)
  ⑧如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

(5)方法区(Method Area)
  ①在虚拟机启动的时候创建(虚拟机在main方法执行时创建)。
  ②所有jvm线程共享
  ③除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,可以选择不实现垃圾收集
  ④用于存放已被虚拟机加载的类信息、常量池、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据
  ⑤运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  ⑥方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”

四、GC垃圾回收机制

(一)了解堆内存
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分
在这里插入图片描述
(二)新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace)
伊甸区:所有的类都是在伊甸区被new出来的
幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)
执行过程:当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC,轻GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收(Minor GC,轻GC),然后移动到1区。那如果1去也满了呢?再移动到养老区。

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

  • Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

(三) 养老区
  养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。
  若养老区也满了,那么这个时候将产生FullGC(或MajorGC,重GC),进行养老区的内存清理。若养老区执行FullGC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

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

   如果出现java.lang.OutOfMemoryError: PermGen space(此异常存在于jdk1.6及之前),说明是Java虚拟机对永久代Perm内存设置不够。原因有二:

  • 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
  • 大量动态反射生成的类不断被加载,最终导致Perm区被占满。

(五)永久代变更历程
(1)JDK1.6及之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
(2)JDK1.7中,存储在永久代的部分数据就已经转移到Java Heap堆或者Native memory本地内存,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings),类的静态变量(class statics)转移到了Java heap。但永久代仍存在于JDK 1.7中,并没有完全移除,
(3)JDK8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,逻辑上可认为在堆中(JDK 1.8中参数PermSize和MaxPermSize已经失效,(java.lang.OutOfMemoryError: PermGen space(PermGen space意为永久代)这种错误将不会出现在JDK1.8中)。

介绍两篇对JVM内存详细探究的帖子
JAVA 方法区与堆–java7前,java7,java8各不相同
Java8内存模型

五、垃圾回收算法

下面介绍几种垃圾回收的算法:
(一)引用计数法
    这个算法的思想很简单,既然我们说当一个对象A不再被引用时为垃圾对象,那就为对象A设置一个计数器,所以对于对象A,只要有任何一个对象引用了对象A,那么引用计数器就加一;而当这个引用失效了之后,引用计数器就减一。当一个对象的引用计数器的值为0的时候,那么这个对象就不可能再被使用了。
在这里插入图片描述
但是引用计数法有一个很麻烦的问题,就是它无法处理垃圾对象的循环引用,来看看下面这个图:
在这里插入图片描述
    由于这种算法存在上述缺陷,所以JVM其实并不使用它,而是使用另一种算法,是根搜索算法。这种算法是现代垃圾回收算法的思想基础。它的做法是:把一些对象设为根对象(也可以说是根结点),当任何一个根结点都不可达某一个对象的时候,这个对象就被认为是可以回收的。基于这种思想,上述引用计数法存在的问题就可以解决了,由于根对象无法到达那三个循环引用的对象,所以这三个对象都是视为可回收的垃圾。
那么什么对象可以作为根对象呢?

  • Java虚拟机栈中引用的对象;
  • 方法区中的类静态成员引用的对象(static修饰的);
  • 方法区中的常量引用的对象(主要是final修饰的);
  • 本地方法栈中JNI(Java Native Interface)引用的对象。

(二)标记-清除法
    标记-清除法将垃圾回收分为两个阶段,即标记阶段和清除阶段。做法是,首先在标记阶段,遍历所有的根结点,将这些根结点的可达对象进行标记;而在清除阶段,遍历堆中的所有对象,对那些没有被标记的对象进行清除。
我们来看看一个示意图:
1、标记-清除算法示意(标记阶段)
在这里插入图片描述

    上图中的整个表格是我们用来模拟堆内存的,其中表格中存储的是一系列的对象,我们从根结点出发,深蓝色的表格中的对象表示的是从根结点出发的可达的对象,而灰色表格中表示的是从根结点出发不可达的对象,而其余浅蓝色的表格表示的还是空闲的堆内存。上述进行的是标记阶段,由根结点出发可达的对象都进行了标记。

2、标记-清除算法示意(清除阶段)

标记阶段完成后,下面就是清除阶段,对没有进行标记的对象进行清除:
在这里插入图片描述
至此标记-清除算法就执行完毕。

(三)标记-压缩算法
    基于上一种算法的缺点,标记-压缩算法是对标记-清除算法的一种改良,它们的工作原理差不多,也是分为两个阶段:标记阶段和压缩阶段。标记阶段和标记-清除算法一样;而在压缩阶段的时候,它不是将所有的未标记的对象清除,而是将所有的标记对象压缩熬堆内存的一段,然后清除边界以 外的所有空间。

标记-压缩算法示意(标记阶段)在这里插入图片描述
标记完后,把对象进行压缩:
在这里插入图片描述
标记-压缩算法示意(压缩阶段):压缩对象

在这里插入图片描述
压缩完毕

清除边界以外的区域:
在这里插入图片描述

(四)复制算法
    复制算法的思想是,将堆内存分为两块大小完全相同的内存(不一定是全部堆内存空间),每一次只用一块(活跃空间),另一块(空闲空间)闲置不用。当其中的活跃空间使用完后,就将活跃空间中还存活的对象复制到空闲空间里去,按照地址整齐排好序。之后清除活跃空间中所有的对象。然后两者交换使用职位,现在空闲空间成为了活跃空间,活跃空间成为了空闲空间。
首先将存活对象复制到空闲空间

在这里插入图片描述
复制算法(复制存活对象)
在这里插入图片描述
转换两个空间的角色,以及清空原来的活跃空间:
在这里插入图片描述
至此,复制算法执行完毕。

(五)总结
1、标记-清除算法
    首先是速度慢,因为标记-清除算法在标记阶段需要使用递归的方式从根结点出发,不断寻找可达的对象;而在清除阶段又需要遍历堆中的所有对象,查看其是否被标记,然后清除。
    其次是其最大的缺点,使用这种算法进行清理而得的堆内存的空闲空间一般是不连续的,我们知道对象实例在堆中是随机存储的,所以在清理之后,会产生许多的内存碎片。
2、标记-压缩算法
    首先这种算法克服了标记-清除算法中会产生内存碎片的缺点,也解决了复制算法中内存减半使用的不足。
而其缺点则是速度也不是很快,不仅要遍历标记所有可达结点,还要一个个整理可达存活对象的地址,所以导致其效率不是很高。
3、复制算法
    复制算法很明显的缺点就是浪费内存空间,因为将内存分为两块,一次只能使用一块,这也意味着分的块越大,浪费的内存越多。而且当对象的存活率很高的时候,不断的复制操作也会显得费时和不可忽视。
    但是也是托了浪费内存的福,复制算法执行的速度较快,典型的空间换时间。

六、JVM运行实例

(一)各模块执行流程
运行时数据区各个模块协作工作的总结较好的图,首先要执行的代码是:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

(二)代码编译流程
Java代码编译(Java Compiler)过程,也就是由.java文件到.class文件的过程
在这里插入图片描述

注:源代码就是.java文件,JVM字节码就是.class文件
1、是先加载字节码文件还是先执行main方法:先加载字节码文件到方法区,然后在找到main执行程序。
2、java被编译成了class文件,JVM怎么从硬盘上找到这个文件并装载到JVM里呢?
答:是通过java本地接口(JNI),找到class文件后并装载进JVM,然后找到main方法,最后执行。

参考:
[1] 【狂神说Java】JVM快速入门篇
[2] Java的GC算法种类
[3] JVM原理最全、清晰、通俗讲解,五天40小时吐血整理

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读