什么是垃圾回收机制
不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收,垃圾收集器在一个 Java 程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用 System.gc 方法来 ” 建议 ” 执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
finalize 方法作用
Java 技术使用 finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
新生代与老年代
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 (Young)、老年代 (Old)。新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
(本人使用的是 JDK1.6,以下涉及的 JVM 默认值均以该版本为准。)
默认的,新生代 (Young) 与老年代 (Old) 的比例的值为 1:2 (该值可以通过参数 –XX:NewRatio 来指定),即:新生代 (Young) = 1/3 的堆空间大小。老年代 (Old) = 2/3 的堆空间大小。其中,新生代 (Young) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 (可以通过参数 –XX:SurvivorRatio 来设定),即:Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
根据垃圾回收机制的不同,Java 堆有可能拥有不同的结构,最为常见的就是将整个 Java 堆分为
新生代和老年代。其中新生带存放新生的对象或者年龄不大的对象,老年代则存放老年对象。
新生代分为 den 区、s0 区、s1 区,s0 和 s1 也被称为 from 和 to 区域,他们是两块大小相等并且可以互相角色的空间。
绝大多数情况下,对象首先分配在 eden 区,在新生代回收后,如果对象还存活,则进入 s0 或 s1 区,之后每经过一次
新生代回收,如果对象存活则它的年龄就加 1,对象达到一定的年龄后,则进入老年代。
常见参数配置
-XX:+PrintGC 每次触发 GC 的时候打印相关日志
-XX:+UseSerialGC 串行回收
-XX:+PrintGCDetails 更详细的 GC 日志
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-XX:SurvivorRatio 用来设置新生代中 eden 空间和 from/to 空间的比例.
-XX:NewRatio 配置新生代与老年代占比 1:2
含以 -XX:SurvivorRatio=eden/from=den/to
总结: 在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
-XX:SurvivorRatio 用来设置新生代中 eden 空间和 from/to 空间的比例.
如何判断对象是否存活
- 引用计数法
引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
首先需要声明,至少主流的 Java 虚拟机里面都没有选用引用计数算法来管理内存。
什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1. 任何时刻计数器值为0的对象就是不可能再被使用的。那为什么主流的 Java 虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。
- 根搜索算法
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 (Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链(即 GC Roots 到对象不可达)时,则证明此对象是不可用的。
那么问题又来了,如何选取 GCRoots 对象呢?在 Java 语言中,可以作为 GCRoots 的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中 JNI(Native 方法)引用的对象。
垃圾回收机制策略
- 标记清除算法
该算法有两个阶段。
- 标记阶段:找到所有可访问的对象,做个标记
- 清除阶段:遍历堆,把未被标记的对象回收
该算法一般应用于老年代, 因为老年代的对象生命周期比较长
标记清除算法的优点和缺点
1. 优点
- 是可以解决循环引用的问题
- 必要时才回收(内存不足时)
2. 缺点:
- 回收时,应用需要挂起,也就是 stop the world。
- 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
- 会造成内存碎片(会导致明明有内存空间, 但是由于不连续, 申请稍微大一些的对象无法做到)
- 复制算法
如果 jvm 使用了 coping 算法,一开始就会将可用内存分为两块,from 域和 to 域,每次只是使用 from 域,to 域则空闲着。当 from 域内存不够了,开始执行 GC 操作,这个时候,会把 from 域存活的对象拷贝到 to 域, 然后直接把 from 域进行内存清理。
coping 算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用 coping 算法进行拷贝时效率比较高。jvm 将 Heap 内存划分为新生代与老年代,又将新生代划分为 Eden(伊甸园) 与 2 块 Survivor Space(幸存者区) , 然后在 Eden –>Survivor Space 以及 From Survivor Space 与 To Survivor Space 之间实行 Copying 算法。不过 jvm 在应用 coping 算法时,并不是把内存按照 1:1 来划分的,这样太浪费内存空间了。一般的 jvm 都是 8:1。也即是说,Eden 区:From 区:To 区域的比例是
始终有 90% 的空间是可以用来创建对象的, 而剩下的 10% 用来存放回收后存活的对象。
1、当 Eden 区满的时候, 会触发第一次 young gc, 把还活着的对象拷贝到 Survivor From 区;当 Eden 区再次触发 young gc 的时候, 会扫描 Eden 区和 From 区域, 对两个区域进行垃圾回收, 经过这次回收后还存活的对象, 则直接复制到 To 区域, 并将 Eden 和 From 区域清空。
2、当后续 Eden 又发生 young gc 的时候, 会对 Eden 和 To 区域进行垃圾回收, 存活的对象复制到 From 区域, 并将 Eden 和 To 区域清空。
3、可见部分对象会在 From 和 To 区域中复制来复制去, 如此交换 15 次 (由 JVM 参数 MaxTenuringThreshold 决定, 这个参数默认是 15), 最终如果还是存活, 就存入到老年代
注意: 万一存活对象数量比较多,那么 To 域的内存可能不够存放,这个时候会借助老年代的空间。
优点: 在存活对象不多的情况下,性能高,能解决内存碎片和 java 垃圾回收算法之 - 标记清除 中导致的引用更新问题。
缺点: 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,coping 的性能会变得很差。
- 标记压缩算法
标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化
任意顺序 : 即不考虑原先对象的排列顺序,也不考虑对象之间的引用关系,随意移动对象;
线性顺序 : 考虑对象的引用关系,例如 a 对象引用了 b 对象,则尽可能将 a 和 b 移动到一块;
滑动顺序 : 按照对象原来在堆中的顺序滑动到堆的一端。
优点: 解决内存碎片问题,缺点压缩阶段,由于移动了可用对象,需要去更新引用。
- 分代算法
这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。
新生代对象朝生夕死, 对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。
- 新生代
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集 - 老年代
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除 - 压缩”算法进行回收。
新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。
老年代区存放 Young 区 Survivor 满后触发 minor GC 后仍然存活的对象,当 Eden 区满后会将存活的对象放入 Survivor 区域,如果 Survivor 区存不下这些对象,GC 收集器就会将这些对象直接存放到 Old 区中,如果 Survivor 区中的对象足够老,也直接存放到 Old 区中。如果 Old 区满了,将会触发 Full GC 回收整个堆内存
Minor GC 和 Full GC 区别
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具
备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常
会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
就有直接进行 Major GC 的策略选择过程)。MajorGC 的速度一般会比 Minor GC 慢 10
倍以上。
Minor GC 触发机制:
当年轻代满时就会触发 Minor GC,这里的年轻代满指的是 Eden 代满,Survivor 满不会引发 GCFull GC 触发机制:
当年老代满时会引发 Full GC,Full GC 将会同时回收年轻代、年老代,当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载其 Minor
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值)来设置