本篇博客大部分内容来自《深入理解java虚拟机》,也参考了http://jbutton.iteye.com/blog/1569746这篇文章的部分内容,这里注明出处。这篇博客也是这个系列的第二篇,在这篇博客里我会对java的内存回收机制做个详细的整理。希望通过书写这个系列的博客能让自己对Java底层执行过程有个详细的了解,花费了一整天的时间终于有了一个详细的了解了—TML

概述

接下来的全文流程会按照四个部分来解释垃圾回收机制,做一个全面总结。
第一部分:基本介绍垃圾回收机制的目标,执行时间和方式等。
第二部分:一些成熟的垃圾回收算法
第三部分:一些成熟的垃圾收集器
第四部分:垃圾回收策略有哪些

基本介绍

首先提出三个问题:1,哪些内存需要回收? 2,什么时候回收? 3,如何回收?然后进行简单的回答:

1,哪些内存需要回收?

回收区域主要集中在java堆和方法区

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,所以不需要考虑回收,而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

2,什么时候回收

1、 对象没有引用
2、 作用域发生未捕获异常
3、 程序在作用域正常执行完毕
4、 程序执行了System.exit()
5、 程序发生意外终止(被杀进程等)

3,如何回收

引用计数算法

原理

在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!

问题

当我们的代码出现下面的情形时,该算法将无法适应
a) ObjA.obj = ObjB
b) ObjB.obj - ObjA

这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。

可达性分析算法

原理

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不达)时,则证明此对象是不可用的

可作为GC root的对象

1,虚拟机栈(栈帧中的本地变量表)中引用的对象。
2,方法区中类静态属性引用的对象。
3,方法区中常量引用的对象。
//4,本地方法栈中JNI(即一般说的Native方法)引用的对象。

如何逃逸

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”

第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会。
第二次标记,GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

注意:finalize()方法只能被系统调用一次,如果这次对象逃逸成功,下一次再次被回收时候就无法自救了。

=====================TML=============TML===============TTML==========

了解了以上部分的内容,就会对垃圾回收算法有个大概的概念,可以得知,其实对象是否要被垃圾回收,最核心的问题还是有没有引用指向它。只要有引用一直指向该对象,无论该对象是否有用都不会被回收,所以引用是关键。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

四类引用

1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)4种

强引用(最强)

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

总结起来就是一直存在,超过内存就报错,报错就报错,我就是一直在!

软引用(第二强)

软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常

总结起来就是我很想存在,除非即将发生内存溢出,为了大局,就算被引用也请将我回收吧!

弱引用(第三强)

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,**被弱引用关联的对象只能生存到下一次垃圾收集发生之前。**当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

总结起来就是虽然我很想存在,但我好像没那么必要哈,直接把我回收吧!

虚引用(最弱)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引
用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

总结起来就是有我没我都一样,回收的时候我告诉您一声。

=====================TML=============TML===============TTML==========

上边谈完了关于对象的回收,主要是在java堆里的回收,那么在方法区里又是怎么回收的呢

方法区的回收

回收的主要内容

永久代的垃圾收集主要回收两部分内容:废弃常量无用的类

回收废弃常量

回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

回收无用类

判定一个类是否是“无用的类”的条件有三个:
1,类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2,加载该类的ClassLoader已经被回收
3,该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是
和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用**-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading**查看类加载和卸载信息,

可能发生场景

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁
自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

Hotpot的算法实现

枚举根节点

要进行可达性分析,首先要找到GCroot,可以作为GC Roots的节点主要就是一些全局性的引用(例如常量或者静态属性)或者栈帧中的本地变量表。而我们做可达性分析的时候,必须确保整个分析期间整个执行系统的一致性,就好像整个执行系统看起来像被冻结在某个时间点上一样,为了保证实现,必须进行GC停顿,停止所有java线程,(stop the world).

停顿后不需要一个不落的检测,在类加载完成时,hotpot把存储对象内什么偏移量上有什么类型数据计算出来,然后在**特定位置(后文提到的安全点)**记录(使用OopMap数据结构)下栈和寄存器中哪些位置是引用,所以hotpot可以知道对象的引用信息

安全点

hotpot如果为每一条指令都设置OopMap,GC成本会很高,所以GC停顿实际只有在安全点才会执行。一般选定为“是否具有让程序长时间执行的特征”为标准,一般是方法调用,循环跳转,异常跳转这些情况。所以一般设置在这些地方。另一个问题是如何让所有线程都跑到最近的安全点位置再停下来

抢先试中断

GC发生时,首先把所有线程都中断,然后让不在安全点位置中断的线程恢复,跑到安全点上,然后再中断。

主动式中断

当GC需要主动式中断线程的时候,不直接对线程操作,而是在安全点位置设置一个标志,让各个线程执行时主动去轮询这个标志,发现中断标志位真时就自己中断挂起

安全区域

有些时候只有安全点是不够的。为什么不够呢? 因为我们忘掉了一种常见执行的情况。我们忘了它,因为它实际上不是长时间执行,而是长时间闲置。有这样一类情况程序不能及时响应GC触发事件,比如sleep,因系统调用阻塞。 这些操作不是JVM能够控制的。JVM在此期间不能够响应GC事件。 因此,我们引入了安全区域的概念来解决这个问题。安全区域是其中引用不会改变的一段代码片段,那么在其中任一点进行根枚举(GC)都是安全的。 换句话说,安全区域是安全点的一个很大的扩展。

在线程执行Safe Region中代码时,首先标识自己已经进入Safe Region,那么,当在这段时间里JVM要发起GC时,在安全区域里的就自动被GC,它不用关心该区域里的线程。

垃圾回收算法

内存分区与回收算法使用

依据垃圾回收机制作用的内存分区

垃圾回收区域主要分为:Permanent Space 和 Heap Space。

  1. Permanent 即 持久代(Permanent Generation),主要存放的是Java类定义信息,与垃圾收集器要收集的Java对象关系不大。这个在运行时内存中其实就是运行时内存分区里的方法区,定义为(非堆)。

  2. Heap = { Old + NEW = {Eden, from, to} },Old 即 年老代(Old Generation),New 即 年轻代(Young Generation)。年老代和年轻代的划分对垃圾收集影响比较大。

分代收集算法

对于Heap里的不同区域通常使用不同的算法区解决。统称为分代收集算法:根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代(年轻代)和老年代(年老代),这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去(朝生夕死),只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收

标记清理算法

可以使用在年老代,但不怎么推荐。

算法原理

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点

1,效率问题,标记和清除两个过程的效率都不高;
2,空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

复制算法的改进算法用于年轻代的垃圾回收。

原理

将内存分成两块,每次只使用其中一块,垃圾回收时,将标记的对象拷贝到另外一块中,然后完全清除原来使用的那块内存。复制后的空间是连续的。因为垃圾对象多于存活对象,复制算法更高效

缺点

虽然高效,但是这种算法的代价是将内存缩小为了原来的一半,大大减小了可使用内存。

改进复制算法(年轻代GC算法)

Minor GC触发条件:当Eden区满(没有足够空间再进行分配)时,触发Minor GC(记住GC是一个过程)。

原理

JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过它自己的第一次Minor GC后,如果仍然存活,并且能被Survivor区容纳,则将会被移到Survivor(to)区,并且将年龄设置为1。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度(15)时,就会被移动到年老代中

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将无法容纳的新的存活对象移动到年老代中。

总结来说:当Eden中无法再分配空间的时候,触发Minor GC,将存活的对象放到Survivor区,之后的每次Minor GC都会既清理Eden,也清理From,将存活的复制到TO

关于年轻代的Jvm参数

1)-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

标记整理算法(年老代GC算法)

年老代通常使用的算法,发生在该代的GC称之为Full GC
###原理
标记阶段与标记清除算法一样。但后续并不是直接对可回收的对象进行清理,而是让所有存活对象都想一端移动,然后清理。优点是不会造成内存碎片。

Minor GC的触发时机

Full GC的触发时机

一般Full GC总会伴随一次Minor GC
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法区(持久代)空间不足

(4)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小(Minor GC不安全

(5)通过Minor GC后进入老年代的平均大小大于老年代的可用内存(不冒险直接Full GC
#垃圾收集器

年轻代垃圾收集器

三种年轻代的垃圾回收器:串行GC(Serial)、并发GC(Parallel Scavenge)和并行GC(ParNew)。全部使用复制算法

Serial(串行GC)

原理

是一种单线程垃圾回收机制,而且不仅如此,它最大的特点就是在进行垃圾回收的时候,需要将所有正在执行的线程暂停(Stop The World)

应用场景

只要我们能够做到将它所停顿的时间控制在N个毫秒范围内,是完全可以接受的,该收集器适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client(客户端)级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

ParNew(并行GC)

虽说同时做好几件事,但这几件事都是关于垃圾回收的。

原理

基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率。

可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

使用场景

它可以被用在**服务器端(Server)**上,同时它可以与CMS GC配合,所以,更加有理由将它置于Server端。

Parallel Scavenge(并行GC)

原理

吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。该收集器是吞吐量优先收集器,也就是说优先吞吐量,可能会导致用户感觉停顿时间长

使用场景

多核可以最大可能利用cpu,要求时间间隔短,主要适用于在后台运行且不需要太多交互的任务,常应用在服务器端。

年老代垃圾收集器

年老代垃圾收集器有以下三个

Serial Old(串行GC):

使用标记整理算法,是Serial在年老代里的版本。
1, 这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
2,如果在Server模式下,那么它主要还有两大用途:

  1. 一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
  2. 另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Parallel Old(并行GC)

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法

这样可以用全套的吞吐量优先

CMS(并发GC)

基于“标记—清除”算法
初始标记(CMS initial mark)、并发标记(CMS concurrenr mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)。

其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。**初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。**由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的

优点与缺点

优点是:高并发、高响应
缺点是:以下三点
1,CMS收集器对CPU资源非常敏感,随着cpu数量增加性能增强。

2,CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用,要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

3,最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC

年轻代与年老代的组合

年轻代与年老代通用(G1)

实现方式


JDK1.7u4,是面向服务端应用的垃圾收集器。以期未来可以替换CMS,新生代和老年代都可以进行。
特点:
并行与并发:充分利用多CPU、多核环境下的硬件优势,多CPU来缩短停顿时间。
分代收集:采用不同方式去处理新建对象以及熬过多次GC的旧对象以获得更好的收集效果;
空间整合:整体上是标记整理,局部是复制;有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。将Java堆分为多个大小相等的独立区域,新生代和老年代不再是物理隔离的。
可预测的停顿:除了追求低停顿,还能建立可预测的停顿时间模型。
运作步骤:初始标记、并发标记、最终标记、筛选回收。
最终标记需要停顿线程,但是可以并行执行。

优势

相比CMS收集器有不少改进,

  • 可以并发

  • 首先基于标记-整理算法,不会产生内存碎片问题

  • 其次,可以比较精确的控制停顿

内存分配与回收策略

内存分配在不同的垃圾收集器里是不同的,这里介绍几条通用规则几条规则

对象优先在Eden分配

通过**-Xms20M、-Xmx20M、-Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1。Eden大小为8M。向Eden中加四个对象:allocation1(2M)、allocation(2M)、allocation(2M),当要加入allocation(4M)时发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。但三个对象都是存活的,GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制**提前转移到老年代去。然后allocation(4M)进入Minor GC

大对象直接进入老年代

虚拟机提供了一个**-XX:PretenureSizeThreshold参数**,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制
##长期存活的对象将进入老年代
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数**-XX:MaxTenuringThreshold**设置
###动态对象年龄判断
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到
了MaxTenuringThreshold才能晋升老年代,**如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄.**例如Survivor空间10个对象,有6个3岁,1个4岁,3个2岁,则4岁的进入老年代空间。

空间分配担保

例子:例如Eden有8M对象,from有1M对象,如果Minor GC后还活着的对象大于to(1M)的容量,则一定需要老年代的空间担保分配。

空间分配担保策略

First,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。比如说老年代有10M,就算年轻代的对象在Minor GC后都活着也可以足够容纳(加起来才9M)如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败

First—1 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

  • First—1----1如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,这里的风险指的是:如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC
  • First—1----2如果小于那么这时要进行一次Full GC。

First—2 如果不允许,那么这时要进行一次Full GC。

虽然担保失败时绕的圈子是最大的,但大部分情况下还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁

#内存分配基本流程

1,JVM会试图为相关Java对象在年轻代的Eden区中初始化一块内存区域。

  1. 如果放入的是大对象,则可以根据参数设定直接放入年老代

2, 当Eden区空间足够时,内存申请结束。否则执行下一步。
3, Eden区空间无法再为新进对象分配内存的时候执行Minor GC,每次Minor GC之前都会执行一次空间分配担保检查策略。

3.1 ,FIRST<mark><mark>如果现有存活对象总大小大于Survivor区,则将存活对象放入年老代
3.2, Second</mark></mark>如果小于则将存活对象放到from中,并且设定年龄为1,然后from和to轮流交换角色,不停地Minor GC,每Minor GC一次,原来活着的年龄加1

  1. 如果From中年龄到一定值的(15)或者动态年龄分配方式满足跳进则进入年老代
  2. 没达到的进入to

4,直到to空间满了无法容纳,则将Minor GC后存活对象放入年老代。
5,当年老代空间不够时,JVM会在年老代进行完全的垃圾回收(Full GC)。
6,Full GC后,若Survivor区及年老代仍然无法存放从Eden区复制过来的对象,则会导致JVM无法在Eden区为新生成的对象申请内存,即出现“Out of Memory”。

参数说明

参数说明

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

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

-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的频率,这样做可以在某种程度上提高服务稳定性。