1.JVM自动内存管理机制

 

  1. 运行时数据区域

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

  1.1 程序计数器

    比较小的内存空间,当前线程所执行的字节码的行号指示器。独立存储,线程私有。无OOM情况

  1.2  Java虚拟机栈

    线程私有,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出站的过程。

    局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型)和returnAddrerss类型(指向了一条字节码指令的地址)。long和double类型的数据占用2个局部变量空间(Slot),其余的占用1个。局部变量表所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。

    两种异常情况:如果线程请求的栈的深度大于虚拟机多允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。

  1.3 本地方法栈

    与虚拟机栈所发挥的作用是非常相似的,区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法区域也会抛出StackOverflowError和OutOfMemoryError异常。

  1.4  Java堆

    Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。被所有线程共享的一块内存区域,虚拟机启动时创建。唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(非绝对,栈上分配,标量替换)

    Java堆是垃圾回收器管理的主要区域,也被称为“GC堆”。

    异常:OOM;

  1.5 方法区

    与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。又被称为“永久代”。可进行内存回收,主要针对常量池的回收和对类型的卸载。

    异常:OOM  

  1.6 运行时常量池

    方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

    异常:OOM

  1.7 直接内存

    直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。这部分内存会被频繁的使用,也有可能导致OOM。

    Jdk1.4 中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,他可以是用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

    显然,本机直接内存的分配不会受到Java堆大小的限制,但是最为内存,还会受到本机总内存大小以及处理器寻址空间的限制。可能会导致OOM异常。

 

  2.HotSpot虚拟机对象


  2.1对象的创建(new关键字创建对象的过程)
    虚拟机遇到一条new指令时,先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的泪是否已被加载、解析和初始化。若没有,则必须先执行类的加载。
    

    类加载通过后,将为新生对象分配内存。对象所需的内存在类加载完成后便完全确定,为对象分配内存等同于把一块确定大小的内存从Java堆中划分出来。两种分配方式,指针碰撞(内存规整)和空闲列表(内存不规整)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器决定。


    除划分空间外,还需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。解决这种问题有两种方案,一种是对分配内存空间的动作进行同步处理-虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。


    内存分配完成,虚拟机需要将分配的的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋予初始值就直接使用。


    接下来,虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、独享的哈希码、对象的GC分带年龄等信息。这些信息存放在对象的对象头中。


    上面工作都完成之后,执行<init>方法,把对象按照程序员的设置进行初始化,这样一个真正可用的对象才算完全产生出来。

  2.2 对象的内存分布
    对象在内存中存储的布局可以分为3块区域:对象头、示例数据和对齐填充。


    对象头包括两部分信息,第一部分信息用于存储对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称“Mark Word”。
    对象头的另外一部分是类型指针,即对象指向他的类元数据的指针,虚拟机可以通过这个指针来确定这个对象是哪个类的实例。若对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为Java对象的大小可以通过对象的元数据信息确定,但数组的元数据中却无法确定数组的大小。
    

    实例数据部分是对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录。

    对齐填充并不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象头正好符合,因此,当对象实例没有对齐时,就需要对齐填充来补全。

  2.3 对象的访问定位
  使用句柄:Java堆中将会划分出一块内存作为句柄池,栈中reference中存储的就是对象的句柄地址,而句柄中包含了对象的示例数据与类型数据各自的具体地址信息。优点,对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改。

  指针访问:Java堆对象的布局中就要考虑如何防止访问类型数据的相关信息,而reference中存储的直接就是对象地址。节省了一次指针寻址,速度快

  

   2.4 配置及异常

    -Xms 堆的最小值

    -Xmx 堆的最大值

    -Xss  栈容量(虚拟机栈与本地方法栈)

    -XX:PermSize 方法区

    -XX:MaxPermSize 方法区最大容量

 

    java.lang.OutOfMemoryError: Java heap space  堆内存溢出

    java.lang.OutOfMemoryError: PermGen space  方法区溢出

   

  3.垃圾收集器与内存分配策略

   回收主要针对堆内存和方法区,第一件事就是确定这个对象哪些还活着,哪些已经死去。

    3.1 对象已死吗

    3.1.1 引用计数法

      给对象添加一个引用计数器,每当有一个地方引用时,计数器+1;引用失效时,计数器值-1。任何时刻计数器为0的对象就是不可能再被使用。缺点:它很难解决对象之间相互循环引用的问题。

    3.1.2 可大型分析算法

      在主流的商用程序语言的主流实现中,都是通过可达性分析来判断对象是否存活的。这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

 

      Java语言中,可作为GC Roots的对象包括以下几种:

      虚拟机栈(栈帧中的本地变量表)中引用的对象。

      方法区中类静态属性引用的对象。

      方法区中常量引用的对象。

      本地方法栈中JNI引用的对象。

    3.1.3 引用

      JDK1.2以前,Java中的引用定义很传统·:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

      JDK1.2之后,对引用进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,强度依次逐渐减弱。

      强引用:代码中普遍存在的,类似“Object obj = new Object()”这类引用,只要引用还在,垃圾回收期永远不会回收掉被应用的对象。

      软引用:有用但非必要的对象。系统将要发生内存溢出异常之前,将会把这些对象列入可回收范围内进行第二次回收。

      弱引用:描述非必需的对象。只能生存在下一次垃圾回收之前。

      虚引用:唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

    3.1.4 回收方法区

      主要回收两部分:废弃常量和无用的类。收益较低。

      该类的所有实例已被回收

      该类的类加载器已被回收

      该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

 

  3.2 垃圾收集算法

    3.2.1 标记-清除算法

      算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。不足点:一个效率问题;另一个空间问题,标记清除后会产生大量不连续的内存碎片。

       

    3.2.2复制算法

      为解决效率问题,“复制”算法出现了。它可以将内存分为大小相等的两块,每次只是用其中的一块。当这块的内存用完了,就将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。

      现在的商用机都采用这种手机算法来回收新生代。因为新生代的对象大部分都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot默认Eden和Survivor的大小比例是8:1。当Survior空间不够用时,需要依赖老年代进行分配担保。

    3.2.3 标记-整理算法

      复制算法在对象存活率较高时要进行较多的复制操作,效率很低,还浪费空间。所以老年代一般不能直接采用这种算法。

      标记-整理算法,标记过程仍与“标记-清除”算法一样。但是后续不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

 

 

     3.2.4 分代收集算法

 

  3.3 HotSpot的算法实现

    GC停顿 、安全点、安全区域 

 

posted @ 2018-11-22 17:55  hws2017  阅读(194)  评论(0编辑  收藏  举报