《深刻java虚拟机》读书笔记之Java内存区域

前言

该读书笔记用于记录在学习《深刻理解Java虚拟机——JVM高级特性与最佳实践》一书中的一些重要知识点,对其中的部份内容进行概括,主要是方便以后进行复习。java

运行时数据区域

Java虚拟机在执行过程当中会将其管理的内存划分为多个不一样的数据区域。其中一些区域随着虚拟机启动而建立,一些区域生命周期则依赖用户线程的启动和结束。算法

下面是JDK1.7 数组

JDK1.7

程序计数器

是一块较小的内存空间,用于记录当前线程所执行的字节码的行号,在执行过程当中经过改变计数器的值来选择下一条被执行的指令。分支、循环、异常处理等都经过程序计数器实现。安全

在多线程环境下,CPU在不一样线程间进行切换时,为了保证CPU下一次切换到线程时能继续以前的执行轨迹进行,须要时用程序计数器记录下切换前执行到哪一步。该区域为各个线程私有,互不干扰。该区域不会发生OOM。数据结构

若是线程在执行一个java方法,那么程序计数器将会记录正在执行的虚拟机字节码的指令地址。而若是正在执行的是一个native方法,计数器的值则为空。多线程

Java虚拟机栈

Java虚拟机栈由线程私有,其生命周期同线程同样。Java虚拟机栈主要是用于描述Java程序中方法执行时的内存模型。并发

每个方法在执行的时候都会建立一个栈帧,栈帧里存储局部变量表、操做数栈,动态连接等信息。每个方法从开始调用到执行结束的过程就对应着一个栈帧在java虚拟机栈的入栈到出栈的过程。函数

栈帧

栈帧是支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表,操做数栈,动态连接和方法返回地址等信息。每个方法从调用开始到执行结束都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。oop

  • 局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义的变量。
  • 操做数栈用于执行一个方法,本质是一个栈。如对于两数相加,会先将两个数入栈,执行加法操做是将两个数出栈相加而后将结果入栈。
  • 动态连接是指在运行期间才能肯定具体调用某一个方法。
  • 方法返回地址表示一个方法完成后返回到调用方的地址,方法返回可使正常返回或者未被处理的异常。

本地方法栈

本地方法栈和虚拟机栈的做用同样,不一样之处在于java虚拟机栈服务于虚拟机中的字节码(java方法)。而本地方法栈则是为虚拟机使用native修饰的方法服务。布局

Java堆

java堆是Java虚拟机管理的内存中最大的一块,同时堆被全部的线程所共享,堆内存在虚拟机启动是被建立。Java堆的做用在于存放对象的实例,几乎全部的对象实例都在Java堆上分配内存。同时Java堆也是垃圾收集器管理的主要区域,根据分代收集算法还能够将堆分为新生代和老年代,更细致的能够分为Eden区、from区、to区。

方法区

方法区和堆同样被各个线程所共享。方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也被称为“永久代”(PermGen)。

运行时常量池

运行时常量池在JDK1.7以前是方法区的一部分,在JDK1.7的时候运行时常量池就被移到堆中。常量池用于保存编译期生成的各类字面量和符号引用,但并非说只有编译期才能生成常量。在运行时也能够将新的常量放入常量池中(String的intern()方法)。

直接内存

直接内存并非Java虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,因此直接内存的大小不受Java堆大小的影响,可是仍是受宿主机内存大小影响,也会发生OOM。在Java中使用该区域的典型表明就是NIO。

元空间(MetaSpace)

在JDK1.8以后,HotSpot JVM移除了方法区,而使用本地化内存来代替,这块区域被称为“元空间”。“永久代”被移除使得JVM参数PermSize 和MaxPermSize会被忽略,当前在启动时会有警告信息。添加了一个MaxMetaspaceSize对元数据区大小进行调整。默认状况下,类元数据分配受到可用的本机内存容量的限制。

为何要移除方法区?

使用永久代来存储类信息、常量、静态变量等数据不是个好主意, 很容易遇到内存溢出的问题.JDK8的实现中将类的元数据放入 本地内存, 将字符串池和类的静态变量放入java堆中。同时对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引起的Full GC和OOM等问题。

各个JDK版本的运行时数据区域一览

JDK1.6

JDK1.6

JDK1.7

JDK1.7

JDK1.8

JDK1.8

内存区域内的异常

堆栈溢出

Eorror

主要发生的异常分为两种:OutOfMemeryErrorStackOverFlowError。其中OutOfMemeryError是当Java虚拟机因为内存不足而没法分配对象时抛出,而且垃圾收集器再也不有可用的内存。而StackOverFlowError是当堆栈溢出发生时抛出的。

在内存区域的各部分中,程序计数器不会发生内存溢出的状况。

虚拟机栈和本地方法栈用于存储方法执行的顺序,当方法的调用层次过深(递归)时,可能会致使分配的栈内存不足时将会抛出StackOverFlowError的异常。从上图咱们能够看出这一块区域还会抛出OutOfMemeryError。这是由于挡在多线程状况下,虚拟机中大量的线程进行方法调用致使建立的栈帧建立过多使得Java虚拟机因为内存不足而没法分配对象。

对于Java堆和方法区可能会因为程序中建立的实例过多而致使OutOfMemeryError

若是线程请求的栈深度大于虚拟机锁容许的最大深度,将抛出StackOverFlowError,StackOverFlowError通常是函数调用层级过多致使,好比死递归、死循环。这类异常通常须要咱们检查代码是否存在逻辑上的问题。

若是虚拟机在扩展栈时没法申请到足够的内存空间或者堆中存在着大量没法被gc的类信息,将抛出OutOfMemeoryError,前者通常是在多线程环境才会产生,通常用“减小内存的方法”,既减小最大堆和减小栈容量来换取更多的线程支持。减小最大堆使得栈可被分配的内存变大,能够容纳更多的线程,减小栈容量使得可建立的栈帧数量变大。后者须要咱们dump出堆异常模型检查问题。若是是内存溢出则调整堆的大小,内存泄露则须要检查相关代码。

方法区或元数据区溢出

方法区和元数据区用于存放class的相关信息,当工程中类比较多,而方法区或者元数据区过小,在启动时容易抛出OOM异常。

JDK1.7以前,经过-XX:PermSize,-XX:MaxPerSize,调整方法区的大小;

JDK1.8后,经过-XX:MetaspaceSize ,-XX:MaxMetaspaceSize,调整元数据区的大小;

本机直接内存溢出

jdk自己不多操做直接内存,而直接内存(DirectMemory)致使溢出最大的特征是:Heap Dump文件不会看到明显异常,而程序中直接或者间接的用到了NIO。

直接内存不受java堆大小限制,但受本机总内存的限制,能够经过MaxDirectMemorySize来设置。

对象的建立

在咱们平时使用Java语言建立对象时,使用最多的确定是经过new关键字完成,固然还有其余的方式如反射,克隆等。咱们使用很简单,可是实际上关于建立一个对象在虚拟机中是作了大量的事情的。

当虚拟机检测到一条new指令时,首先到常量池中检测new指令所携带的参数是否能和常量池中的某一符号引用所对应,若是找到了对应的符号引用还须要检查其所表明的类是否已经完成了加载、解析、初始化等过程。若是以上条件不知足那么还须要先进行类加载过程。

内存分配

当完成了类加载过程后就须要对对象进行内存分配了,为一个对象分配内存的大小实际上在类加载过程当中就已经肯定下来,这里的内存分配过程就是讲一块肯定大小的内存从Java堆中划分出来。

指针碰撞(Bump the Pointer)

假设堆中的内存是一块绝对规整的,即被使用的内存和没有被使用的内存有着明确的划分,一边是使用过的,一边是没有使用的,存在着一个指针做为分界的标志。那么分配过程实际上就是讲指针像没有被使用的区域移动肯定大小的距离。这种分配方式被称为“指针碰撞”。

空闲列表(Free List)

实际上不少状况下内存区域并不是像上面那种状况,而是已经使用过的内存和没使用过的相互交错,这时没有办法使用指针碰撞了。虚拟机会维护一个用于记录那些内存是可用的的列表。在内存分配时找出一块足够大小的地方划分给对象。这种方式被称为“空闲列表”。

使用哪一种方式

从上面的描述可知使用哪一种方式是由Java堆是否规整来决定,而Java堆是否规整则由使用的垃圾收集器是否带有压缩整理功能决定。

  • 指针碰撞:使用Serial,ParNew等带有Compact过程的收集器。
  • 空闲列表:使用CMS这种基于Mark-Sweep算法的收集器。

保证内存分配的安全性

在程序运行过程当中,对象建立时很是频繁的事情,在多线程状况下这个过程就变得很是危险。

同步

经过对对象建立过程进行同步保证在并发下的安全性,在虚拟机中经过CAS+失败重试的方式保证原子性。

TLAB(thread location allocation buffer)

经过预先在Java堆中为不一样的线程分配一块内存将线程间的内存分配过程隔离开来,这块内存区域被称为线程本地缓冲(thread location allocation buffer)。线程进行内存分配时在TLAB上进行。只有当TLAB不足须要从新分配时才须要同步操做。虚拟机是否使用TLAB能够经过参数-XX:+/-UseTLAB来控制。

初始化

上面的内存分配过程完成后,就须要将分配到的内存都初始化为零值。同时设置对象信息,例如该对象是哪一个类的实例、对象的哈希码、对象的GC分代年龄等信息。这些信息都是存放在对象的对象头中。

以上过程完成后会调用方法对对象的信息进行初始化,就是调用构造方法。

对象内存布局

在HotSpot Jvm中,对象的内存布局主要分为三个区域:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对其填充(Padding)

对象头(Header)

对象头中主要包含两部分,第一部分用于存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这一部分的数据在32位和64位虚拟机中的长度分别为32bit和64bit。

对象的另外一部分是类型指针,也便是一个对象指向它所属类的指针。虚拟机能够经过这个指针肯定对象属于具体哪个类,固然这一过程并非必定得,查找对象的所属类并非必定要经过对象自己。若是存储的是一个数组,那么对象头中还会存储该数组的长度。

实例数据(Instance Data)

实例数据是用于存储程序代码中定义的各个字段的内容,包括从父类继承的和子类从新定义的。该部分的存储顺序受到虚拟机分配策略参数和字段在源代码中定义的数据顺序影响。HotSpot Jvm中默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Points),即相同大小的字段会被分配在一块儿。在知足分配策略的条件下父类中的字段会被分配在子类的前面。

若是CompactFields为true(默认),那么子类中较小的变量会被插入到父类的空隙中取。

对其填充(Padding)

这一部分并非必要的,仅仅起到占位符的做用,除此外没有其余的做用。由于HotSpot虚拟机的自动内存管理系统要求对象的大小要求时8字节的倍数,对象头部分固定为8的倍数,而实例数据若是大小不到8的倍数就由对其填充来补充。

对象的访问定位

对象是在堆区域建立,为了使用对象,咱们须要经过栈上的reference数据来操做堆上的对象。目前主流的访问方式有两种:

  • 句柄访问
  • 直接指针访问

句柄访问

使用句柄访问的方式须要在堆上额外分配出一块区域来做为句柄池,在栈上的reference数据则存储对象的句柄地址而句柄则包括对象实例数据和类型数据的地址。

句柄访问

//图来自《深刻理解java虚拟机》

直接指针访问

直接指针访问的方式中reference存储的是对象的实例地址,而对象类型数据的地址则是存储在对象的实例中。

直接指针访问

//图来自《深刻理解java虚拟机》

使用句柄方式是reference数据不存储对象的具体地址而是经过句柄来指向,当对象移动(垃圾回收)后也不须要修改reference中的值。

使用直接指针访问好处在于直接定位到对象实例数据,不须要通过句柄这一次定位,在频繁的对象定位过程当中能有效提高效率,HotSpot使用直接指针定位方式。

相关文章
相关标签/搜索