JVM-内存模型

控制参数:

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -Xmn这只新生代的空间大小。(for jdk 1.4 or later)
  • -XX:NewSize设置新生代最小空间大小。(for jdk 1.3/1.4)
  • -XX:MaxNewSize设置新生代最大空间大小。(for jdk 1.3/1.4)
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小。

  没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。

老年代空间大小 = 堆空间大小 - 年轻代大空间大小

程序计数器(Program Counter Register):

  程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。

    由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行

    位置,每条线程都需要有一个独立的程序计数器,

    各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

   如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

   此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

堆(Heap):

  Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

   此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

      堆分为新生代(Young generation)老年代(Old generation),新生代又分为Eden空间、From Survivor空间、To Survivor空间。

   新生代(Young generation):的绝大多数最新被创建的对象会分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”Minor GC

   老年代(Old generation):对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中

                消失的过程,我们称之为”Major GC“(或者”Full GC“)

      根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx-Xms控制)。

      新生代的构成(如图):

 

                      

 

                               ( from ---> s1、to ---> s2 )

   一共三个空间,其中包含两个幸存者空间,每个空间的执行顺序如下:

   1、大多数刚刚被创建的对象会存放在伊甸园空间。

   2、在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。

     3、此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。

     4、当一个幸存者空间饱和,还存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。

     5、在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

     Survior区域的作用在于避免过早出发Full GC,如果没有Survior,Eden每进行一次Minor GC,就会还存活的对象直接送入老年代,老年代很快会内存不足触发一次Full GC。Survior区域分为两块的目的是为了提高性能,

     避免内存碎片的出现

                 如果你仔细观察这些步骤就会发现,其中一个幸存者空间必须保持是空的。如果两个幸存者空间都有数据,或者两个空间都是空的,那一定标志着你的系统出现了某种错误。

    通过频繁的minor GC将数据移动到老年代的过程可以用下图来描述:

                              

   需要注意的是HotSpot虚拟机使用了两种技术来加快内存分配。他们分别是是”bump-the-pointer““TLABs(Thread-Local Allocation Buffers)”

     Bump-the-pointer技术跟踪在伊甸园空间创建的最后一个对象。这个对象会被放在伊甸园空间的顶部。如果之后再需要创建对象,只需要检查伊甸园空间是否有足够的剩余空间。

   如果有足够的空间,对象就会被创建在伊甸园空间,并且被放置在顶部。这样以来,每次创建新的对象时,只需要检查最后被创建的对象。这将极大地加快内存分配速度。但是,如果

   我们在多线程的情况下,事情将截然不同。如果想要以线程安全的方式以多线程在伊甸园空间存储对象,不可避免的需要加锁,而这将极大地的影响性能。

     TLAB是HotSpot虚拟机针对这一问题的解决方案。该方案为每一个线程在Eden空间分配一块独享的空间,这样每个线程只访问他们自己的TLAB空间,再与bump-the-pointer技术结合可以在不加锁的情况下分配内存。

            从本质上讲,TLAB管理是依靠三个指针:start、end、top 。start和end标记了Eden被TLAB管理的区域,该区域不会被其他线程分配内存使用,top是分配指针。开始时指向start的位置,随着内存分配的进行,

         慢慢向end靠近,当撞上end时触发TLAB refill,因此内存中Eden的结构大体为:

                                                                                          

     堆内存参数设置:  -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

   解释:-Xms 堆内存最小值, -Xmx 堆内存最大值,-XX:+HeapDumpOnOutOfMemoryError  输出Dump 内存堆转存快照  可以用 Eclipse Memory Analyzer工具来分析

   如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

      如果老年代的对象需要引用一个新生代的对象,会发生什么呢?

     为了解决这个问题,老年代中存在一个”card table“,他是一个512 byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,

     只需要查询card table来决定是否可以被收集,而不用查询整个老年代。这个card table由一个write barrier来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,

        但GC的整体时间被显著的减少。

方法区(Method Area):

     方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

     程序员喜欢把方法区称作"永久代(Permanent Generation)",本质上两者并不等价。

     相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确

      实是有必要的。方法区的GC事件也是MajorGC(Full GC)

虚拟机栈(VM Stack):

     与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。

   每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

     局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象

  相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空

  间是完全确定的,在方法运行期间不会改变局部变量表的大小。

          在Java虚拟机规范中,对这个区域规定了两种异常状况:(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;(2)如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请

   到足够的内存时会抛出OutOfMemoryError异常。(栈容量只由 -Xss 参数设定)

本地方法栈(Native Method stack):

   本地方法栈和虚拟机栈的区别:虚拟机栈为java虚拟机执行方法(字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError异常 和 OutOfMemoryError异常

哪儿的OutOfMemoryError:

   Exception in thread “main”: java.lang.OutOfMemoryError:Java heap space

  原因:堆中没有内存完成对象实例分配,并且堆无法再扩展时将抛出OOM。

  Exception in thread “main”:java.lang.OutOfMemoryError:PermGen space

  原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库。

       Exception in thread “main”:java.lang.OutOfMemoryError:Requested array size exceeds VM limit

       原因:创建的数组大于堆内存的空间

  Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

       原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。

  Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)

  原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现

参考:

【1】个人博客,纯洁的微笑,http://www.cnblogs.com/ityouknow/p/5610232.html

【2】《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明

【3】ImportNew,http://www.importnew.com/1993.html

【4】博客,http://www.cnblogs.com/QG-whz/p/9636366.html

posted @ 2018-04-10 18:45  寻找风口的猪  阅读(276)  评论(0编辑  收藏  举报