Java垃圾回收
Posted novalist
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java垃圾回收相关的知识,希望对你有一定的参考价值。
在内存管理部分比较大的一块内容是GC(垃圾回收),所谓垃圾回收就是将垃圾占用的内存回收掉。(垃圾回收针对的是JVM的堆内存)。那么第一个问题:什么是垃圾?
http://blog.csdn.net/zouxinfox/article/details/1594216
1.引用计数算法:被引用次数为0的对象。
堆中每一个对象都有一个引用计数。当新创建一个对象,或者有变量被赋值为这个对象的引用,则这个对象的引用计数加1;当一个对象的引用超过生存期或者被设置一个新的值时,这个对象的引用计数减1。当对象的引用计数变为0时,就可以被当作垃圾收集。
这种方法的好处是垃圾收集较快,适用于实时环境。缺点是这种方法无法监测出循环引用。例如对象A引用对象B,对象B也引用对象A,则这两个对象可能无法被垃圾收集器收集。因此这种方法是垃圾收集的早期策略,现在很少使用
2.根搜索算法(可达性算法):从GC Roots沿着引用找不到的对象。
这种方法把每个对象看作图中一个节点,对象之间的引用关系为图中各节点的邻接关系。垃圾收集器从一个或数个根结点遍历对象图,如果有些对象节点永远无法到达,则这个对象可以被当作垃圾回收。
容易发现,这种方法可以检测出循环引用,避免了引用计数法的缺点,较为常用。
这里都提到了引用,在JDK 1.2之后Java就已经对引用的概念进行了扩充,那么第二个问题:有哪些类型的引用?
1.强引用:Object o = new Object()这种都是强引用。
2.弱引用:还有用但非必须的,在OOM之前被回收。
3.软引用:更弱的引用,在下次GC的时候被回收。
4.虚引用:最弱的,唯一的作用是在对象被回收的时候可以收到通知。
这里只有强引用才能对对象的生命周期造成影响。
1.标记-清除算法
这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。
- 复制算法
这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,虚拟机生成的新对象则放在另一半空间中。垃圾回收器运行时,它把可到达对象复制到另一半空间,没有被复制的的对象都是不可达对象,可以被回收。这种方法适用于短生存期的对象,持续复制长生存期的对象由于多次拷贝,导致效率降低。缺点是只有一半的虚拟机空间得到使用。
- 标记-整理算法
- 分代收集算法
在实际中用到的回收器都是这几种算法的组合,比如从VisualVM中看到的内存是这样的(需要明白各部分都是怎样互相配合的):
整体上来看是分代收集算法,而S0、S1这两部分可以看做是标记-整理算法。
年轻代对象因为生命周期短,每次有约90%以上对象的占用空间被回收,采用“复制-清除”算法清理,具体过程:将新生代分为一个Eden空间和两个Survivor空间,默认Eden空间和Survivor空间的比例为8:1,对象分配到Eden和其中一个Survivor空间,回收时将存活的对象复制到另一个Survivor空间,然后将Eden空间和先前使用的Survivor空间清理。
老年代因对象生命周期较长,每次回收只有少部分对象没清理,如果使用“复制-清理”算法的话需要额外预留更多的空闲空间用于复制生存对象,
( 例如,100M的老年代占用空间 每次能回收50% ,那么他需要预留50M的空间 内存使用上不经济 )
所以回收时使用“标记-整理”算法。
那么第三个问题:常见的CMS垃圾回收器的执行流程是怎样的?
1. 初始标记:GC Roots直接关联的对象。
2. 并发标记:Root Tracing。
3. 重新标记:修复由于程序运行导致标记产生变动。
4. 并发清除
具体如下图所示:
可以看到只有在初始标记和重新标记的时候才需要Stop The World,其他都是和用户线程一起执行,不要以为这就完美了,并行执行的过程会消耗掉一些CPU资源。
Garbage-First,面向服务器的垃圾收集器,主要针对配置多处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
可以像CMS收集器一样,GC操作与应用的线程一起并发执行
紧凑的空闲内存区且没有很长的GC停顿时间
需要可预测的GC暂停耗时
不想牺牲太多吞吐量性能
启动后不需要请求更大的Java堆
G1是一款压缩型的收集器,G1通过有效的压缩完全避免了对细微空闲内存的分配,不用依赖于regions,大大简化了收集器,还消除了潜在的内存碎片问题。
G1执行垃圾回收的处理方式与CMS相似. G1在全局标记阶段(global marking phase)并发执行, 以确定堆内存中哪些对象是存活的。标记阶段完成后,G1就可以知道哪些heap区的empty空间最大。它会首先回收这些区,通常会得到大量的自由空间. 这也是为什么这种垃圾收集方法叫做Garbage-First(垃圾优先)的原因。顾名思义, G1将精力集中放在可能布满可收回对象的区域, 可回收对象(reclaimable objects)也就是所谓的垃圾. G1使用暂停预测模型(pause prediction model)来达到用户定 义的目标暂停时间,并根据目标暂停时间来选择此次进行垃圾回收的heap区域数量
被G1标记为适合回收的heap区将使用转移(evacuation)的方式进行垃圾回收. G1将一个或多个heap区域中的对象拷贝到其 他的单个区域中,并在此过程中压缩和释放内存. 在多核CPU上转移是并行执行的(parallel on multi-processors), 这样能减少 停顿时间并增加吞吐量. 因此,每次垃圾收集时, G1都会持续不断地减少碎片, 并且在用户给定的暂停时间内执行. 这比以前 的方法强大了很多. CMS垃圾收集器(Concurrent Mark Sweep,并发标记清理)不进行压缩. ParallelOld 垃圾收集只对整个堆 执行压缩,从而导致相当长的暂停时间。
需要强调的是, G1并不是一款实时垃圾收集器(real-time collector). 能以极高的概率在设定的目标暂停时间内完成,但不保证 绝对在这个时间内完成。 基于以前收集的各种监控数据, G1会根据用户指定的目标时间来预估能回收多少个heap区. 因此, 收集器有一个相当精确的heap区耗时计算模型,并根据该模型来确定在给定时间内去回收哪些heap区.
注意 G1分为两个阶段: 并发阶段(concurrent, 与应用线程一起运行, 如: 细化 refinement、标记 marking、清理 cleanup) 和 并行阶段(parallel, 多线程执行, 如: 停止所有JVM线程, stop the world). 而 FullGC(完整垃圾收集)仍然是单线程的, 但如果进 行适当的调优,则应用程序应该能够避免 full GC。
S Concurrent Mark Sweep并发标记清理收集器,回收年老代内存。
吞吐量 应用花在非GC上的时间百分比
GC负荷 与吞吐量相反,指应用花在GC上的时间百分比
暂停时间 应用花在GC stop-the-world 的时间
GC频率 顾名思义
Footprint 一些资源大小的测量,比如堆的大小
反应速度 从一个对象变成垃圾道这个对象被回收的时间
一个交互式的应用要求暂停时间越少越好,然而,一个非交互性的应用,当然是希望GC负荷越低越好。
一个实时系统对暂停时间和GC负荷的要求,都是越低越好。
一个嵌入式系统当然希望Footprint越小越好。
整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
1. 堆大小设置
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
2. 典型设置 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。
此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。
更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -
XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,
则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,
则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。
如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,
这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
3. 参数信息
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
· -XX:+PrintGC 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]
· -XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
· -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
· -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
· -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
· -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。
并行收集线程数。
· -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
· -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
· -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
· -Xloggc:filename
· -XX:+UseSerialGC:设置串行收集器
· -XX:+UseParallelGC:设置并行收集器
· -XX:+UseParalledlOldGC:设置并行年老代收集器
· -XX:+UseConcMarkSweepGC:设置并发收集器
· -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:设置持久代大小
1. 年轻代大小选择
· 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
· 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
2. 年老代大小选择
· 并发垃圾收集信息
· 持久代并发收集次数
· 传统GC信息
· 花在年轻代和年老代回收上的时间比例
· 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:减少年轻代和年老代花费的时间,一般会提高应用的效率
· 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
3. 较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
· -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
· -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
GC打印时间: [垃圾回收类型回收时间: [收集器名称: 年轻代回收前占用大小->年轻代回收后占用大小(年轻代当前容量),
年轻代局部GC时JVM暂停处理的时间] 堆空间GC前占用的空间->堆空间GC后占用的空间(堆空间当前容量)
,GC过程中JVM暂停处理的时间]。
垃圾回收类型:分为GC和Full GC.
GC一般为堆空间某个区发生了垃圾回收,
Full GC基本都是整个堆空间及持久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。
收集器名称:一般都为收集器的简称或别名,通过收集器名称基本都能判断出那个区发生了GC。
代码执行
把Java源码丢给JVM肯定是不能执行的,需要先用javac编译成class文件才行,那么第一个问题:class文件的结构是怎样的?
· 常量池
· 访问标志
· 类索引、父类索引和接口索引
· 字段表
· 方法表
· 属性表
虚拟机规范并没有规定在什么时候要加载类,但是规定了在遇到new、反射、父类、Main的时候需要初始化完成。
JVM将类加载过程划分为三个步骤:装载、链接和初始化。
装载(Load):装载过程负责找到二进制字节码并加载至JVM中,JVM通过类的全限定名(com.bluedavy. HelloWorld)及类加载器(ClassLoaderA实例)完成类的加载;
链接(Link):链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量及解析类中调用的接口、类;
初始化(Initialize):执行类中的静态初始化代码、构造器代码及静态属性的初始化。
解释执行的好处是下载后启动速度快,但是缺点也非常明显:运行速度慢。
JIT正是用来解决这个问题的,能够将多次调用的方法、多次执行的循环体编译成本地代码。
优化是个很好玩的题目,记得在参加一次变成比赛的时候用gcc -O3编译之后的代码把printf()都没输出了。。在JIT中比较常见的优化手段有:
在HotSpot虚拟机中,对象在内存中存储的布局可以分为对象头包括两部分信息,第一部分
任何对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。反对使用原因:代价高昂,不确定性大,如果发生死循环可能导致整个内存回收系统崩溃。
回收方法区(HotSpot虚拟机中的永久代):很多人认为方法去是没有垃圾收集的
主要回收两部分内容:废弃敞亮和无用的类
怎样的类可以被回收:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
以上是关于Java垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章
Java学习笔记3.11.2 垃圾回收 - 垃圾回收的实现方式
53.垃圾回收算法的实现原理启动Java垃圾回收Java垃圾回收过程垃圾回收中实例的终结对象什么时候符合垃圾回收的条件GC Scope 示例程序GC OutOfMemoryError的示例