在Java虚拟机运行时数据区中,堆内存是各类内存中最大的一块。堆内存的创建伴随着虚拟机的启动而创建。所有对象实例的创建都是在堆内存中。在Java虚拟机规范中明确的描述了: 所有对象实例以及数组都要在堆上分配内存空间 。垃圾回收的主要区域也是发生在堆内存中。
从内存回收的角度来看,现在的垃圾收集器基本上都采用了分代收集算法,所以,堆内存又可以分为 新生代 和 老年代 。
在为新创建的对象分配内存时,为了 保证并发的安全,在堆空间中会为每个线程创建一个TLAB(本地线程分配缓冲区)区域 ,所以虽然堆内存是线程共享的,但是在内存分配的角度来看,它又能够划分出多个线程私有的TLAB区域。
当然,重点关注的还是堆内存中的新生代与老年代,首先看一下堆空间中新生代与老年代的划分比例:
新生代与老年代的空间大小比例为1:2,而在新生代中还划分了三个区域: Eden区、Surivivor From区、Surivivor To区 。它们之间的比例分别为8:1:1。下面来详细分析不同年龄代的区域。
新生代主要作用是用来放置新创建的对象,任何一个对象初次创建时都会被分配至此区域。在新生代中,98%的对象都是"朝生夕死"的,并不需要一比一来划分内存空间,而是讲内存空间划分为两个大的区域: Eden区、Survivor区(其中,Surivivor区域又被划分成两个Survivor From区,Survivor To区域) 。每一次内存分配都是使用Eden区和Survivor区域中的一块。既然划分了三个区域,那么就来说说对象在这三个区域中怎么流转的。
强调的一点是, 新生代中,只会Eden区和Survivor区域其中的一块被同时使用 ,另一块Survivor区域始终为空的。
上面说到,Survivor区域中,始终会有一块Survivor区域被空置,那么在有限的堆内存中,岂不是造成了内存的浪费。首先了解一下划分出Survivor区域的意义在哪里。
试想如果没有划分Survivor区域,那么Eden区每进行一次Monir GC,都会将对象直接送入老年代,老年代将会很快被填满,从而触发Major GC。Major GC的执行效率相对Monir GC来说效率慢了十倍以上。那么与有Survivor区的情况相比,Major GC触发的频率则会相对提高,严重的将会影响到程序执行及相应的速度。
如果存在Survivor区域,它可以作为一个缓冲区,当Eden区触发Monir GC后,对象不直接送往老年代,而是复制到Survivor区,相对来说就可以降低触发Major GC的机率。所以,Survivor区存在的根本意义是: 减少对象被送往老年代的频率,从而减少Major GC和Full GC的发生,Survivor可以保证在经历了16次Monir GC还能在新生代存活的对才会被送到老年代 。
另外一个方面就是Survivor区有效的 解决了内存的碎片化 。回顾之前说到的新生代对象流转流程。新创建的对象都被分配在Eden区,一旦该区域的内存满了,会触发Monir GC,然后对象会被从Eden送至Survivor区,往复循环。由此,问题来了,在进行Monir GC时,Eden和Survivor区都有一切存活的对象,此时将Eden取中的对象强行存放到Survivor区时,明显两部分的对象所占用的内存空间是不连续的,也就导致了内存的碎片化。
内存碎片化最终的结果就是会严重影响到程序的性能。试想当堆空间被散布的对象占据了不连续的内存,当有一个内存需求较大的对象被创建的时,堆中可能就没有足够大的连续内存空间来分配给该对象,那么就会触发Full GC。
如果将Survivor区划分成两块。在Eden区刚刚创建新对象时,经历一次Monir GC,Eden区存活的对象就被被复制移动到第一块Survivor区中,Eden被清空,等Eden区再满了的时,再次触发Monir GC,Eden区和第一块Survivor区存活的对象会被复制移动到第二块Survivor区。Eden和第一块Survivor区域被清空。
所以,为什么要划分两个Survivor区呢?
老年代主要存放的是经历过几次垃圾回收之后还存活的对象,刚刚在说到新生代对象的复制转移的时候,当被标记了16次的对象如果还存活着,就会被送入到老年代。
另外一种就是较大的对象,较大的对象也就被直接送入到老年代中。
不怕路歹行不怕大雨淋,心上一字敢 面对我的梦,甘愿来作憨人。 --<憨人>