写了上篇JVM的文章后,我被同事diss了

前言

上周我刚把和小姐姐关于JVM的愉快探讨过程整理成文字发出来,就惨遭蛋哥的diss。java

对了,还没看过上篇文章的小可爱请先移步这里: 那天我和小姐姐扯了半天的JVMweb

蛋哥:关于JVM小姐姐理解的挺不错的,为何你不整理完整!面试

我:由于文章字数有限,浓缩的都是精华嘛~算法

蛋哥:懒就懒嘛,还把懒说的那么清新脱俗~(并扔给我一个白眼)微信

我:嘻嘻~那我再补充补充...数据结构

蛋哥没再搭理我,扔给我一个文件,就开始闷头写代码。编辑器

过了一下子,微信上弹出蛋哥发的两行消息:看了你的文章以后我大体围绕如下几点进行了简单的补充:布局

  1. JVM内存区域的转变
  2. Java代码运行过程
  3. 运行时栈帧结构
  4. JVM堆内存分配方法
  5. JVM是如何对对象的访问进行定位的?

正文

JVM内存区域的转变

在上篇文章中咱们知道,方法区主要存储类的相关信息,且被全部线程共享,采用永久代的方式实现了方法区。post

不过方法区和永久代又有着本质的区别。方法区是JVM的规范,而永久代则是JVM规范的一种实现,而且只有HotSpot才有永久代,而对于其余类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并无永久代一说。url

本文咱们主要以HotSpot为例展开探讨,咱们先看一下jdk1.6及之前的JVM运行区域,以下图:

但在jdk1.8之后,永久代被移除,改成了元空间。具体演变过程是酱紫的:

可是为何在jdk1.8之后永久代被移除,改成了元空间呢?这样作有什么好处呢?

在讨论这个问题以前咱们要先明白元空间这个概念:

元空间:简单来讲就是存储类的元数据信息的一个空间区域,元空间并不在虚拟机中,而是使用本地内存。

那什么又是元数据呢?

元数据,关于数据的数据或者叫作用来描述数据的数据或者叫作信息的信息。

听起来是否是很抽象???

这么说吧,咱们能够把元数据简单的理解成,最小的数据单位。元数据能够为数听说明其元素或属性,好比名称、大小、数据类型、等,或其结构像长度、字段、数据列之类,或其相关数据,位于何处、如何联系、拥有者等等。

明白了元空间元数据这两个概念以后,那么咱们就来讲说他的好处。

本质上来说,元空间和永久代相似,都是对JVM规范中方法区的实现。咱们知道,类及方法的信息等比较难肯定其大小,所以对于永久代的大小指定比较困难,过小容易出现永久代溢出,太大则容易致使老年代溢出;而元空间并不在虚拟机中,而是使用本地内存。所以,默认状况下,元空间的大小不受JVM控制,也不会再进行GC了,它仅受本地内存的限制,所以不会出现OOM异常。

Java代码运行过程

Java代码的整个运行过程能够分为编译阶段加载阶段

咱们先来看下在编译阶段Java源代码经历了些什么:

1. 编译阶段

Java源代码经过词法解析、语法解析、语义解析等一系列执行过程,生成字节码。

很抽象有木有?啥是词法解析、啥是语法解析、啥是语义解析???别急,下面咱们一一解释:

词法解析:即经过空格分隔出单词、操做符、控制符等信息,将其造成token信息流,传递给语法解析器。

语法解析:把词法解析得到的token信息流按照Java语法规则组装成语法树。

语义解析:检查关键字的使用是否合理、类型是否匹配、做用域是否正确等。

明白了这些,下面咱们一块儿来看看加载阶段都作了什么:

2. 加载阶段

加载阶段类加载器对字节码通过Load阶段、Link阶段、Init阶段等一系列动做,将其加载到JVM,才能够执行。执行模式有三种:解释执行JIT编译执行JIT编译与解释混合执行

啥啥啥???啥Load阶段?啥Link阶段?啥Init阶段?可不能够具体一点???

具体来讲是酱紫的:

Load阶段读取类文件,产生二进制流,并转化为特定的数据结构,初步经过cafe babe魔法数来校验是否为Java类文件或文件是否受损、常量池、文件长度、是否有父类等等,而后建立对应的java.lang.Class实例。

Link阶段包括验证、准备、解析三个步骤。验证是更详细的校验,好比final是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值;解析类和方法确保类与类之间相互引用的正确性,完成内存结构布局。

Init阶段执行类构造器clinit方法,若是赋值运算是经过其余类的静态方法完成的,那么会立刻解析另一个类,在虚拟机中执行完毕后经过返回值进行赋值。

它的整个过程以下图:

运行时栈帧结构

运行时栈帧结构是酱紫的:

  1. 虚拟机栈的内部结构是栈帧,每一个方法在执行的时候都会建立一个栈帧,用于存储局部变量表,操做数栈,动态连接,方法返回地址等信息;

  2. 局部变量表用来完成方法参数以及局部变量列表的传递过程,若是是实例方法,那么局部变量表中的每0位索引的Slot默认是用于传递方法所属对象实例的引用;

  3. 在方法执行的过程当中,会有各类字节码指向操做数栈中写入和提取值;

  4. 某方法经过动态连接在常量池中查询方法的引用来调用另外一个方法,进而完成方法调用;

  5. 方法返回地址即在方法退出以前,都须要返回到方法被调用的位置,程序才能继续执行;

  6. 某方法在调用另外一个方法的过程,便是一个栈帧在虚拟机中的入栈到出栈的过程。

最后,虚拟机中的方法入栈的顺序和方法的调用顺序是一致的,先入栈的方法后出栈,后入栈的方法先出栈。

JVM堆内存分配方法

目前内存分配方法主要有指针碰撞法和空闲列表法。

指针碰撞法:假设堆中内存是绝对规整的,全部用过的内存放一边,未使用过的放一边,中间有一个指针做为临界点,若是新建立了一个对象则是把指针往未分配的内存挪动与对象内存大小相同距离,这个称为指针碰撞。以下图所示:

空闲列表法:其基于标记清除算法,内存划分红网格区,内存分配不规整,即已使用的和未使用的内存随机分布。JVM 会维护一个记录表,用于记录那些内存可用于分配,当须要给对象分配内存区域时,寻找一块足够大的内存空间分配给对象,并更新记录表,这种分配内存的方法叫作空闲列表法。以下图所示:

JVM是如何对对象的访问进行定位的?

咱们建立对象天然是为了后续使用该对象,Java程序会经过栈上的reference数据来操做堆上的具体对象。

但是在《Java虚拟机规范》里面,只规定了reference类型是一个指向对象的引用,并无定义这个引用应该经过什么方式去定位、访问到堆中对象的具体位置,因此对象访问方式也是由虚拟机实现而定的。

不过,目前主流的访问方式有句柄直接指针两种方式。

指针: 指向对象,表明一个对象在内存中的起始地址。

句柄: 能够理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。

了解了指针和句柄的概念,咱们就来看看他们具体是怎么对对象的访问进行定位的。

1. 句柄访问

Java堆中划分出一块内存来做为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造以下图所示:

其优势是,引用中存储的是稳定的句柄地址,在对象被移动。好比:垃圾收集时移动对象是很是广泛的行为时,只会改变句柄中的实例数据指针,而引用自己不须要修改。

2. 直接指针

若是使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。

速度更快,节省了一次指针定位的时间开销。因为对象的访问在Java中很是频繁,所以这类开销聚沙成塔后也是很是可观的执行成本。HotSpot中采用的就是这种方式。

最后

很是感谢小可爱们能看到这里,JVM内容复杂繁多,小码仔不可能在仅仅两篇文章中分析的面面俱到。最近这两篇文章整理的是咱们面试中最为常见的一些问题,问题基本上比较常规,但也正是面试中的高频问题。

越是常规,咱们越更要领悟、理解、掌握。可是这些相对常规的知识点你说你还不知道,那么我还能说什么~自裁吧。哦不,是关注我吧~

若是本篇文章有任何错误,请批评指教,不胜感激 !

若是你喜欢本文,那就点个赞吧~

欢迎关注个人微信公众号【小码仔】,咱们一块儿探讨代码与人生。

文章参考:

  1. 《深刻理解Java虚拟机》
  2. https://blog.csdn.net/u011531613/article/details/62971713
  3. https://blog.csdn.net/qq_38905818/article/details/10458235
  4. https://www.zhihu.com/topic/19566470/hot
  5. https://blog.csdn.net/u010588262/article/details/81365547
相关文章
相关标签/搜索