JVM的内存分配策略

1、内存分配有哪些策略

从编译原理讲起,不同的开发环境、开发语言会有不同的策略。一般程序运行时有三种内存分配策略:静态的、栈式的、堆式的。

 

静态存储分配:是指在编译时就能够确定每个数据目标在运行时的存储空间需求,所以在编译时就可以给它们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间。

 

栈式存储分配:栈式存储分配是动态存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反。在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到了运行的时候才能知道,但是规定在运行中进入一个程序模块的时候,必须知道该程序模块所需要的数据区的大小才能分配其内存。在变量在编译时已经确定大小但是在程序运行时才生成。比如函数中的变量,参数,只有等函数被调用了才生成。但是大小是在编译时就已知了。

 

堆式存储分配:堆式存储分配专门负责在编译时或运行时,对无法确定存储要求的数据结构进行内存分配。比如可变长度串和对象实例,堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

 

对应的java三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

 

静态存储区:或者叫主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

 

栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

 

堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

 

    现在这些new出来的对象是分配在堆中的,但是堆又可以划分为更小的区域以便进行垃圾回收。

    一:对象优先分配在线程的本地分配缓冲区:在前面我们提到,每个线程可以在堆中预先分配得到一片区域,作为本地线程分配缓冲区(TLAB)。当该线程执行时,有对象创建的话,就在该线程的TLAB中分配内存。当该线程的TLAB用完了才申请堆中的空闲内存。

   

    二:堆中优先分配Eden:大多数情况下,对象都在新生代的Eden区中分配内存。而因为大部分的对象都是“朝生夕死”的,所以新生代又会频繁进行垃圾回收。将JVM内存划分为一块较大的Eden空间(80%)和两块小的Servivor(各占10%)。当回收时,将Eden和Survivor中还存活的对象一次性采用复制算法直接复制到另外一块Servivor空间上,最后清理到院Eden空间和原先的Survivor空间中的数据。 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC。 在这里先说明两个概念:

  • 新生代GC/小垃圾收集(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多是具有朝生夕灭的特性,所以Minor GC非常频繁,而且该速度也比较快,所有的Minor GC都会引发STW,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。如果大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,则会导致Minor GC 执行时暂停的时间将会长很多。 
  • 老年代GC/主要垃圾收集(Major GC):指发生在老年代的GC,出现了Major GC,一般可能也会伴随着一次Minor GC,但是与Minor GC不同的是,Major GC的速度慢十倍以上。
  • 完全GC(Full GC):指的是对整个堆空间的的清理,包括年轻代和老年代甚至永久代(在JDK8以后取消了永久代改为元空间),也就是包含了Minor GC和Major GC。

        其中关键的一点区别是只有Full GC会清理永久代,Minor GC不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。 

    Minor GC触发机制: 当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。 
    Full GC触发机制: 
     (1)调用System.gc时,系统建议执行Full GC,但是不必然执行 
     (2)老年代空间不足 
     (3)方法区空间不足 
     (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存 
     (5)由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

     (6)当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。 

 

    三:大对象直接进入老年代:需要大量连续空间的对象,如:长字符串、数组等,会直接在老年代分配内存。这是因为,这样可以避免在新生代区频繁的GC时发生大量的内存赋值(新生代的GC是采用复制算法的)。如果我们将大对象分配在新生代中,那样子的话很容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。为了避免上述情况的经常发生而导致不需要的GC活动所浪费的资源和时间,可采用的分配策略是将大对象直接分配到老年代中去,虚拟机中也提供了-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代里面分配内容。

 

    四:长期存活的对象进入老年代:新生代中经历了多次GC仍然存活的对象,当年龄达到一定程度(默认15)时就会晋升到老年代。为了更好地适应内存情况,虚拟机不是要求对象必须到达阀值才可晋升老年代的,而是采用动态年龄判定的方法:如果Servivor空间中相同年龄的对象大小大于Servivor空间的一半时,由于下一次的MinorGC时,这些对象如果仍然存活的话,复制到ToServivor空间时就放不下了。所以,在本次GC时就可以把这些对象以及年龄大于等于这些对象的直接进入老年代。在MinorGC时,如果Eden和FromServivor中存活的对象在复制到ToServivor时放不下了,也会直接分配到老年代。

   

    五:空间分配担保:在MinorGC之前,会先检查老年代最大可用空间是否可以容纳新生代所有对象(防止新生代全部晋升时放不下),如果可以容纳,则MinorGC可以安全执行。否则,检查是否允许担保失败,否则检查老年代最大可用空间是否大于历次晋升到老年代的对象的平均大小,是则尝试进行MinorGC;小于或者MinorGC失败,则会发起一次FullGC清理老年代。在发生Minor GC之前,虚拟机会先检查老年代中最大的可用的连续空间是否大于新生代中所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFaiure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次GC是有风险的;如果小于,或者HandlePromotionFaiure设置不允许冒险,那么这时就要改为进行一次Full GC。

 

 

展开阅读全文

没有更多推荐了,返回首页