来自:
成为JavaGC专家(1)—深入浅出Java垃圾回收机制
Java垃圾回收机制
1.数据划分
(1)虚拟机栈:用来存放一些局部变量、方法出口等,生命周期随着程序的结束而结束。
(2)堆:对于引用类型的实例和数组都在栈上分配,java垃圾回收机制就是对堆中的内存进行回收。
(3)方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等,这个区域内存回收的目标主要是对常量池的回收和类型的卸载,回收的内存比较少,所以也有称这个区域为永久代(PermanentGeneration)的。
(4)运行时常量池:运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
(5)程序计数器:程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。
(6)本地方法栈:为虚拟机使用到的Native方法服务
2.如何确定对象被回收
(1)引用计数器
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象+1),但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
(2)根搜索算法
该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
jvm对不可用的对象进行回收,哪些对象是可用的,哪些是不可用的?Java并不是采用引用计数算法来判定对象是否可用,而是采用根搜索算法(GCRoot Tracing),当一个对象到GCRoots没有任何引用相连接,用图论的来说就是从GC Roots到这个对象不可达,则证明此对象是不可用的,说明此对象可以被GC。对于这些不可达对象,也不是一下子就被GC,而是至少要经历两次标记过程:如果对象在进行根搜索算法后发现没有与GC Roots相连接的引用链,那它将会第一次标记并且进行一次筛选,筛选条件是此对象有没有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用执行过一次,这两种情况都被视为没有必要执行finalize()方法,对于没有必要执行finalize()方法的将会被GC,对于有必要有必要执行的,对象在finalize()方法中可能会自救,也就是重新与引用链上的任何一个对象建立关联即可。
3.常用的垃圾收集算法
(1) 标记-清除算法
这种垃圾收集算法思路非常简单,主要是首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
但是有一个明显的缺点,采用这种算法之后会发现内存块回收之后就不连续了,这就导致了在下一次想分配一个大内存块的时候无法分配。
(2)标记-清除-压缩
这种垃圾收集算法主要是对上面的算法进行了优化,内存回收了对内存进行了一次优化压缩。这样回收后内存块的连续性又比较强了。
但是这种算法会涉及到不停的内存间的拷贝和复制,性能会非常差。
(3)标记-清除-复制
这种算法会将内存空间分配成两块相同的区域A和B。当内存回收的时候,将A中的内存块拷贝到B中,然后一次性清空A。
但是这种算法会对内存要求比较大一些,并且长期复制拷贝性能上也会受影响。
(4)分代收集算法
Java主要采用了分代收集算法。分代收集算法主要将对象存活期的长短将内存进行划分。
Java主要将内存划分为两部分:新生代和老生代
Java的新生代中,对象的存活率低,存活期期会相对会比较短一些,所以可以选用复制算法来进行内存回收。
Java的老生代中,对象的存活率比较高,并且相对存活期比较长一些,可以采用标记-清除-压缩的算法来进行内存回收。
新生代(Young generation): 绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minorGC“。新生代是用来保存那些第一次被创建的对象,他可以被分为三个空间
· 一个伊甸园空间(Eden )
· 两个幸存者空间(Survivor )
一共有三个空间,其中包含两个幸存者空间。每个空间的执行顺序如下:
1.绝大多数刚刚被创建的对象会存放在伊甸园空间。
2.在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。
3.此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
4.当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。
5.在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。
老年代(Old generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC“(或者”fullGC“)
老年代空间的GC事件基本上是在空间已满时发生,执行的过程根据GC类型不同而不同。
持久代( permanent generation )也被称为方法区(method area)。他用来保存类常量以及字符串常量。因此,这个区域不是用来永久的存储那些从老年代存活下来的对象。这个区域也可能发生GC。并且发生在这个区域上的GC事件也会被算为major GC。
4.垃圾收集器
(1).Serial 收集器串行
(2).ParNew收集器并行
(3). Prallel Scavenge收集器并行
(4).Serial Old收集器串行
(5). Parallel Old 收集器并行
(6). CMS收集器
5.内存分配策略
(1) 优先在Eden上分配。
Java的对象优先会在新生代的Eden上分配。
(2) 大对象直接进入老生代。
(3) 长期存活的对象进入老年代
(4) 动态对象年龄判定
为了使内存分配更加灵活,虚拟机并不要求对象年龄达到MaxTenuringThreshold才晋升老年代
如果Survivor区中相同年龄所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象在MinorGC时将复制至老年代
(5) 空间分配担保
新生代使用复制算法,当MinorGC时如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,以完成Minor GC。
在触发Minor GC时,虚拟机会先检测之前GC时租借的老年代内存的平均大小是否大于老年代的剩余内存,如果大于,则将Minor GC变为一次Full GC,如果小于,则查看虚拟机是否允许担保失败,如果允许担保失败,则只执行一次Minor GC,否则也要将Minor GC变为一次Full GC。