Java 垃圾回收

什么是垃圾回收?

不会再被使用的对象就是垃圾;

我们不需要它了, 但它还要占用空间直到程序结束,真是坏的很.

我们要避免这种情况,就必须在它变成垃圾后 尽快回收它.

垃圾回收的2种方式

  • 手动: 在程序中用回收函数回收垃圾. 例如c++中用free()方法回收一个垃圾.
  • 自动: 不用你管,编程语言帮你搞定垃圾回收. 如java.

判断是否是垃圾的2个方法

1.引用计数法

定义: 给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器+1;当引用失效时,计数器-1。任何时刻计数值为0的对象就是不可能再被使用的。也就是意味着是一个失效的垃圾对象,就会被gc进行回收。

缺点: 但是这种算法在当前的jvm中并没有采用,原因是他并不能解决对象之间相互引用的问题。
假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而是垃圾回收机制无法识别。

2.可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中常量引用的对象。

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

下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。

img

由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,会被当成垃圾处理,可以进行回收。

Java垃圾回收的4种方法

1.标记-清除法(Mark-Sweep)

我先将垃圾标记出来,等垃圾到达一定数量之后,我再出来将这些垃圾一网打尽(清除).

为什么要等垃圾达一定数量后再清除?

因为垃圾回收程序是个大哥,出场费用不便宜(执行一次花费代价不可忽略).

img

方法缺点

  1. 标记和清除两个阶段执行效率不高
  2. 内存碎片化:

2.标记-整理法(Mark-Compact)

标记-整理法 = 标记-清除法 + 整理内存,避免内存碎片化

如下图,不仅进行了垃圾清除,还对存活的对象进行整理,避免了内存碎片化.

img

3.复制法(Copying)

将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉

img

不过这种算法有个缺点,内存缩小为了原来的一半,这样代价太高了

现在的商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此**新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。**每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

4.分代收集算法

上面三种算法的结合.

在java中,把内存中的对象按生命长短分为:

  • 新生代(young):存活时间较短的对象,如某些临时变量
  • 老年代(old):存活时间较长的对象, 如某些全局变量
  • 永久代(permanent):将一直存在的对象,主要用于存储一些类的元数据,常量池,java类,静态文件等信息。

如下图,

img

  1. 当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区,然后整理Survivor的两个区。
  2. 新对象的内存分配都是先在Eden区域中进行的,当Eden区域的空间不足于分配新对象时,就会触发年轻代上的垃圾回收,我们称之为"minor gc".当minor gc被触发后,所有存活的对象(仍然可达对象)会被拷贝到其中一个Survivor区域,同时年龄增长为“1”。并清除整个Eden内存区域中的非可达对象。
  3. 当对象的年龄足够大(这个年龄可以通过JVM参数进行指定,这里假定是2),当minor gc再次发生时,它会从Survivor内存区域中升级到年老代中
  4. 当minor gc发生时,又有对象从Survivor区域升级到Tenured区域,但是Tenured区域已经没有空间容纳新的对象了,那么这个时候就会触发年老代上的垃圾回收,我们称之为"major gc"。
  • minor gc: 回收年轻代的垃圾
  • major gc: 回收老年代的垃圾
  • full gc: 回收整个堆空间—包括年轻代和老年代的垃圾。

不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

1.新生代: 大批对象死去、少量对象存活的; 使用复制算法,复制成本低;

2.老年代: 对象存活率高、没有额外空间进行分配担保的; 采用标记-清理算法或者标记-整理算法。

Stop the World (STW事件): 在这种事件发生时,所有的程序线程都要暂停,直到事件完成(比如这里就是完成了所有回收工作)为止。

垃圾收集器

  1. Serial/Serial Old

    • 最老的收集器,单线程收集器. 用它进行垃圾回收时,必须暂停所有用户线程。

    • Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老年代的收集器,采用Mark-Compact算法。

    • 优点是简单高效,缺点是需要暂停用户线程。

  2. ParNew

    Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。

  3. Parallel Scavenge

    新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

  4. Parallel Old

    Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。

  5. CMS

    Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。

  6. G1

    G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。

理解GC日志

2017-11-22T22:36:06.735+0800:[GC [PSYoungGen: 9193K->1024K(9216K)] 13393K->10990K(29696K), 0.0104110secs] [Times: user=0.05 sys=0.00, real=0.02 secs]

2017-11-22T22:36:06.735+0800 //垃圾回收时的时间

GC //垃圾回收的类型,GC是只回收新生代;Full GC会回收新生代、年老代、永久代,会停止所有用户线程。

PSYoungGen //年轻代的垃圾回收使用的是Parallel Scanvenge垃圾收集器,简称PS,年轻代就是PSYoungGen。

9193K->1024K(9216K)//年轻代划分成Eden区、From Survivor区和To Survivor区,整个年轻代可以用来使用的就是Eden区加上其中一个Survivor区,也就是8M+1M=9M=9216K,9193K是指Eden区+其中一个Survivor区在垃圾回收之前占用的内存,1024K是指Eden区+其中一个Survivor区在垃圾回收之后还在占用的内存

13393K->10990K(29696K) //29696K指堆的可用大小,包含Eden区+其中一个Survivor区+年老代,8M+1M+20M=29M=29696K,13393K指执行垃圾回收前这三个区域所占的内存,10990K指执行垃圾回收之后这三个区域所占的内存。

Times:user=0.05 sys=0.00, real=0.02 secs //user=0.05是指CPU运行的总时长,如果有多核,则累加;sys=0.00,是指内核态消耗的CPU事时间;real=0.02是指操作从开始到结束所经过的墙钟时间,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O,等待线程阻塞,而CPU时间不包括这些耗时。

2017-11-22T22:36:06.751+0800:[Full GC [PSYoungGen: 9216K->0K(9216K)] [ParOldGen:14714K->18607K(20480K)] 23930K->18607K(29696K) [PSPermGen:2570K->2569K(204800K)], 0.2016436 secs] [Times: user=0.22 sys=0.00,real=0.20 secs]

Full GC //垃圾回收的类型,GC是只回收新生代;Full GC会回收新生代、年老代、永久代,会停止所有用户线程。

ParOldGen //年老代的垃圾回收采用的是ParNew收集器

14714K->18607K(20480K) //20480K指年老代最大可以分配的内存20M=20480K;14714K指执行垃圾回收前永久代占用的内存;18607K指年老代执行垃圾回收后所占的内存,由于年轻代经过垃圾回收后年轻代或Survivor中的部分对象被移动到年老代,所以导致年老代执行垃圾回收后占用的内存超过垃圾回收之前所占的内存。

PSPermGen //永久代的垃圾回收采用的是Parallel Scanvenge垃圾收集器

2570K->2569K(204800K) //永久代最大可以分配的内存200M=204800K,2570K指永久代执行垃圾回收前所占内存,2569指永久代执行垃圾回收之后所占内存

jvm参数说明

-Xmx3550m:设置JVM最大堆内存为3550M。

-Xms3550m:设置JVM初始堆内存为3550M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。

-Xmn2g:设置年轻代大小为2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。

-XX:NewSize=1024m:设置年轻代初始值为1024M。

-XX:MaxNewSize=1024m:设置年轻代最大值为1024M。

-XX:PermSize=256m:设置持久代初始值为256M。

-XX:MaxPermSize=256m:设置持久代最大值为256M。

-XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。

-XX:MaxTenuringThreshold=7:表示一个对象如果在Survivor区(救助空间)移动了7次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。

JVM的GC日志的主要参数包括如下几个:

PrintGCDetails //打印出垃圾回收日志

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:+PrintGCApplicationStoppedTime // 输出GC造成应用暂停的时间
-Xloggc:…/logs/gc.log 日志文件的输出路径

减少GC开销的措施

垃圾回收本身也会占用系统资源,我们要尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:

(1)不要显式调用System.gc()

此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

(2)尽量减少临时对象的使用

临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

(3)对象不用时最好显式置为Null

一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

(4)尽量使用StringBuffer,而不用String来累加字符串

由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

(5)能用基本类型如Int,Long,就不用Integer,Long对象

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

(6)尽量少用静态对象变量

静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

(7)分散对象创建或删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

posted @ 2019-05-30 09:54  lee3258  阅读(169)  评论(0编辑  收藏  举报