上次简单记录了jvm的五大模块,着重了解了下栈的机制,这次来学习jvm的核心——堆
堆的作用
1、堆是jvm管理内存最大的一部分,一般用来存储对象和数组(每个对象都包含一个与之对应的class的信息(class信息存放在方法区)),
2、是一块线程共享的区域,
3、堆是垃圾回收器管理(GC)的主要区域。新生代、老生代、永久代的概念就是在堆里堆内存可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
堆的分代
先上图,堆分成年轻代、老年代和永久代。
为什么分代?
1、给堆内存分代是为了提高对象内存分配和垃圾回收的效率
2、新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中。
3、根据不同年代的特点可以采用合适的垃圾收集算法。
4、新生代和老年代是垃圾回收的主要区域。
可以用网上很火的一段话来描述一个对象在堆中面临的生命周期
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收
总结而言就是:
对象在堆中的生命周期:
年轻代回收的步骤:1,在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
2,紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,
3,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
4,经过这次GC后,Eden区和From区已经被清空。
这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。
不管怎样,都会保证名为To的Survivor区域是空的。
5,Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
1个Eden区和2个Survivor区(分别叫from和to)。默认比例为eden:from:to = 8:1:1
为什么是这个比例呢?
因为Eden区中的对象都是朝生夕死的,一次minor GC会回收掉超过90%的对象,此时进入Survivor区的对象已经很少了。
且比较高效,
统计学测算出超过98%以上的对象是一次就会被minor gc时回收的。然GC时9层丢掉,剩下的放在S1,所以给了1层.然后结束后S1肯定要放回给S0区,也就是对调。这也是为什么会多S0出来。这样比例就是8:1:1了
除了以上几种年龄增加的进入指定分区,还有以下几种特例:
1、动态年龄判断:当新的对象进入交换区时,交换区已经容放不下,会判断大于某个年龄的对象直接进入老年代
2、当对象足够大时(新生代放不下时),直接进入老年代。
下面再来讨论几种GC的算法
Minor GC、Major GC和Full GC之间的区别
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
Major GC 是清理老年代。
Full GC 是清理整个堆空间—包括年轻代和老年代。
标记清除算法
见图,这种算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
标记清除算法(效率100,但产生内存碎片(可用的空间不是连续的))
它的主要缺点有两个:
(1)效率问题:标记和清除过程的效率都不高;
(2)空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。
复制回收算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
它的主要缺点有两个:
(1)效率问题:在对象存活率较高时,复制操作次数多,效率降低;
(2)空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)
标记整理算法
标记整理算法主要是在标记清除算法上做了优化,标记存活对象->清除垃圾->整理存活对象。标记出所有能存活的对象,然后这些存活的对象向内存的某一端移动。
最后简单写个main函数,说下其在jvm中的生命周期
public class A{
public int i=1;
public static void mian(String args[]){
A a=new A();
}
}
1.加载class文件到class内容区域,加载静态方法和静态变量到静态区(同时加载的)
2.调用main方法到栈内存
3.在栈内存中为a变量(A对象的引用)开辟空间
4.在堆内存为A对象申请空间
5.给成员变量进行默认初始化(此时 i=0),同时有一个方法标记,在方法区中创建一个A的方法区,将A的方法区的地址0x01给方法标记
6.给成员变量进行显示初始化(此时 i=1)
7.将A对象的地址值给变量a
堆大小分配
-Xms; 堆的最小值
-Xmx; 堆的最大值
-Xmn; 新生代大小
-XX:NewSize;新生代最小值
-XX:MaxNewSize 新生代最大值
为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
内存分配与回收策略
1、对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC
2、大对象直接进入老年代,大对象:需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。
-XX:PretenureSizeThreshold 大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。
长期存活的对象将进入老年代,默认15岁,
动态年龄判断-XX:MaxTenuringThreshold 调整动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。