对于C、C++程序员来讲,在内存管理领域,他们既拥有每个对象的“全部权”,又担负着每个对象生命开始到终结的维护责任。html
对Java程序员来讲,在虚拟机的自动内存管理机制的帮助下,再也不须要为每一个new操做去写匹对的 delete/free 代码,不容易出现内存泄露和内存溢出的问题。java
根据《Java虚拟机规范(Java SE 7版)》规定,Java虚拟机所管理的内存将包括如下几个运行时数据区域,如图:程序员
线程私有的内存区域:web
全部线程共享的内存区域: 算法
在语言层面,建立对象(例如:clone,反序列化)一般是一个 new 关键字,而在虚拟机中,对象建立的过程是如何呢?数组
在虚拟机遇到 new 指令时:安全
1. 类加载:确保常量池中存放的是已解释的类,且对象所属类型已经初始化过,若是没有,则先执行类加载数据结构
2. 为新生对象分配内存:对象所需内存大小在类加载时能够肯定,将肯定大小的内存从Java堆中划分出来并发
3. 将分配的内存空间初始化为零值:保证对象的实例在Java代码中能够不赋值就可直接使用,能访问到这些字段的数据类型对应的零值(例如,int类型参数默认为0)oracle
4. 设置对象头:设置对象的类的元数据信息、哈希码、GC分代年龄等
5. 执行<init>方法初始化:将对象按照程序员的意愿初始化
在HotSpot虚拟机中,对象在内存中存储的布局分为3个区域,以下图所示:
除程序计数器外,JVM其余几个运行时区域均可能发生OutOfMemoryError异常。
1. 堆内存溢出,OutOfMemoryError:java heap space
缘由:Java堆用于存储对象实例,只要不断建立对象,并保证GC Roots到对象间有可达路径避免这些对象的GC,当对象数量达到堆的最大容量限制后就会产生OOM
解决方法:
2. 栈内存溢出,StackOverflowError
缘由:
解决方法:
堆中存放着几乎全部的对象实例,GC收集器在对堆进行回收前,首先要肯定哪些对象是“存活”的,哪些是“死去”的
1. 引用计数法
给每一个对象添加一个引用计数器,每当有地方引用它时,计数器 +1;引用失效时,计数器 -1。当计数器为0时对象就再也不被引用。
但主流Java虚拟机没有采用这种算法,主要缘由是:它难以解决对象之间循环引用的问题
2. 可达性分析算法
经过一系列称为“GC Roots”的对象做为起始点,从这些节点向下搜索,搜索的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到该对象不可达),则此对象是不可用的,会判断为可回收对象。
Java中,可做为 GC Roots 的对象包括:
垃圾回收主要是回收堆内存。在堆中,新生代常规应用进行一次GC通常可回收 70%~95% 的空间,永久代的 GC效率远低于此
方法区进行垃圾回收的“性价比”通常比较低,主要回收两部份内容:废弃常量和无用的类
堆外内存是把内存对象分配在Java虚拟机的堆之外的内存,包括JVM自身运行过程当中分配的内存,JNI 里分配的内存、java.nio.DirectByteBuffer 分配的内存等,这些内存直接受操做系统管理。这样能必定程度的减小GC对应用程序的影响。但 JVM 不直接管理这些堆外内存,存在 OOM 的风险,能够在 JVM 启动参数加上 -XX:MaxDirectMemorySize,对申请的堆外内存大小进行限制
DirectByteBuffer 对象表示堆外内存,DirectByteBuffer 对象中持有 Cleaner 对象,它惟一保存了堆外内存的数据、开始地址、大小和容量。在建立完后的下一次 Full GC 时, Cleaner对象会对堆外内存回收
① 标记-清除算法
标记-清除算法分为“标记”阶段和“清除”阶段。标记阶段是把全部活动对象都作上标记。清除阶段是把那些没有标记的对象(非活动对象)回收
它主要有两个不足:
② 复制算法
复制算法是将可用内存划分为大小相等的两块,每次只使用一块,当一块内存用完了,就将存活的对象复制到另外一块上,而后将已使用的内存空间一次清理掉。
这样分配内存时不用考虑内存碎片等复杂状况,但代价是将内存缩小为原来的一半。当对象存活率较高时,就要较多的复制操做,效率也会下降。
如今的商业虚拟机都采用复制算法来回收新生代。IBM专门的研究代表:新生代中对象 98% 是“朝生夕死”的,全部不须要 1:1 来划分空间,HotSpot虚拟机是将内存分为1块大的 Eden 和 2块小的 Survivor 空间,大小比例为 8:1:1。每次使用 Eden 和 其中一块 Survivor。当回收时,将 Eden 和 一块 Survivor 中的存活对象复制到另外一块 Survivor 上,最后清理掉刚才的 Eden 和 Survivor。新生代每次可利用的整个新生态内存的 90%,10% 会被浪费掉。但当每次回收多余 10% 对象存活时,即剩下一个 Survivor 空间不够时,须要老年代内存担保,这些对象将直接进入老年代中。
③ 标记-整理算法
标记-整理算法在“标记”阶段和标记-清除同样,但后续是让全部存活对象都向一端移动,而后清理掉端边界外的内存
④ 分代算法
根据对象存活周期的不一样将内存划分为几块看,通常把堆分为“年轻代”和“老年代”,根据各个年代的特色采用适当的收集算法。
新生态中,每次 GC 只有少许的对象存活,就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集
老年代中,对象存活率高、没有额外的担保空间,就必须使用“标记-清除”或“标记-整理”算法
垃圾回收算法性能:
高吞吐量和低暂停时间不可兼得。为了得到最大吞吐量,JVM 必须尽量少地运行 GC,只有在无可奈何的状况下才运行GC,好比:新生代或者老年代已经满了。可是推迟运行 GC 的结果是,每次运行 GC 时须要作的事情会不少,好比有更多的对象积累在堆上等待回收,所以每次的 GC 时间则会变高,由此引发的平均和最大暂停时间也会很高
垃圾收集器是内存回收算法的具体实现。本文主要介绍 HotSpot 虚拟机中的垃圾回收器,如图所示:
若是两个收集器之间存在连线,说明他们可搭配使用。各垃圾回收器的功能比较以下表:
该选用哪种垃圾回收器?
1. 客户端程序: 通常使用 -XX:+UseSerialGC (Serial + Serial Old). 特别注意, 当一台机器上起多个 JVM, 每一个 JVM 也能够采用这种 GC 组合
2. 吞吐率优先的服务端程序(计算密集型): -XX:+UseParallelGC 或者 -XX:+UseParallelOldGC
3. 响应时间优先的服务端程序: -XX:+UseConcMarkSweepGC
4. 响应时间优先同时也要兼顾吞吐率的服务端程序:-XX:+UseG1GC
CMS(Concurrent Mark Sweep)垃圾收集器是以最短回收停顿时间为目标的垃圾收集器。通常B/S或互联网站的服务端比较重视响应速度,但愿系统的停顿时间最短,从而带给用户更好的体验,CMS就比较符合这类应用的需求。
① 执行过程
CMS是基于 "标记-清除" 算法实现的,由上文『复制GC算法』中所描述,新生代98%对象是朝生夕死的,因此将新生代分为1个Eden和2个survivor区(默认内存大小是8:1:1),每次使用Eden和一个survivor区,回收时,将活着的对象拷贝到剩余的一个survivor区,并清理以前使用的Eden和survivor区的空间。
它运做过程分为如下几个阶段:
一、初始标记(须要 Stop The World):标记 GC Roots 能直接关联到的对象,速度很快
二、并发标记(和用户线程一块儿工做):GC Roots Tracing的过程,例如:A是GC Root关联到的对象,A引用B,A在初始阶段标记出来,这个阶段就是标记B对象
三、并发预清理(和用户线程一块儿工做):并发查找在并发标记阶段,重新生代晋升到老年代的对象、或直接在老年代分配的大对象、或被用户线程更新的对象,来减小 "从新标记" 阶段的工做量
四、从新标记(须要 Stop The World):修正『并发标记』和『并发预清理』用户线程与GC线程并发执行,用户线程产生了新对象,将这些对象从新标记。这阶段 STW 时间会比『初始标记』阶段长一些,但远比『并发标记』的时间短。暂停用户线程,GC线程从新扫描堆中的对象,进行可达性分析,标记活着的对象
五、并发清理(和用户线程一块儿工做):移除不用的对象,回收他们占用的堆空间。此时会产生新的垃圾,在这次GC没法清除,只好等到下次清理,这些垃圾名为:浮动垃圾
六、并发重置:从新设置 CMS 内部的数据结构,准备下一次 CMS 生命周期的使用
并发标记阶段修改了对象如何处理?
上述 CMS GC过程当中第3个步骤:并发预清理,如何处理并发标记阶段被修改的对象呢?初始标记阶段的引用为 A → B → C,并发标记时,引用关系由用户程序改成 A → C,B再也不引用C ,因为C在 "并发标记" 阶段没法被标记,就会被回收,而这是不容许的。
这能够经过三色标记法解决,将GC中的对象分为三种状况:
初始标记时,A 会被标记为灰色(正在扫描 A 相关),而后扫描 A 的引用,将 B 标记为灰色,而后 A 就扫描完成了,变为黑色
并发标记时,若是用户线程将 A 引用改成了 C,即 A → C,此时 CMS 在写屏障(Write Barrier)里发现有一个白色对象的引用(C)被赋值到黑色对象(A)的字段里,那就会将 C 这个白色对象设置为灰色,即增量更新(Imcremental update)。
出现老年代引用新生代的对象,GC 时如何处理?
JVM采用卡片标记(Card Marking)方法,避免 Minor GC 时须要扫描整个老年代。作法是:将老年代按照必定大小分片,每一片对应 Cards 中一项,若是老年代的对象发生了修改或指向了新生代对象,就将这个老年代的 Card 标记为 dirty。Young GC 时,dirty card 加入待扫描的 GC Roots 范围,避免扫描整个老年代
优势:
一、并发收集、低停顿,Sun公司的一些官方文档也称之为并发低停顿收集器(Concurrent Low Pause Collector)
缺点:
一、对CPU资源很是敏感:在并发阶段,它虽然不会致使用户线程停顿,但会由于占用一部分线程(或CPU资源)而致使应用程序变慢,总吞吐量下降
二、产生空间碎片:基于“标记-清除”算法实现,意味着收集结束后会有大量空间碎片产生,给大对象分配带来麻烦
三、须要更大的堆空间:CMS标记阶段应用程序还在继续执行,就会有堆空间继续分配的状况,为保证 CMS 将堆回收完以前还有空间分配给正在运行的程序,必须预留一部分空间
③ CMS调优策略
-XX:CMSInitiatingOccupancyFraction=70 : 该值表明老年代堆空间的使用率,默认值是92,假如设置为70,就表示第一次 CMS 垃圾收集会在老年代占用 70% 时触发。过大会使 STW 时间过程,太小会影响吞吐率
-XX:+UseCMSCompactAtFullCollection,-XX:CMSFullGCsBeforeCompaction=4:执行4次不压缩的 Full GC 后,会执行一次内存压缩的过程,用来消除内存碎片
-XX:+ConcGCThreads:并发 CMS 过程运行时的线程数,CMS 默认回收线程数是 (CPU+3) / 4。更多的线程会加快并发垃圾回收过程,但会带来额外的同步开销。
年轻代调优:Young GC 频次高,则增大新生代;Young GC 时间长,则减小新生代。尽可能在 Young GC 时候回收大部分垃圾
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,G1的设计初衷是最小化 STW 中断时间,一般限制 GC 停顿时间比最大化吞吐率更重要。在Java9里,G1已经成为默认的垃圾回收器。
① 执行过程
G1的内存布局和其余垃圾收集器有很大区别,它将整个Java堆分为 n 个大小相等的 Region,每一个 Region 占有一块连续的虚拟内存地址。新生代和老年代再也不是物理隔离,而是一部分 Region 的集合。
Region的大小能够经过 -XX:G1HeapRegionSize 指定,若是未设置,默认将堆内存平均分为 2048 份。G1仍属于分代收集器,除了Eden、Survivor、Old区外,还有 Humongous 区用于专门存放巨型对象(一个对象占用空间>50%分区容量),减小短时间存在的巨型对象对垃圾收集器形成的负面影响。
G1 的运做过程分为如下几个步骤:
一、全局并发标记:基于 STAB(snapshot-at-the-beginning)形式的并发标记,标记完成后,G1 基本知道了哪一个区域是空的,它首先会收集哪些产出大量空闲空间的区域,这也是它命名为 Garbage-First 的缘由
1.1 初始标记(STW,耗时很短):标记 GC Roots 能直接关联到的对象,将它们的字段压入扫描栈
1.2 并发标记(与用户线程并发执行,耗时较长):GC 线程不断从扫描栈中取出引用,而后递归标记,直至扫描栈清空
1.3 最终标记(STW):从新标记『并发标记』期间因用户程序执行而致使引用发生变更的那一部分标记(写入屏障 Write Barrier 标记的对象)
1.4 清理(STW):统计各个 Region 被标记存活的对象有多少,若是发现没有存活对象的 Region,就会将其总体回收到可分配的 Region 中
二、拷贝存活对象:将一部分 Region 里的存活对象拷贝到空 Region 里去,而后回收本来的 Region 的空间。
G1 的 GC 能够分为 Young GC 和 Mixed GC 两种类型。Young GC 是选定全部新生态的 Region,经过控制新生代的 Region 个数控制 Young GC 的开销。Mixed GC 是选定全部新生代里的 Region,外加根据『全局并发标记』统计的收益较高的若干老年代 Region,在用户指定的停顿时间内尽量选择收益较高的老年代 Region。G1 里不存在 Full GC,老年代的收集全靠 Mixed GC 来完成。
在 G1 中,使用 Rememberd Set 跟踪 Region 内的对象引用,来避免全堆扫描的。每一个 Region 都有一个与之对应的 Rememberd Set,当程序对 Reference 类型的数据进行写操做时,会产生 Write Barrier 暂停中断写操做,检查 Reference 引用的对象是否处于不一样的 Region 之中,若是是,便经过 CardTable 把相关引用信息记录到被引用对象所属 Region 的 Rememberd Set 中。当 GC 时,在GC Root 的枚举范围中加入 Rememberd Set 便可保证不对全堆扫描,也不会遗漏。
② G1与CMS的比较
G1的设计目标是取消CMS收集器,G1与CMS相比,有一些显而易见的优势:
一、简单可行的性能调优:-XX:+UseG1GC -Xmx32g,使用这两个参数便可应用于生产环境,表示开启G1,堆最大内存为32G;-XX:MaxGCPauseMillis=n 使用这个参数可设置指望的GC中暂停时间。取消老年代的物理空间划分,无需对每一个代的空间进行大小设置
二、可预测的 STW 停顿时间:G1除了追求低停顿外,还能创建可预测的停顿时间模型,能让使用者明确指定 GC 的停顿时间不超过 n 毫秒。这是经过跟踪各个 Region 里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护优先列表,每次根据容许的收集时间,优先回收价值最大的Region,保证了 G1 能在有限的时间内能够获取尽量高的收集效率
三、空间整合:G1 的两个 Region 之间是基于『复制』算法实现,在运做期间不会产生内存碎片,分配大对象时不会由于没法找到连续空间而提早出发 Full GC
③ CMS调优策略
-XX:MaxGCPauseMillis=n:设置GC时最大暂停时间,这个目标不必定能知足,JVM会尽最大努力实现它,不建议设置的太小(<50ms)
-XX:InitiatingHeapOccupancyPercent=n:触发G1启动 Mixed GC,表示垃圾对象在整个 G1 堆内存空间的占比
避免使用 -Xmn 或 -XX:NewRatio等其余显式设置年轻代大小的选项,固定年轻代大小会覆盖暂停时间目标