概述
程序计数器、虚拟机栈和本地方法栈三个区域随线程的生而生,随线程的灭而灭,因此这几个区域的内存分配和回收具有确定性;而Java堆和方法区不一样,只有在程序处于运行期才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,因此垃圾收集器关注的也是这部分的内存。
判断对象死亡的方法
一、判断对象是否需要回收的方法
- 引用计数法
给对象添加一个引用计数器,每当一个地方引用时,计数器加1,引用失效是减1,如果为0时表示没有引用;
缺点:无法解决对象之间的循环引用 - 根搜索算法
通过名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何应用链相连(即对象不可达),则证明此对象不可用
二、引用分类
- 强引用:类似“Object obj = new Object()”的引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象;
- 软引用:还有用,但并非必需的对象,对于此类对象在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
- 弱引用:也是描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论内存是否足够,都会被回收。
- 虚引用:不会对引用对象产生任何影响,设置虚应用关联的唯一目的就是希望这个对象被收集器回收时收到一个系统通知。
三、回收时机
如果判断一个对象不可达,这时候处于“死缓”阶段,然后判断此对象是否有必要执行finalize()方法;当对象没有覆盖finalize()方法或finalize()方法已经被虚拟机调用过,则虚拟机就会判定没必要执行finalize()方法,直接回收;否则虚拟机就会触发finalize()方法,finalize()方法是对象逃脱被回收的的最后一次机会,只要与引用链上与任何一个对象建立关联即可,否则就要被回收。
/**
* 代码演示两点:
* 1、对象可以在被GC是自我拯救
* 2、自救机会只有一次,
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Exception {
SAVE_HOOK = new FinalizeEscapeGC();
//第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,暂停0.5秒
Thread.sleep(500);
if(SAVE_HOOK != null){
System.out.println("对象还没有被收回");
}else{
System.out.println("对象已被收回");
}
//第二次成功拯救自己,单却失败了
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,暂停0.5秒
Thread.sleep(500);
if(SAVE_HOOK != null){
System.out.println("对象还没有被收回");
}else{
System.out.println("对象已被收回");
}
}
}
运行结果:
finalize method executed!
对象还没有被收回
对象已被收回
四、回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
废弃常量:没有任何对象引用该常量
无用类:①该类的所有实例都已经被回收;②加载该类的ClassLoader已经被收回;③该类对应的java.lang.Class对象没有在任何地方呗引用
垃圾收集算法
- 标记-清除算法
首先标记出所有需要清除的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:①效率不高②会产生大量不连续的空间碎片 - 复制算法
解决了1的效率问题,将可用内存按容量划分为大小相等的两块,每次只用其中的一块,当 这一块的内存用完了就将还存活的的对象复制到另外一块,然后再把已使用过的内存空间一次清理掉。
- 标记-整理算法
标记过程和“标记-清除”算法一样,但后续不是对可回收对象进行清理,而是让所有存活对象向一端移动,然后清理掉边界以外的内存。
这种算法主要用来回收老年代。 - 分代收集算法
根据对象的存活周期不同将内存划分为几块,这样就可以根据各个年底的特点采用最适当的收集算法;例如Java堆的新生代和老年代的划分。
垃圾收集器
收集算法是内存回收的方法,垃圾收集器是内存回收的具体实现。
并行:多条垃圾收集线程并行工作,但用户线程处于等待状态
并发:用户线程与垃圾收集线程同时执行
吞吐量:运行用户代码时间 / CPU总消耗时间
CPU总消耗时间:运行用户代码时间 + 垃圾收集时间
垃圾收集器参数总结
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收 |
SurvivorRatio | 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor = 8 : 1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效 |
MaxGCPauseMillis | 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效 |
CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效 |
UseCMSCompactAtFullCollection | 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效 |
CMSFullGCsBeforeCompaction | 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效 |
内存分配与回收策略
自动化内存管理:给对象分配内存以及回收分配给对象的内存
Minor GC 和 Full GC 区别:
Minor GC(新生代GC):新生代的垃圾收集动作,发生频繁且回收速度比较快
Full GC(老年代GC):老年代发生的GC,速度比较慢
- 对象优先在Eden分配
-Xms20M、-Xmx20M和-Xmn10M这3个参数表示限制Java堆大小为20M,且不可扩展,其中10M分配给新生代剩余的10M分配给老年代;
-XX:SurvivorRatio=8 表示新生代中 Eden 区域与 Survivor 区域的容量比值为8:1;
因此新生代可用空间为Eden区+1个Survivor 区。 - 大对象直接进入老年代
所谓大对象就是指需要大量连续内存空间的Java对象,例如很长的字符串及数组;
-XX:PretenureSizeThreshold 大于这个参数的对象将直接在老年代分配;
目的是避免在Eden区与两个Survivor 区发生大量内存拷贝(新生代的复制算法)。 - 长期存活的对象将进入老年代
虚拟机为每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后任然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor空间每熬过一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
-XX:MaxTenuringThreshold 每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代 - 动态对象年龄判断
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。 - 空间分配担保
新生代采用复制算法,只是用其中一个Survivor空间作为轮回备份,因此当出现大量对象在Minor GC之后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会进入存活下来,在完成Minor GC之前是无法确定的,所以只好取之前每一次回收晋升到老年代对象容量的平均值作为经验值,与老年代的剩余空间进行比较;如果大于则改为直接进行一次Full GC;如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许则只会进行Minor GC,如果不允许,则也要改为进行一次Full GC;如果在允许担保失败的情况下,某次Minor GC存活后的对象突增,远高于平均值的情况下,导致担保失败,则会在失败后重新发起一次Full GC。