Java内存结构模型和垃圾回收机制

这篇博客整合了好多位博主的知识分享,同时加上了不少自己的理解。在这里首先感谢各位前辈对这方面知识的总结归纳。

在开始记录相关的知识前,先放上一张非常好的图,很好的描述了JVM的内存模型:

《Java内存结构模型和垃圾回收机制》


一、JVM内存模型

首先来看一下JVM内存模型总体架构图:

《Java内存结构模型和垃圾回收机制》

程序计数器

多线程时,当线程数超过CPU数量或CPU内核数量,线程之间就要根据时间片轮询抢夺CPU时间资源。因此每个线程有

要有一个独立的程序计数器,记录下一条要运行的指令。线程私有的内存区域。如果执行的是JAVA方法,计数器记录正在执行

的java字节码地址,如果执行的是native方法,则计数器为空。

虚拟机栈

每个JVM线程拥有一个私有的栈,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个

桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多

少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求

的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出

OutofMemoryError。

使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

《Java内存结构模型和垃圾回收机制》

本地方法区

和虚拟机栈功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C实现的。

JAVA堆

JVM只有一个为所有线程所共享的堆,所有的类实例和数组都是在堆中创建的。垃圾回收的主要区域。可以分为新生代和

老年代(tenured)。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入

老年代。新生代又可进一步细分为eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。刚创建的对象都

放入eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代(tenured)。

《Java内存结构模型和垃圾回收机制》

  新生代:

  所有新生成的对象首先都是放在年轻代。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代一般分3个区,1个Eden区,2个Survivor区(from 和 to)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当一个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另一个Survivor区也满了的时候,从前一个Survivor区复制过来的并且此时还存活的对象,将可能被复制到年老代。

  2个Survivor区是对称的,没有先后关系,所以同一个Survivor区中可能同时存在从Eden区复制过来对象,和从另一个Survivor区复制过来的对象;而复制到年老区的只有从另一个Survivor区过来的对象。而且,因为需要交换的原因,Survivor区至少有一个是空的。特殊的情况下,根据程序需要,Survivor区是可以配置为多个的(多于2个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

  针对年轻代的垃圾回收即 Young GC。

  老年代;

  在年轻代中经历了N次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

针对年老代的垃圾回收即 Full GC。

  持久代:

  也就是下面将要说道的方法区。用于存放静态类型数据,如 Java Class, Method 等。持久代对垃圾回收没有显著影响。但是有些应用可能动态生成或调用一些Class,例如 Hibernate CGLib 等,在这种时候往往需要设置一个比较大的持久代空间来存放这些运行过程中动态增加的类型。

方法区

JVM只有一个为所有的线程所共享的方法区。它存储类结构,存放被虚拟机加载的类的元数据信息,例如运行时常量池,成员

和方法数据以及方法、构造方法的代码也称为永久代。如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。

回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收。

所以,当一组对象生成时,内存申请过程如下:

  1. JVM会试图为相关Java对象在年轻代的Eden区中初始化一块内存区域。
  2. 当Eden区空间足够时,内存申请结束。否则执行下一步。
  3. JVM试图释放在Eden区中所有不活跃的对象(Young GC)。释放后若Eden空间仍然不足以放入新对象,JVM则试图将部分Eden区中活跃对象放入Survivor区。
  4. Survivor区被用来作为Eden区及年老代的中间交换区域。当年老代空间足够时,Survivor区中存活了一定次数的对象会被移到年老代。
  5. 当年老代空间不够时,JVM会在年老代进行完全的垃圾回收(Full GC)。
  6. Full GC后,若Survivor区及年老代仍然无法存放从Eden区复制过来的对象,则会导致JVM无法在Eden区为新生成的对象申请内存,即出现“Out of Memory”。

OOM(“Out of Memory”)异常一般主要有如下2种原因

  1. 年老代溢出,表现为:java.lang.OutOfMemoryError:Javaheapspace
这是最常见的情况,产生的原因可能是:设置的内存参数Xmx过小或程序的内存泄露及使用不当问题。
例如循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。还有的时候虽然不会报内存溢出,却会使系统不间断的垃圾回收,也无法处理其它请求。这种情况下除了检查程序、打印堆内存等方法排查,还可以借助一些内存分析工具,比如MAT就很不错。

2. 持久代溢出,表现为:java.lang.OutOfMemoryError:PermGenspace

通常由于持久代设置过小,动态加载了大量Java类而导致溢出,解决办法唯有将参数 -XX:MaxPermSize 调大(一般256m能满足绝大多数应用程序需求)。将部分Java类放到容器共享区(例如Tomcat share lib)去加载的办法也是一个思路,但前提是容器里部署了多个应用,且这些应用有大量的共享类库。

二、JAVA内存分配机制

在Java程序中不能显式地分配和注销内存。有些人把相关的对象设置为null或者调用System.gc()来试图显式地清理内存。设置

为null至少没什么坏处,但是调用System.gc()会显著地影响系统性能,必须彻底杜绝(还好,我还没有见到NHN的哪个开发者调用

这个方法)。

在Java中,开发人员无法直接在程序代码中清理内存,而是由垃圾回收器自动寻找不必要的垃圾对象,并且清理掉他们。垃圾回收

器会在下面两种假设(hypotheses)成立的情况下被创建(称之为假设不如改为推测(suppositions)或者前提(preconditions))。

  • 大多数对象会很快变得不可达
  • 只有很少的由老对象(创建时间较长的对象)指向新生对象的引用

这些假设我们称之为
弱年代假设
( 
weak generational hypothesis
)。为了强化这一假设,HotSpot虚拟机将其物理上划分

为两个–新生代(young generation)和老年代(old generation)。

新生代(Young generation): 绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以

很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。

老年代(Old generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。

也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC

(或者”full GC“)

请看下面这个图表。

《Java内存结构模型和垃圾回收机制》

 图1 : GC 空间 & 数据流

上图中的持久代( permanent generation )也被称为方法区method area)。他用来保存类常量以及字符串常量。因此,

这个区域不是用来永久的存储那些从老年代存活下来的对象。这个区域也可能发生GC。并且发生在这个区域上的GC事件也会被

算为major GC。

有些人可能会问:

如果老年代的对象需要引用一个新生代的对象,会发生什么呢?

为了解决这个问题,老年代中存在一个”card table“,他是一个512 byte大小的块。所有老年代的对象指向新生代对象的引用都

会被记录在这个表中。当针对新生代执行GC的时候,只需要查询card table来决定是否可以被收集,而不用查询整个老年代。这个

card table由一个write barrier来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但GC的整体时间

被显著的减少。

                        《Java内存结构模型和垃圾回收机制》

图 2: Card Table 结构

 新生代的构成

为了更好地理解GC,我们现在来学习新生代,新生代是用来保存那些第一次被创建的对象,他可以被分为三个空间

  •  一个伊甸园空间(Eden 
  •  两个幸存者空间(Survivor )

一共有三个空间,其中包含两个幸存者空间。每个空间的执行顺序如下:

  1. 绝大多数刚刚被创建的对象会存放在伊甸园空间。
  2. 在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。
  3.   此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
  4.  当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。
  5. 在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

如果你仔细观察这些步骤就会发现,其中一个幸存者空间必须保持是空的。如果两个幸存者空间都有数据,或者两个空间都

是空的,那一定标志着你的系统出现了某种错误。

过频繁的minor GC将数据移动到老年代的过程可以用下图来描述:

《Java内存结构模型和垃圾回收机制》

图 3: GC执行前后对比

需要注意的是HotSpot虚拟机使用了两种技术来加快内存分配。他们分别是是”bump-the-pointer“和“TLABs

(Thread-Local Allocation Buffers)”。Bump-the-pointer技术跟踪在伊甸园空间创建的最后一个对象。这个对象会被放在伊甸

园空间的顶部。如果之后再需要创建对象,只需要检查伊甸园空间是否有足够的剩余空间。如果有足够的空间,对象就会被创建在

伊甸园空间,并且被放置在顶部。这样以来,每次创建新的对象时,只需要检查最后被创建的对象。这将极大地加快内存分配速度。

但是,如果我们在多线程的情况下,事情将截然不同。如果想要以线程安全的方式以多线程在伊甸园空间存储对象,不可避免的需要

加锁,而这将极大地的影响性能。TLABs 是HotSpot虚拟机针对这一问题的解决方案。该方案为每一个线程在伊甸园空间分配一块

独享的空间,这样每个线程只访问他们自己的TLAB空间,再与bump-the-pointer技术结合可以在不加锁的情况下分配内存。

以上是针对新生代空间GC技术的简要介绍,你不需要刻意记住我刚刚提到的两种技术。不知道他们不会对你产生什么影响,但是

请务必记住在对象刚刚被创建之后,是保存在伊甸园空间的。那些长期存活的对象会经由幸存者空间转存在老年代空间。

三、gc处理机制

老年代空间的GC事件基本上是在空间已满时发生,执行的过程根据GC类型不同而不同,因此,了解不同的GC类型将有助于你理解本节的内容。

JDK7一共有5种GC类型:

  1. Serial GC
  2. Parallel GC
  3. Parallel Old GC (Parallel Compacting GC)
  4. Concurrent Mark & Sweep GC  (or “CMS”)
  5. Garbage First (G1) GC



其中,Serial GC不应该被用在服务器上。这种GC类型在单核CPU的桌面电脑时代就存在了。使用Serial GC会显著的降低应用

的性能指标。现在,让我们共同学习每一种GC类型


1. Serial GC (-XX:+UseSerialGC)

新生代空间的GC方式我们在前面已经介绍过了,在老年代空间中的GC采取称之为”mark-sweep-compact“的算法。

  1. 算法的第一步是标记老年代中依然存活对象。(标记)
  2. 第二步,从头开始检查堆内存空间,并且只留下依然幸存的对象。(清理)

最后一步,从头开始,顺序地填满堆内存空间,并且将对内存空间分成两部分:一个保存着对象,另一个空着(压缩)。

-XX:+UseSerialGC:设置串行收集器。

2. Parallel GC (-XX:+UseParallelGC)

《Java内存结构模型和垃圾回收机制》

图 4: Serial GC 与 Parallel GC的区别

从上图中,你可以轻易地看出serial GC和parallel GC的区别,serial GC只使用一个线程执行GC,而parallel GC使用多个线程,

因此parallel GC更高效。这种GC在内存充足以及多核的情况下会很有用,因此我们也称之为”throughput GC“。

3. Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC在JDK5之后出现。与parallel GC相比,唯一的区别在于针对老年代的GC算法。Parallel Old GC分为三步:

标记-汇总-压缩(mark – summary – compaction)。汇总(summary)步骤与清理(sweep)的不同之处在于,其将依然幸存

的对象分发到GC预先处理好的不同区域,算法相对清理来说略微复杂一点。

4. CMS GC (-XX:+UseConcMarkSweepGC)

《Java内存结构模型和垃圾回收机制》

图 5: Serial GC & CMS GC

就像你从上图看到的那样, CMS GC比我之前解释的各种算法都要复杂很多。第一步初始化标记(initial mark) 比较简单。

这一步骤只是查找那些距离类加载器最近的幸存对象。因此,停顿的时间非常短暂。在之后的并行标记( concurrent mark )

步骤,所有被幸存对象引用的对象会被确认是否已经被追踪和校验。这一步的不同之处在于,在标记的过程中,其他的线程依然

在执行。在重新标记(remark)步骤,会再次检查那些在并行标记步骤中增加或者删除的与幸存对象引用的对象。最后,在并行

交换( concurrent sweep )步骤,转交垃圾回收过程处理。垃圾回收工作会在其他线程的执行过程中展开。一旦采取了这种GC

类型,由GC导致的暂停时间会极其短暂。CMS GC也被称为低延迟GC。它经常被用在那些对于响应时间要求十分苛刻的应用之上。

当然,这种GC类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:

  •  它会比其他GC类型占用更多的内存和CPU
  •  默认情况下不支持压缩步骤

在使用这个GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他

任何GC类型都长,你需要考虑压缩任务的发生频率以及执行时间。

5. G1 GC

最后,我们来学习垃圾回收优先(G1)GC类型。

《Java内存结构模型和垃圾回收机制》

图 6:  G1 GC的结构

 如果你想要理解G1,首先你要忘记你所学过的新生代和老年代的概念。正如你在上图所看到的,每个对象被分配到不同的格子,

随后GC执行。当一个区域装满之后,对象被分配到另一个区域,并执行GC。这中间不再有从新生代移动到老年代的三个步骤。

这个类型是为了替代CMS GC而被创建的,因为CMS GC在长时间持续运作时会产生很多问题。

G1最大的好处是性能,他比我们在上面讨论过的任何一种GC都要快。但是在JDK 6中,他还只是一个早期试用版本。在JDK7之后

才由官方正式发布。就我个人看来,NHN在将JDK 7正式投入商用之前需要很长的一段测试期(至少一年)。因此你可能需要再

等一段时间。并且,我也听过几次使用了JDK 6中的G1而导致Java虚拟机宕机的事件。请耐心的等到它更稳定吧。


四、相关配置参数解读

这一块的设置参数有很多,我先罗列一部分,有需要的再后续进行补充。

堆设置  

-Xms :初始堆大小  

-Xmx :最大堆大小   -Xmn :设置年轻代大小。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统 性能影响较大,Sun官方推荐配置为整个堆的3/8。 -Xss :设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个 值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 -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 :设置持久代大小  

收集器设置  

-XX:+UseSerialGC :设置串行收集器  

-XX:+UseParallelGC :设置并行收集器  

-XX:+UseParalledlOldGC :设置并行年老代收集器  

-XX:+UseConcMarkSweepGC :设置并发收集器  

垃圾回收统计信息  

-XX:+PrintHeapAtGC GC的heap详情 

-XX:+PrintGCDetails  GC详情 

-XX:+PrintGCTimeStamps  打印GC时间信息 

-XX:+PrintTenuringDistribution    打印年龄信息等 -XX:+HandlePromotionFailure   老年代分配担保(true  or false)
并行收集器设置  

-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。  

-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间  

-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)  

并发收集器设置  

-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。  

-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

 

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/wangqyoho/article/details/52203573
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞