深刻理解java虚拟机-第二章:java内存区域与内存泄露异常

2.1概述:

  java将内存的管理(主要是回收工做),交由jvm管理,确实很省事,可是一点jvm因内存出现问题,排查起来将会很困难,为了可以成为独当一面的大牛呢,天然要了解vm是怎么去使用内存的。java

2.2运行时的数据区域

  vm会将管理的内存划分为不一样的区域,不一样的区域间有各自的用途,以及建立和销毁时间。具体的区域划分以下图:算法

  注:执行引擎跟本地库接口不是内存数据区,方法区跟堆内存才是共享的内存数据区数组

2.2.1程序计数器

  是一块较小的内存地址,能够认为是当前线程所执行的字节码的行号指示器。在概念模式中(不一样的虚拟机能够选择本身的实现方式),字节码解释器工做时,经过改变这个计数器的值来选择下一条执行的字节码命令。分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖这个计数器完成的。安全

  在JVM中多线程是经过线程轮流切换并分配处理器执行时间实现的,即在同一个时刻,一个处理器只会执行一个线程的命令,因此为了线程切换可以回到正确的执行位置,每条线程都要有独立的计数器。数据结构

  补充:多线程

  若是线程执行的是java方法,那么计数器记录的是字节码指令的地址,若是是Native方法,计数器则为空(Undefined),该区域在jvm规范中也没有OOM。并发

2.2.2java虚拟机栈

  是线程私有的,生命周期与线程相同。jvm

  虚拟机栈描述的是方法执行的内存模型,方法在执行的时候会建立栈帧(Stack Frame),用于存储局部变量表,操做数栈、动态连接、方法出口等信息。方法从调用到执行完成,对应一个栈帧从入栈到出栈的过程。xss

  局部变量表中存放的是:编译期可知的各类基本数据类型(八个基本数据类型),对象引用(多是指向对象地址的引用指针,也多是执行表明对象的句柄)和returnAddress类型(指向了一条字节码指令的地址)。函数

  long与double会占据两个局部变量空间(slot),其余占据一个,局部变量表所需内存大小编译期间就已经完成分配, 方法运行期间不会改变局部变量表的大小。

  jvm规范中,对栈规定了两个异常状态,线程请求的栈深度大于虚拟机所容许的深度会抛出StackOverflowError异常。

  虚拟机栈能够动态扩展来避免栈溢出,可是当扩展没法申请到足够的内存时,就会抛出OutofMemoryError异常。

2.2.3本地方法栈

  功能做用与虚拟机栈是很是一致的,区别就在于:java虚拟机栈为执行java方法服务,本地方法栈为虚拟机使用的Native方法服务。虚拟机规范并无对本地方法栈作硬性要求。

  HotSpot直接把本地方法栈跟虚拟机栈合二为一,本地方法栈也会抛出两个异常。栈溢出与内存溢出。

2.2.4java堆

  java堆(java Heap)内存是vm管理的最大的内存。

  java堆被全部线程共享的内存区域。该内存区域存在的目的是存放对象实例。

  规范中:全部的对象实例以及数组都是要求在堆上进行分配,可是随着JIT编译器的发展与逃逸分析技术,出现了栈上分配和标量替换,这会致使有一些微妙的变化。

  java堆是垃圾收集器的主要管理区域,也成为GC堆(Garbage Collected Heap),收集器都选择分代收集算法。

  java堆能够细分为:新生代和老年代:再细致点有Eden空间、From Survivor空间、 To Survivor空间等。

  从内存分配的角度看,java堆内是能够划分出多个线程私有的分配缓冲区的(Thread Local Allocation Buffer TLAB)。

  划分的详细的目的在于方便更好地回收内存。

  补充:java堆能够不处于物理上的连续内存,只要逻辑上连续就能够了,当堆没法继续扩展时,也会抛出OutOfMemoryError。

2.2.5方法区:

  方法区(Method Area)跟java堆同样,是线程共享的内存区域,用于储存VM加载的类信息,常量,静态变量,即时编译器编译后的代码等,还有一个别名(Non-Heap)非堆

  对于HotSpot来讲,方法区也可称为永久代(Permanent Generation),本质上二者并不等价,仅仅是由于HotSpot虚拟机把分带收集器扩展到了方法区(用永久代来实现方法区)。

  用永久代实现方法区会容易致使内存溢出问题(永久代有-XX:MaxPermSize的上限)。在jdk1.7中,已经把原来放在永久代的字符串常量池移出永久代了。

  VM规范对于方法区来讲,也能够不选择连续的物理内存,还能够选择固定大小或者可扩展,甚至你还能够选择不实现垃圾回收。

  针对方法区的回收主要是针对常量池的回收和对类型的卸载,当方法区没法知足内存分配的时候,就会出现OutOfMemoryError异常。

2.2.6运行时常量池

  运行时常量池(Runtime Constant Pool)是方法区的一部分。

  class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中。

  VM规范没有对运行时常量池的细节规范,通常状况下除了class的符号引用外,还会把直接引用也存在运行时常量池。

  运行时常量池相对于Class文件的常量池另外一个重要特征是动态性,运行期间也能够放入新的常量进入常量池。比较多的用法是String类的intern()。

  做为方法区的一部分,固然也会OutofMemoryError异常

2.2.7直接内存

  直接内存(Direct Memory)并非VM运行时数据区的一部分,也不是VM规范中定义的内存区域,可是若是该区域被频繁使用,也会致使OutOfMemoryError异常。

  NIO,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,能够直接使用Native函数分配堆外内存,而后经过在堆中的DirectByteBuffer对象做为该内存的引用进行操做。

  虽然该内存不受堆的限制,可是也可能受物理内存的限制,因此也可能由于设置的参数问题,致使动态扩展时出现OutOfMemoryError异常。

 

简单总结:讲了各个内存区域的一些实现细节跟部分VM规范,除了程序计数器外其余全部的内存区域都存在内存溢出异常,甚至于非jvm的内存区域直接内存,也有可能出现内存溢出异常。


 

2.3HotSpot虚拟机的对象探秘

 2.3.1对象的建立

  建立对象是经过关键字new来建立的。

  虚拟机收到new命令时,会去常量池中检查是否有对应参数的类的符号引用,并检查这个符号引用是否已经被加载、解析和初始化过,若是没有的话,那么要先进行类加载。

  类加载的检查完成后,就要对新生对象进行内存分配了,分配方式有两种,根据堆内存是否规整能够分为两类:指针碰撞(Bump the Pointer)、空闲列表(Free List)

  指针碰撞:堆内存规整,分配内存的过程仅仅是将指针向空闲空间挪动一段与对象大小一致的距离。

  空闲列表:若是内存不规整,那么已使用的内存与空闲内存交互,虚拟机会维护一个记录表,记录内存是否可用,在分配时从列表中找足够内存划分给实例,更新记录表。

  堆是否规整又跟垃圾收集器有关,使用Serial、ParNew等带Compact过程的收集器时,采用指针碰撞;使用CMS这种基于Mark-Sweep算法的收集器时,采用空闲列表的方式。

  除了分配内存外,还须要考虑在并发下的安全问题,虚拟机采用了CAS配上失败重试的方式保证更新操做的原子性;另外一种方式是把内存分配的动做按照线程划分在不一样的空间中,即每一个线程在java堆中预先分配一个内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪一个线程须要分配内存,就在哪一个线程的TLAB分配,只有TLAB用完须要进行新增时,才进行同步锁定操做。虚拟机是否使用TLAB,能够经过配置:-XX:+/-UseTLAB参数来设定。

  内存分配完成,VM还须要将分配的内存空间都初始化为零值(对象头除外),若是使用TLAB的话,那么该过程也会提早至TLAB时进行,这一步操做保证了对象实例字段在java代码中能够不赋初始值就直接使用,程序能够直接访问到这些字段数据类型的对应值。

  完成初始化工做后,VM要设置对象的对象头,相关信息:对象是哪一个类的实例,如何找到类的元数据信息,对象的哈希值,对象的GC分带年龄等信息。

  完成以上步骤后,VM的视角,新的对象已经产生了。可是java代码角度,对象建立才刚开始,<init>方法没有执行,全部字段都仍是0,执行完new指令后执行<init>方法后才算对象建立完毕。

  简单描述一下VM视角与程序视角下对象的建立流程:

    VM  ->  类是否初始化  内存分配  内存空间初始化  对象头赋值

    java程序  ->  类是否初始化  内存分配  内存空间初始化  对象头赋值  <init>方法

2.3.2对象的内存布局

  对象在内存中的布局分为三个区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

  HotSpot的对象头包含两部分信息,第一部分:存储对象自身的运行时数据、第二部分类型指针。

  存储运行时数据有:哈希吗,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,根据虚拟机位数不一样分别为32/64bit,称为Mark Word,其实是一个可动态的数据结构,以便以小空间存储更多的信息。

  类型指针:即对象指向它的类元数据的指针,VM经过指针肯定对象属于哪一个类。

  注:若是对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据。

  实例数据:对象真正存储的有效信息。

  父类定义在前,子类灾后,存储的顺序还受VM分配策略参数(FieldsAllocationStyle)和java源码中定义顺序影响。

  HotSpot的顺序是:从长到短,且字段相同的放在一块儿。

  第三部分对齐填充不是必然存在的,仅仅是占位符的做用。因为HotSpot的VM自动内存管理系统要求对象起始地址必须是8字节的整数倍,那么对象就必须是8字节的整数倍了,由于对象头部分是8字节的倍数,全部当实例数据没有对齐时,对齐填充就用来补齐。

2.3.3对象的访问定位

  虚拟机规范只规定了一个指向对象的引用,可是没有规定具体的方式。因此仍是根据虚拟机的具体实现来表述对对象的访问。常规的是两种句柄式与直接指针式:

  若是是采起:句柄访问的话,那么java堆会划分出一块内存来做为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了实例数据地址跟数据类型地址

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

   

两种对象的访问方式各有优劣,句柄的好处时,reference中存储的是稳定的句柄地址,对象被移动时,只改变句柄中的实例数据指针,而reference不改变。

  直接指针访问最大的好处是速度快,节省了一次指针定位的时间开销。

   HotSpot采用的就是直接指针访问的方式。

 


 

2.4实战OOM异常

   本节的目的:一、经过代码验证java虚拟机规范中描述的各个运行时区域存储的内容。

        二、帮助判断实际工做中是什么问题致使哪些区域内存溢出,什么缘由致使该区域内存溢出,出现问题该怎么办。

  使用以下jvm参数:

  

-verbose:gc -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

 

2.4.1java堆溢出

  堆中存放对象实例,只要不停创造对象,且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚对象(将在垃圾回收机制处讲),那么对象达到阈值后天然会产生内存溢出。

  使用参数:-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出时Dump当前的内存堆转储快照,便于过后分析。

示例代码:

public class HeapOOM {
    static class OOMObject {}
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }

    }
}

  解决这个区域的异常,通常经过内存映像分析工具,对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是不是必要的,也就是确认是内存泄漏(Memory Leak)仍是内存溢出(Memory Overflow)。

  注:上面这句也简洁直观地表达了内存泄漏与内存溢出的区别。

  内存泄漏:能够经过工具看泄漏对象到GC Roots的引用链,能够找到泄漏对象时经过怎样的路径与GC Roots相关联并致使垃圾回收器没法自动回收它们。只要掌握了泄漏对象的类型信息及GC Roots引用连的信息,就能够准确地定位出泄漏代码的位置。

  内存溢出:若是对象都必须存活,那么虚拟机的堆参数(-Xmx与-Xms)与机器物理内存对比看是否可以调大;从代码角度看,是否有对象的生命周期过长、持有状态时间过长,以期减小程序运行期间内存的消耗。

2.4.2虚拟机栈以及本地方法栈溢出

  HotSpot中不区分虚拟机栈与本地方法栈,全部-Xoss实际无效,只设置-xss便可。

  对于虚拟机栈和本地方法栈来讲,会出现两种异常:

    线程请求的栈深度大于虚拟机所容许的最大深度,即抛出:StackOverflowError。

    虚拟机扩展栈时,没法申请到足够的内存空间,即抛出:OutOfMemoryError

  其实,当栈空间没法继续分配时,究竟是内存过小,仍是已用栈空间太大,本质都是同一件事情的两种描述。

  注明:在单线程条件下,不管是栈帧过大仍是虚拟机容量过小,都会抛出异常StackOverflowError。

  在多线程条件下,经过不断建立线程的方式是会产生内存溢出的,可是产生内存溢出与栈空间是否足够大无关,在这种状况下,为每一个线程分配的内存越大,越容易栈溢出(总量必定,单次消耗越大,越容易满)。

2.4.3方法区和运行时常量池溢出

  String.intern()是一个Native方法,它的做用是:若是字符串常量池中已经包含了一个等价次string对象的字符串,返回常量池中的该对象,不然将string对象包含的字符串添加到常量池中,返回此string对象的引用。

相关文章
相关标签/搜索