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.标记-清除算法
这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。
  1. 复制算法
这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,虚拟机生成的新对象则放在另一半空间中。垃圾回收器运行时,它把可到达对象复制到另一半空间,没有被复制的的对象都是不可达对象,可以被回收。这种方法适用于短生存期的对象,持续复制长生存期的对象由于多次拷贝,导致效率降低。缺点是只有一半的虚拟机空间得到使用。
  1. 标记-整理算法
  2. 分代收集算法
在实际中用到的回收器都是这几种算法的组合,比如从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虚拟机中的永久代):很多人认为方法去是没有垃圾收集的
主要回收两部分内容:废弃敞亮和无用的类
怎样的类可以被回收:
  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
 
原文地址:https://www.cnblogs.com/novalist/p/6398676.html