带你认识JVM

目录

前言

一、Java虚拟机结构

1-1、整体结构

1-2、编译流程

1-3、类加载器

1-4、加载流程

二、虚拟机内存管理

三、垃圾回收

3-1、垃圾收集算法

3-2、垃圾回收算法

四、Dalvik VM与JVM的不同

五、Dalvik与ART的不同


前言

在这个专栏的上一篇介绍了class文件和dex文件解析相关的内容,如果有感兴趣的可以看一看:

文章地址:https://blog.youkuaiyun.com/JArchie520/article/details/83684686

这一篇接着来说说虚拟机,如果没有虚拟机,那么即使有class文件和dex文件也是毫无作用的。因为DVM和ART都是从JVM演变而来的,对JVM做了一些优化,使之更适合于移动端,所以这里重点来说一下Java虚拟机,因为把它搞清楚了,再了解一下DVM和ART相较于它的优势,那么对虚拟机这块的学习算是暂时OK了。

一、Java虚拟机结构

1-1、整体结构

先来看一张图(纯手工制作,画的有点丑将就着看吧),这张图就是JVM整体的组成部分,主要由以下几个模块来组成:

1、Class文件生成模块:这一部分在上一篇中已经详细的说过了,主要是通过JDK中的javac编译命令去生成class文件。

2、类加载器子系统:将class字节码加载到JVM虚拟机内存中,类加载器的核心就是ClassLoader,通常所说的类加载器就是指ClassLoader,classloader则是动态更新的核心,下一篇会介绍Android中的ClassLoader,这里暂时不说。

3、内存空间:Class字节码被ClassLoader加载到JVM对应的内存空间之后,JVM则把内存分为方法区、堆区、栈区和本地方法栈四个部分,这四个部分分别被用来存储class字节码不同的部分。

4、垃圾收集器:通常就是我们所说的GC

以上这四个部分是对开发者来说是比较重要的,如果理解了这四个部分那你对JVM就已经有了比较深入的了解了。其它的指令计数器、执行引擎、本地方法接口这些部分则是JVM底层用来与CPU打交道的,我们可以不用过多关心。

1-2、编译流程

从上面这张图中可以看出:由最开始的Java源代码经过词法分析器等一系列的分析最后通过字节码生成器生成了JVM字节码,也就是class字节码,这个过程其实就是javac内部的执行流程,因为javac是编译命令,所以整个编译的流程就是编译器分析源文件,核心就是对源文件的词法和语法分析,如果你大学学过编译原理这门课的话你肯定知道任何一门语言的核心都是编译器,能写出来编译器的都不是一般人。

1-3、类加载器

上面已经了解了整个编译流程,在编译流程结束后会生成class字节码,有了字节码,那肯定是需要ClassLoader将字节码加载到JVM的虚拟机内存中,所以来了解下在Java中都有哪些类加载器呢?下面这张图中就是JVM所提供的所有ClassLoader:

1、Bootstrap ClassLoader:这个classloader是加载jre\lib\rt.jar这个jar包中所有的class字节码,rt.jar是jdk提供的核心的运行时环境的jar包。

2、Extension ClassLoader:这个classloader是加载jre\lib\ext这个目录下所有的jar包中的字节码,它和上面的Bootstrap ClassLoader都是用来加载jdk特定jar包的。

3、App ClassLoader:它是用来加载应用程序的ClassLoader,所以它是应用程序真正用到的加载器。

4、Custom ClassLoader:这个是可以自定义的ClassLoader,它的作用是可以重写一个ClassLoader,然后让它来加载自定义的Class文件。Android虚拟机也继承了Java虚拟机这一特性,所以在Android开发中也可以实现动态加载。

1-4、加载流程

了解了以上这些类加载器之后,那接下来就是要看一下这些类加载器是如何将Class字节码加载到JVM对应的内存中的,先来看一下下面这张流程图吧:

首先是Loading就是加载,接着是Linking是一个连接的过程,这个过程又会分为三个步骤执行,首先第一步是Verifing验证,然后是Preparing准备就绪,最后是Resolving分析字节码,在走完整个Linking流程以后才会执行Initializing去完成变量的初始化。下面来具体的看一下这几步分别都有着什么作用:

  • Loading:类的信息从文件中获取并且载入到JVM的内存里
  • Verifying:检查读入的结构是否符合JVM规范的描述
  • Preparing:分配一个数据结构用来存储类的信息
  • Resolving:把这个类的常量池中的所有的符号引用改变成直接引用
  • Initializing:执行静态初始化程序,把静态变量初始化成指定的值

比如我在java源代码中定义了一个常量:

public static final String CONSTANT = "Jarchie"; 

这样一行代码其实它并不会立即给CONSTANT这个常量赋值,二是会先走上面说的这个流程。

二、虚拟机内存管理

内存管理也是JVM比较重要的模块,而且也是面试过程中的一个重点,如果考察JVM的内容很容易被问到这个东西。在上面说到JVM结构的时候提到过,JVM将内存空间划分为四个区:方法区、堆区、栈区、本地方法栈,下面就来看一下每个区分别是什么作用?

1、Java栈区

作用:它存放的是Java方法执行时的所有的数据,是用来描述Java方法执行的完整的内存模型

组成:由栈帧组成,一个栈帧代表一个方法的执行。

下面来说说Java栈帧是什么?

栈帧的作用:每个方法从调用到执行完成就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

举个栗子:a()方法在运行时调用到b()方法,在执行到调用代码时,JVM会创建一个保存b()方法的栈帧,然后把这个栈帧push到Java栈区中,当b()方法执行完成返回到a()方法中时,这个栈帧就会随之被pop出Java栈区,这就是一个栈帧描述一个方法调用的完整过程。

栈帧的组成:局部变量表、栈操作数、动态链接、方法出口,可见栈帧存储了方法调用过程中的所有内容。

2、本地方法栈

作用:本地方法栈是专门为native方法服务的,它也是通过栈帧来记录每个方法的调用

普通栈区是为Java方法服务的,本地方法栈是专门为native方法服务的,这是Java栈区和本地方法栈的一个重要区别。

3、方法区

作用:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的数据等,且方法区是永远占据内存的

4、堆区

作用:所有通过new创建的对象的内存都在堆中分配

特点:是虚拟机中最大的一块内存,是GC要回收的部分

对于堆区这块大内存,JVM是如何分配的,来看一下下面这张图(我觉得这张画的还不错😀):

上面:Young Generation是堆区中的新生代区,对于刚创建的对象会被放入到新生代区。

中间:Old Generation是老年代区,当新生代区内存不足时,Java虚拟机会按照一定的算法将新生代区的对象移到老年代区中,这样新生代区就又有了新的内存空间。

下面:Permanent Generation是持久代(这个现在好像已经移除了),主要存放的是Java类的类信息,与GC要收集的Java对象关系不大。

GC重点回收的是新生代和老年代这两部分的内存区域,当新生代和老年代都没有足够的内存时,JVM就会抛出OOM内存溢出的异常。JVM将内存区域分新生代和老年代而不是一块完整的内存区域,这样做最大的好处就是开发人员可以按需分配新老两代的内存空间,使之更好的适应不同的业务场景,当然了对于移动端不会这么做哈,这个手动调节说的是服务器。

三、垃圾回收

垃圾回收也就是我们平时经常说的GC,这个也是面试中经常被问到的东西。既然是垃圾回收,那首先就来看看哪些对象会被标记为垃圾对象?

3-1、垃圾收集算法

1、引用计数算法

这个算法是Java虚拟机最早使用的算法,在JDK1.2之前都是使用的这个算法来标记对象是不是垃圾对象,在内存中创建一个对象的同时会为它产生一个引用计数器,同时将引用计数器加1,每当有新的引用引入到子对象的时候,计数器就累计加1,当其中的一个引用销毁的时候,引用计数器减1,当引用计数器减为0的时候,标志这个对象已经是垃圾对象了,可以被回收。

这种算法存在一个很大的问题,举个栗子:下图中ObjectA引用了ObjectB,所以ObjectB的引用计数器是+1,而ObjectB又引用了ObjectA,所以ObjectA的引用计数器也是+1,同时ObjectA和ObjectB都是不可达的,就是没有路径能指向这两个对象,所以它们其实已经是垃圾了,但是在这种算法下却不能被回收。

2、可达性算法

由于引用计数法存在的问题,所以从JDK1.2以后对算法进行了改进,开始使用可达性算法,也叫根搜索算法,这个算法是由离散数学中的图论引入的。整个程序把所有的引用关系看做一张图,从GCRoot根节点开始寻找对应的所有的引用节点,然后继续寻找对应节点所有的引用,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用的节点,即不可达的节点,被认为是垃圾对象,所以这种算法就避免了上面出现的那个问题。

上面这张图中可以看到,从GC Root这个根对象引用开始一直遍历,只要路径可达,那么这个对象就是被引用则不能被回收,像ObjectD、ObjectE、ObjectF因为没有路径可以到达这几个部分,所以它们都是垃圾对象可以被回收。在确定垃圾对象的时候经常会说到引用,下面来看一下对象都有哪些类型的引用呢?

3、引用的类型

引用类型分为四种:强引用、软引用、弱引用、虚引用,实际开发中使用最多的是强饮用和弱引用。

弱引用的创建,简单的来看个例子吧:

Object obj = new Object(); //强引用创建,obj这个引用指向Object对象
WeakReference<Object> wf = new WeakReference<Object>(obj); //创建obj对象的弱引用wf
obj = null; //此时只有wf这个弱引用指向Object对象
//获取真正的引用,使用时要判断获取到的引用是否为空
//因为弱引用不会阻碍对象的回收,所以很有可能被置为空
wf.get(); 

在确定了垃圾对象以后,那么如何回收垃圾对象呢?

3-2、垃圾回收算法

1、标记-清楚算法:通过一张图来看一下它具体是如何进行垃圾回收的

首先它会从根集合也就是根节点来遍历所有的引用,从根集合可以路由到A对象的引用,通过A对象也可以路由到C对象的引用,而B对象成为了不可达的对象引用,所以扫描过后B被标记为可回收对象,最后在垃圾回收执行的时候,会直接把B对象置为空,在内存块中就剩下了A和C对象的引用,B则被垃圾回收给回收掉了,这就是标记-清楚算法的流程。

优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象较多的情况下极为高效。

缺点:由于它是直接回收不存活的对象,所以会造成内存碎片,不利于后续对象的内存分配。

2、复制算法:同样的通过图形来看一下:

它的处理较为简单,首先也是从根集合进行遍历,遍历到A引用是可达的,则把A引用复制到另一块空闲的内存中,接着发现B引用是不可达的,则跳过不进行复制,继续遍历发现C也是可达的,同样的把C也复制到这块空闲的内存中,等所有的复制都处理完以后,它会把原来的内存空间清空,只保留复制以后的这块内存空间,这样就完成了对垃圾对象的回收,这种算法其实是利用了以空间换时间的思想。

优点:当存活对象较少时,极为高效

缺点:它需要一块内存作为交换空间进行对象的移动

3、标记-整理算法:同样的通过下图来看一下它又是如何处理的:

首先它也是从根集合进行遍历,通过整个内存区的扫描,将可回收对象扫描出来,到了第二阶段图中B就被标记为了可回收对象,从第二阶段到第三阶段,直接扫描整个空间并清除被标记的对象,可见标记-整理算法是在标记-清楚算法上进行的改进,只是在回收不存活对象占用的空间时,它会将所有的存活对象往左端空闲处移动,并更新对应的指针,所以这种算法在标记-清楚算法上进行对象的移动,因此成本更高,但是解决了内存碎片的问题,同时它因为需要不断的移动对象到另一侧,所以它不适合频繁创建和回收对象这种场景。

通过对比可以发现,这三种算法只是相对优劣,并没有哪一种算法是彻底解决了所有问题,所以这三种算法在虚拟机中是结合使用的。

4、触发回收

  • Java虚拟机无法再为新的对象分配内存空间了(即应用程序的存储空间已经分配完了)
  • 手动调用System.gc()方法(强烈不推荐)
  • 低优先级的GC线程,被运行时就会执行GC

四、Dalvik VM与JVM的不同

  • 执行的文件不同,一个是class,一个是dex 
  • 类加载系统与JVM区别较大
  • JVM只能同时存在一个,而DVM可以同时存在多个
  • Dalvik是基于寄存器(寄存器是比内存快的存储介质,所以运行速度更快)的,而JVM是基于栈的

五、Dalvik与ART的不同

首先了解两个名词JIT和AOT,关于这两个词的解释我在《Dart语言之常用数据类型》一文中Dart概述里有提到过,有不了解的可以直接点过去看看,这里不再解释了,下面直接来看它们的不同点:

  • DVM使用JIT来将字节码转换成机器码,效率低(JIT应用程序每次运行都会将字节码转化成本地机器码再去执行)
  • ART采用了AOT预编译技术,执行速度更快(应用程序安装时就将字节码转换成本地机器码)
  • ART会占用更多的应用安装时间和存储空间(以空间换时间)

对于DVM和ART这两种虚拟机就不再详细介绍了,这里也已经对比着了解了一下,它们的内存管理和垃圾回收等模块和JVM都是类似的,区别较大的是类加载模块,这个会在下一篇中介绍,当然了介绍的是Android中的类加载机制,写到这里,这一篇就已经结束了,有不对的地方欢迎留言探讨,因为我也是个菜鸟!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值