Java内存区域和垃圾回收

  1. Java运行时分区介绍
  2. 对象创建过程
  3. 判断对象死亡的引用计数算法和可达性算法
  4. 垃圾收集算法介绍
  5. HotSpot中的垃圾回收器

1、java虚拟机在运行的过程中,会将所管理的内存分为不同的几个区域,不同的区域有不同的创建时间和销毁时间,以及不同的区域会依赖线程的启动而建立和销毁,具体的内存分区就如下图:

在这里插入图片描述

  • 程序计数器:此内存空间是一个线程私有的内存空间,每个线程都相互独立。主要用于保存当前线程执行的字节码行数。因为Java虚拟机多线程的执行是线程流转切换执行,为了在切换回到线程正确的执行位置,每个线程必须有一个独立的程序计数器。
  • 虚拟机栈:同程序计数器一样也是线程是私有的,主要描述的是Java方法内存模型。每个方法执行的同时回创建一个栈帧用于存储局部变量表,操作数栈,动态链接、方法出口等信息,每个方法的创建个执行都是一个入栈和出栈的过程。这个要说明的java栈主要指栈帧中的局部变量表,局部变量表主要保存的是编译期可知的各种基本数据类型,对象的引用和returnAdderss类型。
    在这里插入图片描述
  • 本地方法区:这个其实和虚拟机栈的作用是非常相似的,只不过这个作用对象是Native方法。
  • Java堆:这个是虚拟机管理内存中最大的一部分。也是回收器管理的主要区域。Java堆区的内存由所有线程共享,在虚拟机启动的时候创建。主要保存的是:对象实例和数组。由于现在收集器采用的分代回收,故将Java堆中对象分为:新生代和老年代。这个会在后面根据不同的状态使用不同的收集器回收垃圾。
  • 方法区:这个区也是内存共享区域。它的作用是存储已经被加载的类信息、常量、静态变量、即时编译后的代码等数据。这个区域很少被收集器回收,一般我们称为”永久代“。

2、这里我想讲一下在堆中对象的创建过程和内存的分配,主要是我们知道了虚拟机对内存的管理和分区后,什么时候分配内存,内存分配多少那就是对象创建时候所要知道的。首先我们知道Java创建一个对象有4种途径(new 、克隆、反射、序列化)那我们讨论new关键字生成一个对象的时候的过程

  • 当虚拟机遇到new指令的时候,首先检查这个指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有那么就执行类加载的过程
  • 加载过后虚拟机为新生对象分配内存,对象内存的大小在类加载完后就可以确认,那知道了所需要的内存大小,虚拟机就会在Java堆中划一块确定大小的内存分配给这个对象。那这个时候内存空间中使用过的内存和没有使用的内存位置状态可能会有两种:
    1、Java堆中内存是规整的。使用过的内存在一块区域,没有使用的在另一个区域中间使用一个指针隔开。
    2、Java堆中内存是不规整的,没有使用的内存以碎片的形式存在。
    如果堆内存是规整的,那么申请一个内存大小的时候,直接将指针向空闲区域挪动一个与对象内存大小相等的内存距离
    如果不是规整的,那么虚拟机必须维持一个列表,记录哪些内存块是可用的,在分配的时候将在列表上找到一个足够大小的空间划分给对象实例。

3、那对象创建后,后面必定会涉及到对象的回收,那就是会有几个问题需要了解清楚

  • 什么对象会回收
  • 什么时候回收
  • 怎么回收

首先回答第一个问题,什么对象会回收:之前介绍几个内存区域,其中程序计数器、虚拟机栈、本地方法栈3个区域内存的分配和回收,都是跟随线程的生命周期和方法的结束内存就会回收了。并且栈帧的内存大小分配基本上是在类结构确定下来时就已知的,故这几个区域的内存分配和回收都具备确定性,这几个区域的内存就不过多的考虑回收的问题。只有java堆中的内存,只要程序处于运行期间才知道会创建哪些对象,这部分内存的分配和回收是虚拟机特别关注的也是接下来要说的地方。

如何判断对象已经死亡:那么死亡后才会开始回收机制。那判断对象死亡的算法有两种

  1. 引用计数算法:计数就是给对象添加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0 的对象就是不可能再被使用的。这个算法有个弊端就是很难解决相互之间循环引用的问题,优点就是实现简单,判定效率高。
  2. 可达性算法:这个算法基本思路是根据一系列称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明对象是不可用。判定一个对象是否能是一个GC Roots对象包括以下几种:
  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中静态属性引用得对象
  • 方法区中常量引用的对象
  • 本地方法中JNI(一般说的Native方法)引用的对象
    Java虚拟机在对对象做可达性算法分析的时候,当一个对象要宣告死亡至少经过两次的标记过程。当对象在进行可达性分析后发现并没有GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,如果这个对象判定为有必要执行finalize(),那么这个对象将被放到一个叫F-Queue的队列中,并等待一个低优先级的线程去执行它,之后GC将对F-Queue中的对象再进行一次标记,如果还没有于一个对象建立连接,那么就真的被回收了。这个方法是对象最后被救活的可能,对于finalize方法以及、final、finally、这也是Java面试经常会问的问题。

4、首先常见的垃圾回算法有以下几种,不同的垃圾算法适合不同的回收区域或者针对不同的对象年代,也是后面垃圾回收器的实现基础。
这里先介绍下虚拟机对堆的分代,虚拟机将堆区划分为新生代老年代堆大小=新生代+老年代;(新生代占堆空间的1/3、老年代占堆空间2/3
新生代又被划分成Eden空间、From SurvivorTo Survivor(8:1:1)三块区域新生代对象98%都是快速的创建又很快被回收,故适合使用复制算法来进行快速回收
当对象在eden(其中包括一个survivor,假如是from),当此对象经过一次minor gc后仍然存活,并且能够被另外一块survivor所容纳(这里survivor则是to了),则使用复制算法将这些仍然存活的对象复制到to survior区域中,然后清理掉eden和from survivor区域,并将这些存活的对象年龄+1,以后对象在survivor中每熬过一次gc则增加1,当年龄达到某个值时(默认15,通过设置参数-xx:maxtenuringThreshold来设置),这些对象就会成为老年代!
老年代,对象的存活率较高,在分配内存的时候需要较大的内存空间,只有使用标记整理算法既能得到连续的内存空间,相对于复制算法又能避免内存的浪费

  • 标记-清除:这个算法回收对象分为两步,首先标记要回收的对象,然后再集体清除。这个算法主要的不足点第一个是效率问题,标记和清除都不是很高效;第二点就是清除后会产生许多不连续的内存空间。当再继续给对象分配内存的时候,遇到较大的对象空间无法分配时,将提前触发另一次垃圾回收动作。
  • 复制算法:将内存空间分为两块,每次使用其中的一块,当其中一块使用完毕后,将还存活的对象复制到另一个内存空间中,将前面一个内存全部清除,反复进行这样的操作。这样的回收方式的好处就是保证了内存区域没有内存碎片,在内存分配的过程中只需要移动堆顶指针,按顺序分配就可以实现简单。但这个算法缺点就是可使用内存大小缩小。
  • 标记-整理:不同的算法可以针对不同的内存区域,如果将复制算法使用到新生代那是恰到好处,那么当将其使用到老年代,那就一点都不合适了。那根据老年代的特点,就有了标记整理算法,这个算法实现和标记-清除过程一样,首先对需要回收的对象进行标记,然后将存活的对象往内存的一段移动,然后将边界以外的内存直接清除掉,得到的将是一块连续的可分配内存。
  • 分代回收:分代回收算法是根据对象存活的周期不同将内存划分为几块,一般把堆划分为新生代和老年代,这样根据各个年代的特点采用最合算的搜集算法

5、这里讨论的搜集器基于JDK1.7 Upadte 14之后的HotSpot虚拟机,虚拟机包含的所有收集器入如下图

在这里插入图片描述

  1. Serial 收集器
    是最基本发展历史最悠久的收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
    在这里插入图片描述
    Serial是一个单线程收集器,适用于Client模式下的垃圾回收器
  2. ParNew收集器
    这个回收器是Serial的多线程版本,除了使用多线程进行垃圾回收外,其余行为包含Serial收集器可用的所有参数,收集算法,Stop The World、对象分配规则,回收策略等都与Serial收集器完全一样。
    在这里插入图片描述
    ParNew回收器是Server模式下虚拟机回收新生代的首选,还有一个要注意的是只有这个收集器和Serial能配合CMS回收器一起使用

3 .Parallel Scavenge收集器
这是一个新生代收集器,它使用的是复制算法的收集器,也是并行的多线程收集器。这个收集器的目的是达到一个可控制吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,既吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行时间100分钟,垃圾收集时间1分钟,吞吐量就是99%
主要适用于在后台运算而不需要太多交互的任务,Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是

  • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio:直接设置吞吐量大小
    Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy,当这个参数打开后就不需要手工指定新生代对象的大小、Eden与Survivor区的比例,晋升老年代对象的大小等细节参数,只需要吧基本的内存数据设置好(如-Xmx),然后使用MacGVPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标。
    自适应调节策略也是Parallel Scavenge收集器和ParNew收集器的重要区别

4.Serial old收集器
Serial old收集器是Serial收集器的老年版本,同样是一个单线程收集器,其使用的是标记-整理算法,主要是给Client模式下的虚拟机使用
如果在Server模式下,主要有两个用处

  1. 在JDK1.5之前与.Parallel Scavenge收集器配合使用
  2. 作为CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure 时使用。
    在这里插入图片描述

5.Parallel Old收集器
Parallel Old是Parallel Scavenge搜集器的老年版本,使用多线程 标记-整理算法,在JDK1.6后提供

6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求
CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.
在这里插入图片描述

CMS收集器主要优点:并发收集,低停顿
CMS三个明显的缺点:
(1)CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数(Cpu数量+3)/4 ,当CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想
(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中蓝年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阀值已经提升至92%。
(3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)

  1. G1收集器
    G1收集器的优势:
    (1)并行与并发
    (2)分代收集
    (3)空间整理 (标记整理算法,复制算法)
    (4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)

使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的灰机效率
G1 内存“化整为零”的思路
在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:
(1)初始标记
(2)并发标记
(3)最终标记
(4)筛选回收
在这里插入图片描述

深入理解Java虚拟机
https://www.cnblogs.com/chengxuyuanzhilu/p/7088316.html
https://www.cnblogs.com/KingIceMou/p/6960767.html

http://blog.csdn.net/chaihuasong/article/details/8289367
http://www.cnblogs.com/nathan909/p/5372981.html
https://www.zhihu.com/question/20170470
http://blog.csdn.net/chaihuasong/article/details/8289367
https://www.zhihu.com/question/24217234
https://zhuanlan.zhihu.com/p/27176914
http://blog.csdn.net/linghu_java/article/details/39480761/
https://www.jianshu.com/p/c4b283848970

猜你喜欢

转载自blog.csdn.net/oheg2010/article/details/82826014