频道栏目
首页 > 资讯 > Java > 正文

JVM学习(4)——全面总结Java的GC算法和回收机制

17-07-12        来源:[db:作者]  
收藏   我要投稿

JVM学习(4)——全面总结Java的GC算法和回收机制。

涉及到的知识点总结如下:

一些JVM的跟踪参数的设置

Java堆的分配参数

-Xmx 和 –Xms 应该保持一个什么关系,可以让系统的性能尽可能的好呢?是不是虚拟机内存越大越好?

Java 7之前和Java 8的堆内存结构

Java栈的分配参数

GC算法思想介绍 –GC ROOT可达性算法 –标记清除 –标记压缩 –复制算法

可触及性含义和在Java中的体现

finalize方法理解

Java的强引用,软引用,弱引用,虚引用

GC引起的Stop-The-World现象

串行收集器

并行收集器

CMS

记得JVM学习1里总结了一个例子,就是使用-XX:+printGC参数来使能JVM的GC日志打印,让程序员可以追踪GC的踪迹。如例子:

View Code

配置参数-XX:+printGC,再次运行会打印GC日志,截取一句:

[GC (Allocation Failure) 4416K->716K(15872K), 0.0018384 secs]

代表发生了GC,花费了多长时间,效果是GC之前为4M多,GC之后为716K,回收了将近4M内存空间,而堆的大小大约是16M(默认的)。

如果还嫌这些信息不够,JVM还提供了打印详细GC日志的参数:-XX:+PrintGCDetails

[GC (Allocation Failure) [DefNew: 4480K->0K(4992K), 0.0001689 secs] 5209K->729K(15936K), 0.0001916 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

会详细显示堆的各个代的GC信息,还详细的给出了耗时信息:user代表用户态cpu耗时,sys代表系统的cpu耗时,real代表实际经历时间。除此之外,-XX:+PrintGCDetails,还会在JVM退出前打印堆的详细信息:

View Code

经过分析得知,该堆的新生代有5M空间,使用了3M

def new generation total 4992K, used 3226K [0x03800000, 0x03d60000, 0x08d50000)

在对象出生的地方,也就是伊甸园,有4M空间,使用了72%

eden space 4480K, 72% used [0x03800000, 0x03b26830, 0x03c60000)

还有幸存代,from和to,他俩一定是相等的。

from space 512K, 0% used [0x03ce0000, 0x03ce0000, 0x03d60000)

to space 512K, 0% used [0x03c60000, 0x03c60000, 0x03ce0000)

最后还有一个老年代空间,总共有10M,使用了729K

tenured generation total 10944K, used 729K [0x08d50000, 0x09800000, 0x13800000)

最后是Java 8改进之后的元数据空间,其中还有些16进制数字,比如[0x08d50000, 0x09800000, 0x13800000),意思依次是低边界,当前边界,最高边界,代表内存分配的初始位置,当前分配到的位置,和最终能分配到的位置。

重定向GC日志的方法

-Xloggc:log/gc.log,指定GC log的位置,把GC日志输出到工作空间的log文件夹下的gc.log文件中,能更加方便的帮助开发人员分析问题。

打印最详细的GC堆的日志:-XX:+PrintHeapAtGC

意思是每次记录GC日志,前后都要打印Java堆的详细信息。如下一次:

View Code

监控Java类的加载情况: -XX:+TraceClassLoading

监控系统中每一个类的加载,每一行代表一个类,主要用于跟踪调试程序。

监控类的使用情况:-XX:+PrintClassHistogram

在程序运行中,按下Ctrl+Break后,打印类的信息:截取发现程序使用了大量的hashmap:

View Code

分别显示序号(按照空间占用大小排序)、实例数量、总大小、类型

下面看看Java堆的分配参数,指定最大堆和最小堆-Xmx –Xms

-Xms 10m,表示JVM Heap(堆内存)最小尺寸10MB,最开始只有 -Xms 的参数,表示 `初始` memory size(m表示memory,s表示size),属于初始分配10m,-Xms表示的 `初始` 内存也有一个 `最小` 内存的概念(其实常用的做法中初始内存采用的也就是最小内存)。

-Xmx 10m,表示JVMHeap(堆内存)最大允许的尺寸10MB,按需分配。如果 -Xmx 不指定或者指定偏小,也许出现java.lang.OutOfMemory错误,此错误来自JVM不是Throwable的,无法用try...catch捕捉。

看下对JVM设置:-Xmx20m -Xms5m

View Code

Xmx =19.375M

free mem =4.21685791015625M

total mem =5.875M

1032

记住:Java会尽量的维持在最小堆运行,即使设置的最大值很大,只有当GC之后也无法满足最小堆,才会去扩容。

-Xmx 和 –Xms 应该保持一个什么关系,可以让系统的性能尽可能的好呢?是不是虚拟机内存越大越好?

占坑,后续的GC机制来补充回答这个问题。首先并不是虚拟机内存越大就越好,大概原因是因为:内存越大,JVM 进行 Full GC 所需的时间越久,由于 Full GC 时 stop whole world 特性,如果是用于响应HTTP 请求的服务器,这个时候就表现为停止响应,对于需要低延迟的应用来说,这是不可接受的。对于需要高吞吐量的应用来说,可以不在乎这种停顿,比如一些后台的应用之类的,那么内存可以适当调大一些。需要根据具体情况权衡。

设置新生代大小,-Xmn参数,设置的是绝对值,30m就是30m,10m就是10m。还有一个参数-XX:NewRatio,看名字就知道是按照比例来设置,意思是设置新生代(eden+2*s)和老年代(不包含永久区)的比值,比如-XX:NewRatio4 表示 新生代:老年代=1:4。

设置两个Survivor区(s0,s1或者from和to)和eden的比例-XX:SurvivorRatio,比如-XX:SurvivorRatio8表示两个Survivor : eden=2:8,即一个Survivor占年轻代的1/10。

PS,这里说下Java堆的内存结构,Java 7和Java 8略有不同。先看 7以及以前的:

分为了eden伊甸园,两个幸存代survivor,前三者也叫年轻代,其次是老年代old和永久代permanent。一个Java对象被创建,先是存在于eden,如果存活时间超过了两个幸存代就转移到老年代保存,而永久带保存了对象的方法,变量等元数据,如果永久带没地方了就会发生内存泄漏异常错误OutOfMemeoryError:PermGen。

Java 8的堆内存结构有变化,移除了永久带,也就是不再有OutOfMemeoryError:PermGen错误了。新加了元数据区,和对应的参数-XX:MaxMetaspaceSize。

OOM时导出堆到文件进行内存分析和问题排查

-XX:+HeapDumpOnOutOfMemoryError,-XX:+HeapDumpPath导出OOM的路径,比如:

-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/a.dump

堆的分配参数总结

根据实际事情调整新生代和幸存代的大小

官方推荐新生代占堆的3/8

幸存代占新生代的1/10

在OOM时,记得Dump出堆,确保可以排查现场问题

永久区分配参数

-XX:PermSize , -XX:MaxPermSize,设置永久区的初始空间和最大空间,他们表示,一个系统可以容纳多少个类型。类似-Xms和-Xmx。当使用一些框架时,会产生大量的类,这样的类越来越多,会可能挤爆永久区,导致OOM。也就是说如果堆没有用完(实际堆的空间占用很少),也抛出了OOM错误,很有可能是永久区导致的OOM问题。

栈大小分配-Xss

通常只有几百K,一般很少调大,它的大小决定了函数调用的深度,之前也说了每个线程都有独立的栈空间,保存了局部变量、参数等。如果想跑更多的线程,需要把栈用-Xss尽量调小,而不是变大!因为线程越多,每个线程都要分配内存空间,这样每个栈的空间越大,占据的内存越多……

但是也得预防很深的函数调用可能导致栈内存溢出问题,比如不合适的递归调用。

Garbage Collection 垃圾收集简介

Java中,GC的对象主要是堆空间和永久区,记得很多人都下意识的认为Java的GC使用的是引用计数法,好像地球人都知道,无需多言似的,比如Python就是使用的这个。其实这是误导人的,Java可以说从来都没有用过这个引用计数算法!这是一个非常古老的算法了,另外PS:Java也不是第一个使用GC机制的语言(1960年 List 使用了GC)。

引用计数法,它的一个基本思想

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。就可以回收了。如图有一个根对象,和一个可达的对象:

Java为什么不用他呢,因为引用计数法有很多缺点:

性能,每次引用和去引用都要加减

循环引用问题

对象1没办法回收,但是确实没有用了。

现代Java的垃圾回收使用的基本的算法思想是标记-清除算法:

标记-清除算法是现代垃圾回收算法的思想基础。将垃圾回收分为两个阶段:标记阶段和清除阶段。

一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象(从GC ROOT开始标记引用链——又叫可达性算法)。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。这样就不怕循环问题了。

PS:Java中可以作为GC ROOT的对象有:

静态变量引用的对象

常量引用的对象

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

Java栈中引用的对象

如图,从根节点能到达的都是不能回收的,是被引用的。标记下。而这种算法的缺点就是容易出现内存碎片。利用率不高。

要知道,现代的Java虚拟机都是使用的分代回收的设计,比如在标记-清除算法的基础上做了一些优化的——标记-压缩算法,适合用于存活对象较多的场合,如老年代。

和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。有效解决内存碎片问题。

还有一个算法,针对新生代的回收,叫复制算法

和标记-清除算法相比,复制算法是一种相对高效的回收方法,但是

不适用于存活对象较多的场合如老年代,使用在新生代,

原理是

将原有的内存空间分为两块,两块空间完全相同,每次只用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。同样也没有内存

碎片产生。

复制算法的缺点是内存的浪费,因为每次只是使用了一般的空间,而大多数存活对象都在老年代,故复制算法不用在老年代,老年代是Java堆的空间的担保地区。复制算法主要用在新生代。在垃圾回收的时候,大对象直接从新生代进入了老年代存放,大对象一般不使用复制算法,因为一是太大,复制效率低,二是过多的大对象,会使得小对象复制的时候无地方存放。还有被长期引用的对象也放在了老年代。

Java的垃圾回收机制使用的是分代的思想。

依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法。少量对象存活(新生代,朝生夕死的特性),适合复制算法,大量对象存活(老年代,生命周期很长,甚至和应用程序存放时间一样),适合标记清理或者标记压缩算法。

以上一定注意:Java没有采用引用计数算法!

经过上述总结,想到所有的算法,需要能够识别一个垃圾对象,那么怎么才能识别呢?

因此需要给出一个可触及性的定义:

可触及的–从GC ROOT这个根节点对象,沿着引用的链条,可以触及到这个对象,该对象就叫可触及的,也就是之前说的可达性算法的思想。

可复活的–一旦所有引用被释放,就是可复活状态,因为在finalize()中可能复活该对象(finalize方法只会调用一次)。

不可触及的–在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,就可以回收了。

引出一个方法的理解:finalize方法

GC准备释放内存的时候,会先调用finalize()。而调用了这个方法不代表对象一定会被回收。因为GC和finalize() 都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

finalize()在什么时候被调用?

有三种情况

所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候。

程序退出时为每个对象调用一次finalize方法。

显式的调用finalize方法。

finalize 是Object的 protected 方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。finalize与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性。

不建议用finalize方法完成“非内存资源”的清理工作,因为Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行,而且finalize 方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行,finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的。finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)

但建议用于:

清理本地对象(通过JNI创建的对象);

作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。

说到这里,不得不提下Java的四种引用类型:

前面说了,GC是分代的,GC的的回收条件取决于识别该对象是不是垃圾。而识别垃圾对象又取决于指向该对象的引用类型。Java中有四种引用类型,强,软,弱,虚。

如果一个对象只有弱引用指向它,GC会立即回收该对象,这是一种急切回收方式。相对的,如果有软引用指向这些对象,则只有在JVM需要内存时才回收这些对象。弱引用和软引用的特殊行为使得它们在某些情况下非常有用。

例如:软引用可以很好的用来实现缓存,当JVM需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象。而弱引用非常适合存储元数据,例如:存储ClassLoader引用。如果没有类被加载,那么也没有指向ClassLoader的引用。一旦上一次的强引用被去除,只有弱引用的ClassLoader就会被回收。

强引用:类似我们常见的,比如 A a = new A();a就叫强引用。任何被强引用指向的对象都不能GC,这些对象都是在程序中需要的。

软引用:使用java.lang.ref.SoftReference类来表示,软引用可以很好的用来实现缓存,当JVM需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象。如下:

View Code

强引用置空之后,代码的第二行为对象Counter创建了一个软引用,该引用同样不能阻止垃圾回收器回收对象,但是可以延迟回收,软引用更适用于缓存机制,而弱引用更适用于存贮元数据。

弱引用:使用java.lang.ref.WeakReference 类来表示,弱引用非常适合存储元数据,例如:存储ClassLoader引用。如果没有类被加载,那么也没有指向ClassLoader的引用。一旦上一次的强引用被去除,只有弱引用的ClassLoader就会被回收。也就是说如果一个对象只有弱引用指向它,GC会立即回收该对象,这是一种急切回收方式。如:

View Code

只要给强引用对象counter赋null,该对象就可以被垃圾回收器回收。因为该对象不再含有其他强引用,即使指向该对象的弱引用weakCounter也无法阻止垃圾回收器对该对象的回收。相反的,如果该对象含有软引用,Counter对象不会立即被回收,除非JVM需要内存。

另一个使用弱引用的例子是WeakHashMap,它是除HashMap和TreeMap之外,Map接口的另一种实现。WeakHashMap有一个特点:map中的键值(keys)都被封装成弱引用,也就是说一旦强引用被删除,WeakHashMap内部的弱引用就无法阻止该对象被垃圾回收器回收。

虚引用:没什么实际用处,就是一个标志,当GC的时候好知道。拥有虚引用的对象可以在任何时候GC。

除了了解弱引用、软引用、虚引用和WeakHashMap,还需要了解ReferenceQueue。在创建任何弱引用、软引用和虚引用的过程中,可以通过如下代码提供引用队列ReferenceQueue:

View Code

引用实例被添加在引用队列中,可以在任何时候通过查询引用队列回收对象。

现在我对一个对象的生命周期进行描述:

新建Java对象A首先处于可达的,未执行finalize方法的状态,随着程序的运行,一些引用关系会消失,或者变迁,当对A使用可达性算法判断,对象A变成了GC Roots 不可达时,A从可达状态变迁到不可达状态,但是JVM不会就就这样把它清理了,而是在第一次GC的时候,对它首先进行一个标记(标记清除算法),之后最少还要再进行一次筛选,而对其筛选的的条件就是看该对象是否覆盖了Object的finalize方法,或者看这个对象是否执行过一次finalize方法。如果没有执行,也没有覆盖,就满足筛选条件,JVM将其放入F-Queue队列,由JVM的一个低优先级的线程执行该队列中对象的finalize方法。此时执行finalize方法优先级是很低的,且不会保证等待finalize方法执行完毕才进行第二次回收(怕发生无限等待的情景,JVM崩溃),之后不久GC对队列里的对象进行二轮回收,去判断该对象是否可达,若不可达,才进行回收,否则,对象“复活”(执行finalize的过程中,应用程序是可以让对象再次被引用,复活的)。而在可达性判断的时候,还要兼顾四种引用类型,根据不同的引用类型特点去判断是否是回收的对象。看例子:

View Code

结果:

CanReliveObj finalize called

obj 可用

第二次gc

obj 是 null

说明JVM不管程序员手动调用finalize,JVM它就是执行一次finalize方法。执行finalize方法完毕后,GC会再次进行二轮回收,去判断该对象是否可达,若不可达,才进行回收。

建议:避免使用finalize方法!

太复杂了,还是让系统照管比较好。可以定义其它的方法来释放非内存资源。建议使用try-catch-finally来替代它执行清理操作。

如果手动调用了finalize,很容易出错。且它执行的优先级低,何时被调用,不确定——也就是何时发生GC不确定,因为只有当内存告急时,GC才工作,即使GC工作,finalize方法也不一定得到执行,这是由于程序中的其他线程的优先级远远高于执行finalize()的线程优先级。因此当finalize还没有被执行时,系统的其他资源,比如文件句柄、数据库连接池等已经消耗殆尽,造成系统崩溃。且垃圾回收和finalize方法的执行本身就是对系统资源的消耗,有可能造成程序的暂时停止,因此在程序中尽量避免使用finalize方法。

上面提到了GC或者执行finalize可能造成程序暂停,这引出一个概念:Stop-The-World现象。   这是Java中一种全局暂停的现象,全局停顿,所有Java代码停止,类似JVM挂起的状态……但是native代码可以执行,但不能和JVM交互。这多半由于GC引起,其他的引起原因比如:

Dump线程

JVM的死锁检查

堆的Dump。   这三者出现概率很低,多半是程序员手动引起的,而GC是JVM自动引起的。

GC时为什么会有全局停顿?

类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间在某一个状态下打扫干净。回程序中就是只有程序暂停了,才能全面,完整,正确的清理一次垃圾对象,否则前脚清理了,后脚还有新的,永远清理不完,对判断垃圾对象也是一个判断上干扰的问题,也永远干净不了。

Stop-The-World现象危害   长时间服务停止,没有响应,一般新生代的GC停顿时间很短,零点几秒。而老年代比较时间长,几秒甚至几十分钟……一般堆内存越大,GC时间越长,也就是Stop-The-World越久。所以,JVM的内存不是越大越好,要根据实际情况设置。   遇到HA系统,可能引起主备切换,严重危害生产环境。比如一个系统,一个主机服务器,一个备机服务器,不会同时启动,我们会只使用一个,比如主机暂时因为GC没有响应,如果时间太长,我们会使用备机,一旦主机恢复了,主机也启动了,此时备机主机都启动了,很可能导致服务器数据不一致……      前面罗嗦了一堆,那么这些算法是如何在JVM中配合使用的呢?那么就引出新的问题需要解决:JVM的垃圾回收器。   回忆下堆的结构:还是以Java 7为例子:

  Java堆整体分两代,新生代和老年代,顾名思义,前者存放新生对象,大部分都是朝生夕死!进行GC的次数不多,后者存放的是时间比较久的对象,也就是多次GC还没死的对象。对象创建的时候,大部分都是放入新生代的eden区,除非是很大的对象,可能会直接存放到老年代,还有之前说的栈上分配(逃逸分析)。   如果eden对象在GC时幸存,就会进入幸存区,也就是s0,s1,或者叫from和to,或者叫survivor(两个),大小一样。完全对称,功能也一样。前面说了GC有复制算法,那么就是使用在这里,GC在新生代时,eden区的存活对象被复制到未使用的幸存区,假设是to,而正在使用的是from区的年轻的对象也会一起被复制到了to区,如果to区满了,这些对象也和大对象,老年对象一样直接进入了老年代保存(担保空间)。此时,eden区剩余的对象和from区剩余的对象就是垃圾对象,能直接GC,to区存放的是新生代的此次GC活下来的对象。避免了产生内存碎片。

先不说了,先看看JVM的垃圾回收器吧,先看一种最古老的收集器——串行收集器

最古老,最稳定,效率高,但是串行的最大问题就是停顿时间很长!因为串行收集器只使用一个线程去回收,可能会产生较长的停顿现象。我们可以使用参数-XX:+UseSerialGC,设置新生代、老年代使用串行回收,此时新生代使用复制算法,老年代使用标记-压缩算法(标记-压缩算法首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。有效解决内存碎片问题)。

因为串行收集器只使用一个线程去回收,可能会产生较长的停顿现象。

还有一种收集器叫并行收集器(两种并行收集器)

一种是ParNew并行收集器。使用JVM参数设置XX:+UseParNewGC,设置之后,那么新生代就是并行回收,而老年代依然是串行回收,也就是并行回收器不会影响老年代,它是Serial收集器在新生代的并行版本,新生代并行依然使用复制算法,但是是多线程,需要多核支持,我们可以使用JVM参数:XX:ParallelGCThreads 去限制线程的数量。如图:

注意:新生代的多线程回收不一定快!看在多核还是单核,和具体环境。、

还有一种是Parallel收集器,它类似ParNew,但是更加关注JVM的吞吐量!同样是在新生代复制算法,老年代使用标记压缩算法,可以使用JVM参数XX:+UseParallelGC设置使用Parallel并行收集器+ 老年代串行,或者使用XX:+UseParallelOldGC,使用Parallel并行收集器+ 并行老年代。也就是说,Parallel收集器可以同时让新生代和老年代都并行收集。如图:

关于并行收集器还有两个参数设置:

-XX:MaxGCPauseMills,代表最大的GC线程占用的停顿时间,单位是毫秒,GC尽力保证回收时间不超过设定值,不是100%的保证。

-XX:GCTimeRatio,GC使用的cpu时间占总时间的百分比,理解为吞吐量,0-100的取值范围,垃圾收集时间占总时间的比,默认99,即最大允许1%时间做GC。我们肯定希望停顿时间短,且占用总时间比例少,但是这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优。

如果GC很频繁,那么GC的最大停顿时间变短,但吞吐量变小,如果GC次数很少,最大的停顿时间就会变长,但吞吐量增大。

最后看一个很重要的收集器-CMS(并发标记清除收集器Concurrent Mark Sweep)收集器

顾名思义,它在老年代使用的是标记清除算法,而不是标记压缩算法,也就是说CMS是老年代收集器(新生代使用ParNew),所谓并发标记清除就是CMS与用户线程一起执行。标记-清除算法与标记-压缩相比,并发阶段会降低吞吐量,使用参数-XX:+UseConcMarkSweepGC打开。

CMS运行过程比较复杂,着重实现了标记的过程,可分为:

初始标记,标记GC ROOT根可以直接关联到的对象(会产生全局停顿),但是初始标记速度快。

并发标记(和用户线程一起),主要的标记过程,标记了系统的全部的对象(不论垃圾不垃圾)。

重新标记,由于并发标记时,用户线程依然运行(可能产生新的对象),因此在正式清理前,再做一次修正,会产生全局停顿。

并发清除(和用户线程一起),基于标记结果,直接清理对象。这也是为什么使用标记清除算法的原因,因为清理对象的时候用户线程还能执行!标记压缩算法的压缩过程涉及到内存块移动,这样会有冲突。

并发重置,为下一次GC做准备工作。

CMS的特点

尽可能降低了JVM的停顿时间,但是会影响系统整体吞吐量和性能,比如:

在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半。

清理不彻底。因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理。

因为和用户线程基本上是一起运行的,故不能在空间快满时再清理。

可以使用-XX:CMSInitiatingOccupancyFraction设置触发CMS GC的阈值,设置空间内存占用到多少时,去触发GC,如果不幸内存预留空间不够,就会引起concurrent mode failure。

可以使用-XX:+ UseCMSCompactAtFullCollection, Full GC后,进行一次整理,而整理过程是独占的,会引起停顿时间变长。

可以使用-XX:+CMSFullGCsBeforeCompaction,设置进行几次Full GC后,进行一次碎片整理。

还可以使用-XX:ParallelCMSThreads,设定CMS的线程数量,一般设置为cpu数量,不用太大。

为减轻GC压力,我们需要注意些什么?

从三个方面考虑:

软件如何设计架构

代码如何写

堆空间如何分配

相关TAG标签
上一篇:批处理,删除过期文件
下一篇:[POJ 1330][CodeVS 2370]LCA 倍增
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站