【Java杂货铺】JVM#Java高墙以内存模型

Java与C++之间有一堵由内存动态分配和垃圾回收技术所围成的“高墙”,墙外的人想进去,墙外的人想出来。——《深刻理解Java虚拟机》

图片描述

前言

《深刻理解Java虚拟机》,学习JVM的经典著做,几乎学习JAVA的小伙伴人手一本。当初买了,翻看了一部分,到了字节码那边完全读不下去了,遂弃之。最近打算看Spring源码,反射、动态代理、设计模式等基础工具的确可让我更加容易理解源码内容。然而,看着看着才发现,这个日常咱们几乎用不到的东西(除了面试),才应该是理解java生态的出发站。因此,停下手来,从新看下这本书,再全面的了解下虚拟机,此次不管多么困难,也要把书读完,同时记好内容笔记和思考补充。做为Java围城之一的内存模型,比当时第一个要看的内容。java

出发,看看JVM大工厂

刚开始学Java的时候,被贯彻最多的两句话就是“一次编译,处处运行”和“Java不须要手动释放内存”。能作到这两点都是因为Jvm的存在。记得大学第一个启蒙语言c,电脑安装了一个cfree(一个体积超小的ide)就能够直接写了。而Java还须要下载一个叫JDK的东西,来开发。JDK包含一个叫JRE的东西,是Java的运行环境,之因此能够运行,是jre下拥有着JVM虚拟机。JVM做为一个程序,必定会占用电脑内存,而它所管辖内存间数据的互动,驱动着Java的工做。程序员

线程的指挥官:程序计数器

做为面向对象语言,Java每一个类都有本身的属性和使命,而且暴露方法出来供其余成员调用。一个业务逻辑,不一样对象之间调用方法、返回调用者,一个方法内部分支、循环等基础功能,都须要一个指挥官来完成,指挥官告诉这个线程内的对象执行的前后顺序。这个指挥官就叫作程序计数器。<mark>程序计数器是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。</mark>由于一个CPU同一时间只能操做一个线程中的指令,因此每一个线程须要私有一个指挥官,因此程序计数器这类内存也叫作线程私有内存。面试

若是一个线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;若是是正在执行的Native方法,这个计数器值则为空(Undefined)。Native方法就是Java调取本地其余语言的方法,此方法实现不受JVM管控,因此没法感知到地址,计数器值天然为空。编程

另外,程序计数器区域是惟一一个Java虚拟机规范中没有规定任何OutOfMemoryError状况的内存区域。后端

引用的地盘: Java虚拟机栈

咱们使用Java新建一个对象,首先须要声明类型,此时就出现了一个引用,引用指向建立出的对象。咱们都知道引用在栈中,对象在堆中,此时说的栈就特指Java虚拟机栈。Java虚拟机栈一样属于线程私有的,因此生命周期和线程相同。每一个方法在建立的同时,都会建立一个栈帧用于储存局部变量表、操做数栈、动态连接、方法出入口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。设计模式

局部变量表存放了编译时克制的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)。对象引用直接或者间接指向堆中对象的地址。因为此过程是在编译时期完成的,因此局部变量内存分配大小是固定的,不会在运行时改变大小。其中64位长度的long和double类型的数据都占用了2个局部变量空间(Slot),其余数据类型只占1位。数组

在这个区域可能会出现两种异常:若是线程请求的栈深度过大,也就是说虚拟机栈在本身管辖的内存形成的缘由,会抛出StackOverflowError异常,这个通常比较深的递归可能会形成。若是虚拟机栈发现本身内存不够,动态扩展,而且没法申请到足够的空间时,就会抛出OutMemoryError异常。安全

虚拟机栈的孪生兄弟:本地方法栈

本地方法栈几乎与虚拟机栈发挥的做用基本类似,毕竟孪生兄弟嘛。区别是Java虚拟机栈是为字节码服务的,也就是Java方法自己。而本地方法栈是为了Native方法服务的,这个涉及调取本地的语言,例如C。前后端分离

这里插个小曲,native对于我们Java编程者来讲不多直接操做,可是这东西无处不在,好比说Object类,你看源码,不少方法都有native关键字。这些方法具体实如今java代码里面不管如何都找不到的,由于具体实现就是调取的本地,而且调取本地的代码不受JVM控制!在编译的过程当中,若是发现一个类没有显示继承,那么就会被隐式继承Object类,也就有了Object类全部的方法。ide

GC最喜欢的地方:Java堆

咱们常说的堆栈,说的就是这个堆。能够说Java堆是虚拟机所管辖最大的一块内存空间,而且此空间是全部线程共享的。<mark>几乎全部的对象实例都分配在这里</mark>,全部的对象实例和数组都要在堆上索取空间。Java堆也是垃圾收集器管理的主要区域,这个之后会细讲。
Java堆能够处于物理上不连续的空间中,只要逻辑上是连续的便可。若是堆中没有内存完成实例分配,而且对也没法再拓展时,将会抛出OutOfMemoryError异常。

永久代的假装:方法区

大佬书中讲这部份内容的时候仍是以JDK1.6为范本,可是直接被堆内存所托管了。JDK1.8这部分已经变成元空间了,而且成为了堆外内存,不受JVM直接管辖。可是为了更好的理解JVM内存模型的设计理念仍是看下这部份内容。

方法区也属于线程共享区间,它储存着<mark>类信息、常量、静态变量即时编译后的代码等数据</mark>

相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入了方法区就如永久代的名字同样“永久”存在了。这群有一样的内存回收目标主要是针对常量池的回收和堆类型的卸载,可是回收条件至关苛刻。同堆同样,可能会致使OutOfMemeoryError异常。

运行可变区域:运行时常量池

既然有运行时常量池,就会有普通的常量池(简称常量池)。常量池用于存放编译期生成的各类字面量和符号引用,字面量至关于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了以下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。

运行时常量池相对于普通的常量池(又称Class文件常量池)有一个重要特征动态性。Java语言并不要求常量只能在比那一块儿才能产生,运行期间也能够加入常量到常量池(运行时常量池)中,好比String的intern()方法。

运行时常量池属于方法区的一部分,天然受到方法去内存的限制,也会抛出OutOfMemoryError异常。

JVM外的世界:直接内存

直接内存并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。还记着前面说的有native关键字的方法吗?包括netty模块的一些Native函数库都是直接分配堆外内存的,而后经过一个储存在Java堆中的DirectByteBuffer对象做为这块内存的引用来操做。这样作,就是觉得须要操做的数据在Native堆(你电脑上不被JVM管辖的内存空间)上,避免了将Java堆数据和Native堆数据来回复制。固然这块内存也不能无限放大,好比超过你电脑的内存,因此也可能出现OutOfMemoryError异常。

让数据动起来

内存空间不在于划分,在于使用。大佬在书中继续以HotStop虚拟机堆内存为例,讲解了数据的建立、分布、与访问。

一个对象的诞生

内存分配

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否可以在常量池中定位到一个类的符号引用,并检查这个符号引用表明的类是否已被加载、解析和初始化过。接下来,虚拟机会为这个新生儿分配内存(加载完成后的内存是彻底肯定大小的)。和计算机管理内存的方式同样,Java堆维护内存,有一张空闲列表,用于记录堆内哪些空间没有被使用过。因为堆在物理上是不连续的,因此就须要有个地方记录哪些空间是被使用的,哪些是空闲的。还有一种记录方式叫指针碰撞,假定Java堆中的内存是绝对规整的连续的(这显然很难作到,须要GC作压缩整理)。在这条十分规整的,十分长的堆内存空间上,有一个指针,左右两侧分别是空闲区间和已使用空间,若是有空间须要被申请或者释放,指针就左右移动。就好像温度计,水银好似已使用空间,上方空闲部分就是空闲空间,当温度达到100度,到了温度计的量程,就会炸了(出现OutOfMemoryError异常)。

原子操做

为了保证内存在使用的时候是线程安全的,须要采用一些机制。第一种就是CAS机制,这是一种乐观锁机制,再加上失败重试,能够保证操做的原子性。还有一种就是本地线程分配缓冲,把内存的动做按照线程划分在不一样的空间上进行,即每一个线程在Java堆中预想分配一小块内存供本身使用,让Java堆的共享强制编程线程私有。

对象设置

接下来,虚拟机要对对象头进行必要的设置,例如这个对象是哪一个类的实例、如何才能找到<mark>类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息都存放在对象的对象头之中。</mark>完成上述操做,一个对象在虚拟机的层面已经完成了,可是在代码层面还须要设置初始值,按照程序员的意愿选择不一样的构造函数,传入不一样的参数进行初始化。

对象的内存分布

在HotSpot的虚拟机中,对象在内存中储存的布局能够分为3块区域:<mark>对象头、实例数据、对齐填充。</mark>

HotStop虚拟的对象头包含两部分信息,第一部分用于储存对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程II、偏向时间戳。官方叫这部分是Mark Word,这部分虽然在对空间上,可是这部分会根据对象的状态服用本身的储存空间。除了储存自身状态外,还有一部份内容叫类型指针,即指向它的类元数组的指针,虚拟机经过这个指针来肯定这个给对象是哪一个类的实例。另外,若是对象是一个Java数组,那在对象头中还必须有一块用于记录组长度的数据。

接下了就是实例数据部分,即真实储存的有效信息,也就是程序代码中所定义的各类类型的字段内容。包含从弗雷继承的,和子类定义的。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略中能够看出,相同宽度的字段老是被安排在一块儿。在知足这个前提条年间的状况下,在父类中定义的变量会出如今子类以前

第三部分就是对齐填充,没有什么特别的意义,就是个占位符。因为对象的大小必须是8字节的整数倍,因为对象头部分正好是8字节的倍数,实例数据不必定是,因此就须要填充一下。

对象的访问定位

咱们都知道真正的对象实在堆上,可是咱们操做对象使用的是引用,在虚拟机栈上的引用是如何访问对上的数据呢?主流的有两种方式。

句柄

Java堆中将会划分出一块内存来做为句柄池,reference中储存的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

大佬的图片拿来一用

直接指针

Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中储存的直接就是对象地址。

大佬的图片再用一下

这两种方式的优缺点就好像数组和链表同样,一个访问速度快,一个操做快。毕竟世界是公平的,省功不省力,省力不省功。句柄访问的最大优势就是reference中储存的是稳定的句柄地址,在对象被移动时指挥改变句柄中的实例数据指针,而reference自己不须要修改。因此修改数据特别快。

相应的直接指针访问最大的优点就是访问对象自己更快,毕竟少了一次指针的地址定位。HotShot最主要就是采用这种方式访问对象。

一些补充

大佬在本章还进行了抛OutOfMemoryError异常的实战,内容较长,仍是看书讲的更清楚些。更主要的是,我以为实战这种东西不能只看,具体问题还得具体分析,等遇到的多了,天然解决起来就会驾轻就熟。不过这部份内容有一些值得记录的知识点。

  1. 通常来讲,栈深度(好比递归)达到1000~2000是没有问题的,因此咱们写代码的时候必定要注意栈的深度,不要过深,但也要充分使用递归这种用空间省时间的方式。
  2. JDK1.6~JDK1.8常量池的位置变更,致使一些方法展示出来的现象不一样。例如String.intern()方法,在1.6时代,intern()方法会将首次遇到的字符串实例复制到永久代中,返回永久代中这个字符串实例的引用。而1.7的intern()方法不会复制实例,只是在常量池中记录首次出现的实例引用。
  3. 动态代理(例如CGLib)是对类的一种加强,加强的类越多,就须要更大的内存来保存这些数据。
  4. 还有种动态生成就是JSP(虽然如今大多数都是先后端分离,不用这个了),JSP第一次运行须要编译成Servlet,也须要产生大量的空间。值得一提的是,原来我在上家公司,有个系统是JDK1.7,当时JSP编译出来的东西还存放在方法堆中,当时可能设置的堆内存不大,本地跑一天,每次打开JSP页面,电脑都会卡顿一下(固然机子差也是缘由之一),普通的Java文件就没事,我想是否是也是这个缘由呢。另外对于同一个文件,不一样的加载器加载也会视为不一样的类。

结束

感受每次看JVM这块内容都会有新的体会。JVM做为Java运行的基石,是每个Javaer都须要了解的。和不少面试JVM总结内容相比,看本文确实是浪费时间,但我仍是想记录下看书的感觉,为了未来回忆起看书时灵光一现的小想法留个笔记吧。这本书真的不错,若是想了解JVM的小伙伴仍是买来看一看吧。我一直以为,从长远来看,比起看博客看视频,看书是效益最高的方式,毕竟伴随者大量的思考。

相关文章
相关标签/搜索