详解Java中的堆内存
堆是JVM运行数据区中的一块内存空间,它是线程共享的一块区域(注意了!!!),主要用来保存数组和对象实例等(其实对象有时候是不在堆中进行分配的,想要了解的可以看我之前写的博文:小白秒懂什么是栈上分配)。在堆中内存空间不足以进行分配时,就会出现OutOfMemoryError(OOM)
异常。
在JDK7和JDK8中,堆中的内存结构是不同的:
那在JDK8中的永久代(方法区)为什么没有了那?
它被移动到了本地内存中,被称为元空间。
那又有一个疑问了:为什么要把堆内存中的永久代移动到本地内存中作为元空间?
其实就是为了避免OOM
异常。
年轻代又被分为三部分:Eden区
和两个Survivor区
。两个Survivor区大小是完全相同的,被称为from区
和to区
。Eden区和from区、to区的比例为8 : 1 : 1。
- 当有一个对象新创建后,其首先会被分配到Eden区(部分对象不会首先分配到Eden区,后面会说),当Eden区内存不足时,会标记Eden区中存活的对象,进行垃圾回收,并把存活的对象移动到from区。
- 当Eden区内存再次不足时,再次GC,把Eden区和from区中存活的对象通过复制算法移动到to区。
- 再次不足时,GC,通过复制算法将Eden区和to区中存活的对象移动到from区。
- 就这样一直移动,当一个对象移动了15次,就被将其分配到老年代。
这就是分代回收。
但有些时候对象的除此创建并不会首先被分配到Eden区,这种对象就是大对象,也就是占用大量连续内存空间的对象,其会被直接分配到老年代。
这里可能大家还会有一个疑问:为什么对象移动了15次,就会将其分配给老年代?移动的次数又是存储在哪里的?
这里就又要说到对象的内存结构了。 在HotSpot
虚拟机中,对象在内存中的存储被分为三个部分。如图:
对象头又被分为:
在MarkWord中,就存储了对象的信息(下面只列了部分):
- age:对象分代年龄,占4位。
- hashcode。
- biased_lock:偏向锁标识,1位。(好奇什么是偏向锁的朋友,可以搜一下锁升级,也就是
synchronized
这个锁的知识,我这里不深入聊锁) - 等等…
age就是我们对象的GC年龄,每次移动,都会加1。age占4位,也就是2^4 - 1 = 15
。所以这就是分代年龄为什么是15次,因为它能存储的最大数值是15。
JVM也提供了参数去设置分代年龄的大小,但都不能超过15。