java 虚拟机内存回收相关笔记

一句话简述回收算法经历

标记-清除算法:最基础的回收算法,主要划分为标记,清除两个阶段;后续的算法都是基于这种思路并对其不足之处进行修改而得;

标记-清除算法:问题一为效率问题:标记和清除两个过程的效率都不高;另一个是空间问题:标记清除之后产生大量不连续的内存碎片(导致后期分配大对象不得不提前触发垃圾回收);

复制算法:为了解决效率问题将可用内存划分大小相等的两块,每次只使用其中一块,当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后把已使用的那一半内存空间清理掉;

1
2
3
4
IBM公司发行百分之98%的对象都是马上清理掉的,所以并不需要1:1划分,而是将内存划分为较大的Eden空间和两块较小的Survivor空间
每次使用Eden+Survivor空间,若还存活复制到另一块Survivor空间上,清理刚刚的Eden和Survivor空间,虚拟机默认Eden和Survivor为8:1
如果另一块Survivor没有足够空间存放上一次新生代搜集下来的存货对象,将通过分配担保直接进入老年代
否则将等到年龄>15进入老年代,或相同年龄所有对象大小的总和大于Survivor空间的一半,年龄对于或等于年龄的对象就可以直接进入老年代

标记-整理算法:复制算法在对象存活率较高时就进行较多的复制操作,效率变低且如果不想浪费50%空间就需要额外的空间担保来应对内存中所有对象100%存活的极端情况,所以老年代不选用这类算法
根据老年代的特色,提出了标记-整理算法(标记不存活对象,让所有存活对象向一端移动,然后直接清理掉边界以外的内存)


对象的内存布局

对象在内存中分为:对象头、实例数据和对齐填充

对象头

实例数据:真正存储的有效信息

对齐填充:占位符的作用


回收算法

标记-清除(Mark-Sweep)算法

1
2
效率问题:标记和清除效率都不高
空间问题:空间碎片太多导致分配大内存对象不够而提前触发一次回收

复制(Copying)算法

1
2
3
4
为解决空间碎片太多问题,复制算法出现
划分两块大小相等可不用考虑内存碎片情况
但由于新对象是朝生夕死,所以不1:1划分空间而是出Eden空间和两块较小的Survivor空间, 8:1:1
Eden和Survivor存活的移到另一块Survivor空间,并删除Eden和Survivor

标记-整理(Mark-Compact)算法

1
2
对象存活率较高就进行较多的复制操作,效率会变低
不想浪费50%的空间就需要额外的空间进行分配担保(100%对象都存活的情况)

分代收集(Generational Collection)

标记-清除

标记清除的算法最简单,主要是标记出来需要回收的对象,然后然后把这些对象在内存的信息清除。

标记-整理(压缩)

标记-清除的算法之上进行一下压缩空间,重新移动对象的过程。

因为标记清除算法会导致很多的留下来的内存空间碎片,随着碎片的增多,严重影响内存读写的性能,所以在标记-清除之后,会对内存的碎片进行整理。最简单的整理就是把对象压缩到一边,留出另一边的空间。由于压缩空间需要一定的时间,会影响垃圾收集的时间。

复制

内存分配为两个空间,一个空间(A)用来负责装载正常的对象信息,另外一个内存空间(B)是垃圾回收用的。

每次把空间A中存活的对象全部复制到空间B里面,在一次性的把空间A删除。

这个算法在效率上比标记-清除-压缩高,但是需要两块空间,对内存要求比较大,内存的利用率比较低。

适用于老年代短生存期的对象,持续复制长生存期的对象则导致效率降低

在HotSpot里,考虑到大部分对象存活时间很短,将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,且可能存在空间不够需要分配担保的情况,所以适合在新生代使用;

分代收集算法

一般把Java堆分新生代和老年代,在新生代用复制算法
在老年代用标记-清理或标记-整理算法,是现代虚拟机通常采用的算法。






收集器

Serial收集器(年轻代)

单线程,采取复制算法,并且在它干活的时候它会Stop The World

ParNew收集器(年轻代)

Serial收集器的多线程版本,其余几乎和Serial一样,采取复制算法,并且在它干活的时候它会Stop The World

Parallel Scavenge参数 描述
MaxGCPauseMillis (毫秒数) 收集器将尽力保证内存回收花费的时间不超过设定值, 但如果太小将会导致GC的频率增加.
GCTimeRatio (整数:0 < GCTimeRatio < 100) 是垃圾收集时间占总时间的比率
XX:+UseAdaptiveSizePolicy 启用GC自适应的调节策略: 不再需要手工指定-Xmn-XX:SurvivorRatio-XX:PretenureSizeThreshold等细节参数, VM会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量

Parallel Scavenge收集器(年轻代)

1
吞吐量(Throughput) = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

采取复制算法,并行的多线程收集器

  • –XX:MaxGCPauseMillis(最大垃圾收集停顿时间)
  • –XX:GCTimeRatio(吞吐量大小)

Serial Old收集器(年老代)

单线程,采取标记-整理算法 ,并且在它干活的时候它会Stop The World

Parallel Old收集器(年老代)

多线程,采取标记-整理算法,1.6中才开始提供

在注重吞吐量和CPU敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old

CMS(Concurrent Mark Sweep)收集器(年老代)

运作过程:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)

该算法会有以下两个问题:

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

CMS基于标记-清除算法,所以会产生大量空间碎片,为此它提供两个参数用来缓解这个问题

  • -XX:+UseCMSCompactAtFullCollection

Full GC后进行碎片整理,内存整理过程无法并发

  • -XX:CMSFullGCsBeforeCompaction

执行多少次不压缩的Full GC后跟着来一次带压缩的

相关建议

  1. CMS默认启动的回收线程数=(CPU数目+3)4

    当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.

  2. 无法处理浮动垃圾, 可能出现Promotion FailureConcurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了).

  3. 最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).

G1收集器

一款面向服务端应用的垃圾收集器,后续会替换掉CMS垃圾收集器

步骤

与CMS的过程比较类似

  • 初始标记(标记一下GC Roots能直接关联的对象并修改TAMS值,需要STW但耗时很短)
  • 并发标记(从GC Root从堆中对象进行可达性分析找存活的对象,耗时较长但可以与用户线程并发执行)
  • 最终标记(为了修正并发标记期间产生变动的那一部分标记记录,这一期间的变化记录在Remembered
  • Set Log里,然后合并到Remembered Set里,该阶段需要STW但是可并行执行)
  • 筛选回收(对各个Region回收价值排序,根据用户期望的GC停顿时间制定回收计划来回收)

特点

  • 并行与并发(充分利用多核多CPU缩短STW时间)
  • 分代收集(独立管理整个Java堆,但针对不同年龄的对象采取不同的策略)
  • 空间整合(局部看是基于复制算法,从整体来看是基于标记-整理算法,都不会产生内存碎片)
  • 可预测的停顿(可以明确指定在一个长度为M毫秒的时间片内垃圾收集不会超过N毫秒)

新生代收集

G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.

G1的新生代收集特点如下:

  • 一整块堆内存被分为多个Regions.
  • 存活对象被拷贝到新的Survivor区或老年代.
  • 年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域尺寸.
  • Young GCs会有STW事件, 进行时所有应用程序线程都会被暂停.
  • 多线程并发GC.

老年代收集

G1老年代GC会执行以下阶段:

index Phase Description
(1) 初始标记 (Initial Mark: Stop the World Event) 在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
(2) 扫描根区域 (Root Region Scanning: 与应用程序并发执行) 扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完.
(3) 并发标记 (Concurrent Marking : 与应用程序并发执行) 在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断.
(4) 重新标记 (Remark : Stop the World Event) 完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).
(5) 清理 (Cleanup : Stop the World Event and Concurrent) 见下 5-1、2、3
5-1 (Stop the world) 在含有存活对象和完全空闲的区域上进行统计
5-2 (Stop the world) 擦除Remembered Sets.
5-3 (Concurrent) 重置空regions并将他们返还给空闲列表(free list)
(*) Copying/Cleanup (Stop the World Event) 选择”活跃度”最低的区域(这些区域可以最快的完成回收). 拷贝/转移存活的对象到新的尚未使用的regions. 该阶段会被记录在gc-log内(只发生年轻代[GC pause (young)], 与老年代一起执行则被记录为[GC Pause (mixed)].

总结

将堆分为大小相等的独立区域,避免全区域的垃圾收集;
新生代和老年代不再物理隔离,只是部分Region的集合;
G1跟踪各个Region垃圾堆积的价值大小,在后台维护一个优先列表,根据允许的收集时间优先回收价值最大的Region;
Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,采用Remembered Set来避免全堆扫描;


G1原理调优

分区

G1的分区类型(HeapRegionType)大致可以分为四类:

  • 自由分区(Free Heap Region,FHR)
  • 新生代分区(Young Heap Region,YHR)
  • 大对象分区(Humongous Heap Region,HHR)
  • 老生代分区(Old Heap Region,OHR)

其中新生代分区又可以分为Eden和Survivor;
大对象分区又可以分为:大对象头分区和大对象连续分区。

HR的大小只能为1MB、2MB、4MB、8MB、16MB和32MB),默认情况下,整个堆空间分为2048个HR(该值可以自动根据最小的堆分区大小计算得出)。
HR大小可由以下方式确定:

  • G1HeapRegionSize来指定大小,默认0
  • 在不指定HR大小的时候,由G1启发式地推断HR大小。
    1
    2
    设置Initial HeapSize(默认为0)等价于设置Xms,设置MaxHeapSize(默认为96MB)等价于设置Xmx。
    堆分区默认大小的计算方式在HeapRegion.cpp中的setup_heap_region_size(),
新生代大小

G1MaxNewSizePercent和G1NewSizePercent用于控制新生代的大小

  • 如果设置新生代最大值(MaxNewSize)和最小值(NewSize),可以根据这些值计算新生代包含的最大的分区和最小的分区;
    注意Xmn等价于设置了MaxNewSize和NewSize,且NewSize=MaxNewSize。
  • 如果既设置了最大值或者最小值,又设置了NewRatio,则忽略NewRatio。
  • 如果没有设置新生代最大值和最小值,但是设置了NewRatio,则新生代的最大值和最小值是相同的,都是整个堆空间/(NewRatio+1)。
  • 如果没有设置新生代最大值和最小值,或者只设置了最大值和最小值中的一个,那么G1将根据参数G1MaxNewSizePercent(默认值为60)和G1NewSizePercent(默认值为5)占整个堆空间的比例来计算最大值和最小值。

既然使用到了推断没有定死, 那就可能会变扩展, 如何扩展?

-XX:GCTimeRatio表示GC与应用的耗费时间比
G1中默认为9,计算方式为_gc_overhead_perc=100.0×(1.0/(1.0+GCTimeRatio))
即G1 GC时间与应用时间占比不超过10%时不需要动态扩展

当GC时间超过这个阈值的10%,可以动态扩展。
扩展时G1ExpandByPercentOfAvailable(默认值是20)来控制一次扩展的比例,
即每次都至少从未提交的内存中申请20%,有下限要求(一次申请的内存不能少于1M,最多是当前已分配的一倍)

G1停顿

G1是一个响应时间优先的GC算法,由参数MaxGCPauseMillis控制,默认值200ms,设定整个GC过程的期望停顿时时间

那么G1怎么满足用户的期望呢?

1
2
3
4
5
6
7
8
就需要停顿预测模型了。
G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的堆分区数量(即选择收集哪些内存空间)
从而尽量满足用户设定的目标停顿时间。
如使用过去10次垃圾回收的时间和回收空间的关系,根据目前垃圾回收的目标停顿时间来预测可以收集多少的内存空间。


比如最简单的办法是使用算术平均值建立一个线性关系来预测。
如过去10次一共收集了10GB的内存,花费了1s,那么在200ms的停顿时间要求下,最多可以收集2GB的内存空间。G1的预测逻辑是基于衰减平均值和衰减标准差。
卡表和位图

卡表(CardTable)在CMS中是最常见的概念之一,G1中不仅保留了这个概念,还引入了RSet。卡表到底是一个什么东西?






内存分配与回收策略

内存分配

对象优先在Eden分配,当Eden没有足够空间,虚拟机将发起一次MinorGC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了MajorGC经常会伴随至少一次的MinorGC(但在ParallelScavenge收集器的搜集策略就有直接进行MarjorGC的策略选择过程)

大对象直接进入老年代

虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配;避免Eden区及两个Survivor区之间发生大量的内存复制

长期存活将进入老年代

如果对象在Eden区经历一次MinorGC移动到Survivor区还存活则年龄+1,直到增加到(默认15也可参数设置-XX:MaxTenuringThreshold)晋升到老年代

动态年龄判定

如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等待-XX:MaxTenuringThreshold中要求的年龄

空间分配担保

在发送MinorGC之前,先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果能行则此次MinorGC是安全的,否则将会查看HandlerPromotionFailure设置值是否允许担保失败
如果允许担保失败,继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小
如果大于则尝试进行一次MinorGC,如果小于或HandlerPromotionFailure设置不允许担保失败则进行一次FullGC






root搜索算法

以下对象会被认为是root对象

  • 方法区: 类静态属性引用的对象;
  • 方法区: 常量引用的对象;
  • 虚拟机栈(本地变量表)中引用的对象.
  • 本地方法栈JNI(Native方法)中引用的对象。

如果对象能够达到root,就不会被回收,如果对象不能够达到root,就会被回收。

OopMap

OopMap 用于枚举 GC Roots;避免全栈扫描,加快枚举根节点的速度

OopMap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。

一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。

安全点

1
2
3
一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 
gc发生时程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap,记下栈上哪些位置代表着引用。
枚举根节点时递归遍历每个栈帧的 OopMap,通过栈中记录的被引用对象的内存地址,即可找到这些对象(GC Roots)。

安全区

如果程序没有分配到CPU时间或处于Sleep状态或Blocked状态,jvm也不可能等线程进入Runnable状态走到SafePoint安全点,于是就有安全区域(SafeRegion)
安全区指的是:一片代码片段中,引用关系无变化,这个区域任何地方都是GC安全的安全点


RememberedSet

RememberedSet:记录老年代对象引用新生代对象

RememberedSet 用于处理这类问题:gc 过程是这样的:首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。但问题是确实可能存在位于老年代的某个GC Root,它引用了新生代的某个对象这个对象你是不能清除的。那怎么办呢?

仍然是拿空间换时间的办法,对应上面所举的例子“老年代对象引用新生代对象”这种关系会在引用关系发生时
在新生代边上专门开辟一块空间记录下来这就是RememberedSet。所以“新生代的 GC Roots ” + “ RememberedSet 存储的内容”,才是新生代收集时真正的 GC Roots 。
然后就可以以此为据在新生代上做可达性分析进行垃圾回收。






四种引用关系

  • Strong Reference(强引用):只有在引用对象root不可达的情况下才会标识为可回收,垃圾回收才可能进行回收
  • Soft Reference(软引用):即使在root算法中 其引用的对象root可达到,但是如果jvm堆内存 不够的时候,还是会被回收。
  • Weak Reference(弱引用):无论其引用的对象是否root可达,在响应内存需要时,由垃圾回收判断是否需要回收。
  • Phantom Reference(引用):在回收器确定其指示对象可另外回收之后,被加入垃圾回收队列.

ReferenceQueue

四种状态
每一时刻,Reference对象都处于下面四种状态中。这四种状态用Reference的成员变量queue与next(类似于单链表中的next)来标示。

1
2
ReferenceQueue<? super T> queue;
Reference next;

  • Active。新创建的引用对象都是这个状态,在 GC 检测到引用对象已经到达合适的reachability时,GC 会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。在这个状态中
1
2
3
//如果构造参数中没指定queue,那么queue为ReferenceQueue.NULL,否则为构造参数中传递过来的queue
queue = ReferenceQueue || ReferenceQueue.NULL
next = null
  • Pending。pending-Reference列表中的引用都是这个状态,它们等着被内部线程ReferenceHandler处理(会调用ReferenceQueue.enqueue方法)。没有注册的实例不会进入这个状态。在这个状态中
1
2
3
//构造参数参数中传递过来的queue
queue = ReferenceQueue
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this
  • Enqueued。调用ReferenceQueue.enqueued方法后的引用处于这个状态中。没有注册的实例不会进入这个状态。在这个状态中
1
2
queue = ReferenceQueue.ENQUEUED
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this
  • Inactive。最终状态,处于这个状态的引用对象,状态不会在改变。在这个状态中
1
2
queue = ReferenceQueue.NULL
next = this

有了这些约束,GC 只需要检测next字段就可以知道是否需要对该引用对象采取特殊处理

  • 如果next为null,那么说明该引用为Active状态
  • 如果next不为null,那么 GC 应该按其正常逻辑处理该引用。






GC类型

GC有两种类型:Scavenge(minor) GC和Full GC。

  1. Scavenge(minor) GC
    一般情况下,当新对象生成,并且在Eden申请空间失败时,就好触发Scavenge GC,堆Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。
  2. Full GC
    对整个堆进行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。有如下原因可能导致Full GC:
  • Tenured被写满
  • Perm域被写满
  • System.gc()被显示调用
  • 上一次GC之后Heap的各域分配策略动态变化

GC 细节

  • 对象优先在Eden分配
  • 大对象直接进入年老代
    -XX:PertenureSizeThreshold

    1
    2
    多大的对象才算是大对象,这个是可以控制的,虚拟机参数为-XX:PertenureSizeThreshold
    需要精确到B,-XX:PertenureSizeThreshold=3145728
  • 长期存活的对象将进入年老代
    -XX:MaxTenuringThreshold

    1
    什么样算老年了呢,默认为15岁,可以通过-XX:MaxTenuringThreshold来设置
  • 动态对象年龄判断

    1
    2
    3
    4
    不是永远要求等到MaxTenuringThreshold参数设置的年龄。

    如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,
    大于或等于该年龄的对象就可以直接进入年老代。
  • 空间分配担保

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    当Survivor空间不够用时,需要依赖其他内存(年老代)进行分配担保(Hanle Promotion)

    分配担保流程如下:

    a. 在发生Minor GC之前,JVM首先检查年老代最大可用的连续空间是否大于新生所有对象的空间。

    b. 如果大于,那么可以确保Minor GC是安全的。

    c. 如果不大于,则JVM查看HandlePromotionFailure值是否允许担保失败。

    d. 如果允许,将尝试进行一次Minor GC,但这是有风险对的,失败后只有重新发起一次Full GC;

    e. 如果小于或HandlePromotionFailure值不允许冒险,那这时,要改为进行一次Full GC;

    JDK1.6新规则
    JDK1.6之后,JVM代码中已经不再使用HandlePromotionFailure参数了...
    规则变为:

    只要年老代最大可用的连续空间大于新生所有对象的空间或历次晋升到年老代对象的平均大小,就会进行MinorGC,否则进行Full GC。

    即年老代最大可用的连续空间小于新生所有对象空间时,不在检查HandlePromotionFailure,而是直接检查历次晋升到年老代对象的平均大小。

总结

  1. Eden区最大,对外提供堆内存。当Eden区快要满了,则进行Minor GC,把存活对象放入Survivor A区,清空Eden区;
  2. Eden区被清空后,继续对外提供堆内存;
  3. 当Eden区再次被填满,此时对Eden区和Survivor A区同时进行Minor GC,把存活对象放入Survivor B区,同时清空Eden 区和Survivor A区;
  4. Eden区继续对外提供堆内存,并重复上述过程,即在Eden区填满后,把Eden区和某个Survivor区的存活对象放到另一个Survivor区;
  5. 当某个Survivor区被填满,且仍有对象未被复制完毕时或者某些对象在反复Survive 15次左右时,则把这部分剩余对象放到Old区;
  6. 当Old区也被填满时,进行Major GC,对Old区进行垃圾回收。






工具

jps

1
2
3
4
5
jps
-q 不输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数

jstack

1
2
-l long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况
-m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)

jmap

1
2
3
4
5
6
7
8
 -dump:生成java堆转储快照
-finalizerinfo:显示在F-Queue中等待Finalizer线程执行finalize方法的对象(只在Linux/Solaris下有效)
-heap:显示java堆详细信息(只在Linux/Solaris下有效)
-histo:显示堆中对象统计信息
-permstat:以ClassLoader为统计口径显示永久代内存状态(只在Linux/Solaris下有效)
-F:当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照(只在Linux/Solaris下有效)

jmap -dump:format=b,file=d:xmind.hprof 6012






服务器配置参数

服务的虚拟机参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-server       --启用能够执行优化的编译器,显著提高服务器的性能
-Xmx4000M --堆最大值
-Xms4000M --堆初始大小
-Xmn600M --年轻代大小
-XX:PermSize=200M --持久代初始大小
-XX:MaxPermSize=200M --持久代最大值
-Xss256K --每个线程的栈大小
-XX:+DisableExplicitGC --关闭System.gc()
-XX:SurvivorRatio=1 --年轻代中Eden区与两个Survivor区的比值
-XX:+UseConcMarkSweepGC --使用CMS内存收集
-XX:+UseParNewGC --设置年轻代为并行收集
-XX:+CMSParallelRemarkEnabled --降低标记停顿
-XX:+UseCMSCompactAtFullCollection --在FULL GC的时候,对年老代进行压缩,可能会影响性能,但是可以消除碎片
-XX:CMSFullGCsBeforeCompaction=0 --此值设置运行多少次GC以后对内存空间进行压缩、整理
-XX:+CMSClassUnloadingEnabled --回收动态生成的代理类 SEE:http://stackoverflow.com/questions/3334911/what-does-jvm-flag-cmsclassunloadingenabled-actually-do
-XX:LargePageSizeInBytes=128M --内存页的大小不可设置过大, 会影响Perm的大小
-XX:+UseFastAccessorMethods --原始类型的快速优化
-XX:+UseCMSInitiatingOccupancyOnly --使用手动定义初始化定义开始CMS收集,禁止hostspot自行触发CMS GC
-XX:CMSInitiatingOccupancyFraction=80 --使用cms作为垃圾回收,使用80%后开始CMS收集
-XX:SoftRefLRUPolicyMSPerMB=0 --每兆堆空闲空间中SoftReference的存活时间
-XX:+PrintGCDetails --输出GC日志详情信息
-XX:+PrintGCApplicationStoppedTime --输出垃圾回收期间程序暂停的时间
-Xloggc:$WEB_APP_HOME/.tomcat/logs/gc.log --把相关日志信息记录到文件以便分析.
-XX:+HeapDumpOnOutOfMemoryError --发生内存溢出时生成heapdump文件
-XX:HeapDumpPath=$WEB_APP_HOME/.tomcat/logs/heapdump.hprof --heapdump文件地址






Java内存模型

可见性(主内存和工作内存)、原子性(volatile的long是具备原子性的)、有序性(happen—before规则);

Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系

Java内存模型和硬件内存架构并不一致。硬件内存架构中并没有区分栈和堆

从硬件上看,不管是栈还是堆,大部分数据都会存到主存中

当然一部分栈和堆的数据也有可能会存到CPU寄存器中

Java内存模型和计算机硬件内存架构是一个交叉关系

  • 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序

下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。
如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。

1
2
3
4
5
6
7
8
1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

锁的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

volatile的内存语义

  • 一是保证多个线程对共享变量访问的可见性

  • 二防止指令重排序

volatile内存语义的实现

内存屏障

由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题:

LoadLoad屏障

  • 对于Load1; LoadLoad; Load2 ,操作系统保证在Load2及后续的读操作读取之前,Load1已经读取。

StoreStore屏障

  • 对于Store1; StoreStore; Store2 ,操作系统保证在Store2及后续的写操作写入之前,Store1已经写入。

LoadStore屏障

  • 对于Load1; LoadStore; Store2,操作系统保证在Store2及后续写入操作执行前,Load1已经读取。

StoreLoad屏障

  • 对于Store1; StoreLoad; Load2 ,操作系统保证在Load2及后续读取操作执行前,Store1已经写入,开销较大,但是同时具备其他三种屏障的效果。

下面对volatile写和volatile读的内存语义做个总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

JMM如何实现volatile写/读的内存语义?

重排序分为编译器重排序和处理器重排序。

下面是JMM针对编译器制定的volatile重排序规则表:

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写 NO
volatile读 NO NO NO
volatile写 NO NO

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。


Java各类锁

JVM锁有4种状态:无锁、偏向锁(通过MarkWord的线程ID)、轻量级锁(通过MarkWord的锁记录指针)、重量级锁;

状态 标志位 存储内容
未锁定 01 对象哈希码、对象分代年龄
轻量级锁定 00 指向锁记录的指针
膨胀(重量级锁定) 10 执行重量级锁定的指针
GC标记 11 空(不需要记录信息)
可偏向 01 偏向线程ID、偏向时间戳、对象分代年龄

markword是java对象数据结构中的一部分,markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态.

每种锁只有在其特定的场景下,才会有出色的表现

java中没有哪种锁能够在所有情况下都能有出色的效率

引入这么多锁的原因就是为了应对不同的情况

偏向锁获取过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
  5. 执行同步代码。

偏向锁的释放

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

轻量级锁获取过程

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程:

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录中
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的释放

释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间

之前在获取锁的时候它拷贝了锁对象头的markword

在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了

并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。