您的位置:首页 > 其它

到底谁是辣鸡?(对象是否存活和GC日志分析)

2017-10-18 17:47 246 查看

垃圾收集要搞清楚的三件事(除了方法区回收其他都针对对象也就是堆区的回收)

垃圾收集(Carbage Collection,GC),垃圾收集需要考虑三件事:

1.哪些内存需要回收

方法计数器、虚拟机栈、本地方法栈三个区域随着线程生和灭,每一个栈帧所分配的内存在编译器大体上都是可知的,内存的回收和分配都具备确定性,所以不需要过多的考虑回收问题,因为方法结束或者是线程结束内存自动就会回收了。

方法区和堆区(垃圾收集区域需要回收)的内存分配则不一样,接口的多个实现、方法的多个分支占用的内存都可能不一样,只有在运行期才知道哪些对象会创建,这部分的内存回收和分配都是动态的,所以垃圾收集器关注的是这部分内存。

2.什么时候回收

对象死亡时回收(不被任何途径使用)。

3.如何回收

垃圾收集算法。

判断对象是否存活(什么时候回收和GC日志开启方式以及日志分析)

1.引用计数法

原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被再使用的。

优点:实现简单、判定效率高。

缺点:对象间相互循环引用问题无法解决,

在下边的栗子里objA和objB的instance字段相互持有对方的引用,除此之外没有别的任何引用,而且两个对象已经不可能被访问,但是相互引用着对方,计数器都不是0,于是引用计数法无法告知GC去回收内存。

举个官方栗子:

public class GCObj{
public Object instance = null;
private static final int _1MB = 1024*1024;
//判断对象的这部分内存是否会被回收,判断是不是使用的引用计数法
private byte[] isGCByte = new byte[2*_1MB];

public static void main(String[] args) {
GCObj objA = new GCObj();
GCObj objB = new GCObj();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假设在此GC,objA和objB是否会被回收
System.gc();
}
}




[GC (System.gc()) [PSYoungGen: 7270K->664K(36864K)] 7270K->664K(121856K), 0.0181725 secs] [Times: user=0.02 sys=0.00, real=0.03 secs]

[Full GC (System.gc()) [PSYoungGen: 664K->0K(36864K)] [ParOldGen: 0K->616K(84992K)] 664K->616K(121856K), [Metaspace: 3155K->3155K(1056768K)], 0.0915562 secs] [Times: user=0.11 sys=0.00, real=0.09 secs]


观察日志可分析出(此处涉及到GC日志的分析,以后单独介绍),虚拟机并没有因为两个对象相互引用就不回收他们,这也说明虚拟机没有使用引用计数法判断对象存活。

GC日志分析:

首先如何让代码运行时打印GC日志:



点击Edit Configurations…



然后设置VM optinals参数:
-XX:+PrintGCDetails


新生代老年代,GC,对内存分配

堆设置

-Xms :初始堆大小

-Xmx :最大堆大小

-XX:NewSize=n :设置年轻代大小

-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n :设置持久代大小 收集器设置

-XX:+UseSerialGC :设置串行收集器

-XX:+UseParallelGC :设置并行收集器

-XX:+UseParalledlOldGC :设置并行年老代收集器

-XX:+UseConcMarkSweepGC :设置并发收集器 垃圾回收统计信息

-XX:+PrintHeapAtGC GC的heap详情

-XX:+PrintGCDetails GC详情

-XX:+PrintGCTimeStamps 打印GC时间信息

-XX:+PrintTenuringDistribution 打印年龄信息等

-XX:+HandlePromotionFailure 老年代分配担保(true or false) 并行收集器设置

-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间

-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置

-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

更多设置参考:IDEA和Eclipse GC日志分析打印开启方法和参数设置



针对以上书中的例子来具体分析:

33.125 和 100.667 代表了GC发生的时间,这个数字代表从Java虚拟机启动以来经过的秒数。

[GC 和[Full GC 代表了GC的停顿类型并不是来区分GC发生在新生代还是老年代的,如果有Full说明本次GC发生了Stop The World (简称STW)的停顿。



ParNew收集器中的GC也出现Full,这一般是出现了担保失败之类的问题出现了STW

如果是调用System.gc()方法触发的GC,则如我们上边运行的程序中的那样,[GC (System.gc()) ,[Full GC (System.gc())

[DefNew(新生代)、[Tenured(老年代)、[Perm(永久代)表示GC发生的区域

在Serial收集器中显示[DefNew(Default New Generation)

ParNew收集器中显示[ParNew(Parallel New Generation)

Parallel Scavenge收集器中显示[PSYoungGen

老年代和永久带一致都是跟收集器决定的

后边方括号中的3324k->152k(3712k)含义是:“GC前该内存区域使用量->GC后该内存区域使用量(该内存区域总容量)”

方括号之外的3324k->152k(11904k)的含义是:“GC前Java堆的已使用量”->GC后Java堆的已使用量(Java堆的总容量)

再往后,0.0025925secs表示此次GC所占时间单位是秒,有的收集器(需要在VM optional中设置)可以显示更具体的时间数据,如:[Time:

user=0.01 sys=0.01,real=0.02 secs]这里边的 user、sys、real和Linux的time命令输出的时间含义一致分别代表:用户态消耗的CPU时间(user)、内核态消耗的CPU时间(sys)、操作从开始到结束所经过的墙钟时间(Wall Clock Time)(real)

墙钟时和CPU时间的区别:

墙钟时间包括各种非运算的等待耗时,例如磁盘IO、等待线程阻塞

而CPU时间则不包括这些耗时

但是当有多CPU或者多核时多线程操作会叠加这些CPU时间

所以有时user或者sys的事件会超过real的事件。

2.可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots 到这个对象是不可达),则证明此对象是不可用的。所以它们会被判定为可回收对象(例如图中的Obj5、Obj6、Obj7对象既是不可达的)。



在Java语言中,可作为GC Roots对象的包括下面一下几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中类能静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(Native方法)引用的对象

在可达性分析算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:

1.(缓刑)如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有 覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

2.(对象自我拯救)如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍候GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

对象可以在被GC时自我拯救,这种自救机会只有一次,因为一个对象的finalize()方法最多只会被系统调用一次。

判断对象是否存活与“引用”有关(补充)

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

强引用(Strong Reference):就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用(Soft Reference):用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。

弱引用(Weak Reference):用户描述非必须对象的。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用(Phantom Reference):一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时刻得到一个系统通知。

方法区回收(扩展)

方法区(在Hotpot虚拟机中的永久代),垃圾收集效率要低于堆中对象的收集效率(70%-90%),永久代中回收的主要内容为两块:废弃常量和无用类

废弃常量回收:以常量池中的字面量为例,如字符串常量“abc”,没有任何String对象引用常量池中的“abc”通俗点说就是没有任何String字符串叫“abc”,此时abc就会被清理出常量池,其他类(接口)、方法、字段的符号引用也是如此。

无用类的回收:满足三个条件①该类的所有实例都已经被回收,也就是Java堆中不存在此类的任何实例②加载该类的ClassLoarder已经被回收③该对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。此三项只代表该类可以回收,要想真正回收类,还要在虚拟机参数中设置:-Xnoclassgc

参考:《深入理解Java虚拟机第二版:JVM高级特性与最佳实践》

http://blog.csdn.net/u014381710/article/details/48554465
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: