这里所说的分配机制,主要是指在堆上的分配机制,一般的,对象的内存分配都是在堆上进行的,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本数据类型或String类型),然后在栈上分配,在栈上分配的很少见。
java内存的分配和回收机制概括的话,就是:分代分配和分代回收。对象会根据存在的时间分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Parmenant Generation,也就是方法区)。
年轻代(Young Generation):对象被创建时,内存的变化主要发生在年轻代(大对象可以直接被创建在老年代中),大部分的对象在创建之后很快就不再使用,应此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC 或者Young GC 。注意,Minor GC 并不代表年轻代内存不足,它事实上只表示在Eden区上的GC.
年轻代的内存是这样分配的:年轻代可以分为三个区域:Eden区(内存首次分配的区域)和两个存活区(Survivor0,Survivor1),内存分配过程:
1、绝大多数刚刚创建的对象会被分配到Eden区,其中大多数对象会很快消亡。Eden区是连续的内存空间,应此在其上分配内存空间是极快的。
2、最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象处理掉,并将存活的对象复制到一个存活区中Survivor0(此时Survivor1是空的,两个存活区中始终有一个为空白的)。
3、下次Eden区再满的时候,再执行一次Minor GC,将消亡的对象处理掉,并将存活的对象复制到一个存活区中Survivor1,然后清空Eden区。
4、将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区中,将存活的对象赋值到Survivor1中,然后清空Survivor0区。
5、当两个存活区切换几次(HotSpot虚拟机默认15次)之后,仍然存活的对象,将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。
年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。