一.运行时数据区域
1.Heap Area
- 存储的全都是Object对象实例,对象实例中一般都包含了其数据成员以及与该对象对应Class信息;
- 一个JVM实例在运行的时候只有一个Heap区域,该区域被所有的线程共享;
2.Method Area
- 方法区域又名静态成员区域,包含整个程序Class,static成员等;
- 方法区被所有线程共享
3.Stack Area
- Stack区域属于线程私有,每个线程都会包含一个Stack区域,Stack区域中含有基本的数据类型以及对象的引用,其它线程均不能直接访问该区域;
- 分为三大部分:基本数据类型区域、操作指令区域、上下文等;
二.JVM线程引擎和内存共享交互
三.GC
1.GC的内存结构
Scavenge GC:
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。
Full GC:
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
产生原因:
- 年老代(Tenured)被写满
- 持久代(Perm)被写满
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
Full GC日志:
Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存
Metaspace:
- 类的元数据
- 用metaspace原因:permanent淘汰,情况复杂,类的信息,静态类常量池信息,类动态加载,空间不好评估,解决oom 用Metaspace;GC,持久代内存不够,full gc。
- 用的os内存空间
- 动态伸缩,调整自身大小
- 足够大初始值,最大值,第一次gc,自动调整空间。
- GC算法:所有实例被gc,ClassLoader不存在,判断类加载存不存在,实例和类加载绑定,内部是个链表;permanent 与年老代分开
2.GC时候内存管理
- Heap空间大于50%
- 绝大多数对象使用过后不再使用
- 复制好处不会有垃圾碎片
- 大对象直接进入年老代
3.Young Generation
- 分代是因为GC性能原因
- 年轻代和年老代算法不同
- 年老代GC是迫不得已
- 大部分Jvm对象生命周期比较短,如果体积大直接放到年老代中,对象一般产生Eden中
- Eden对象拷贝到From,阈值到达一定次数会复制到年老代中,如果没有到达次数,会复制到To中
- Full GC:From中生命周期达到一定阈值;年老代满了
- 年老代回收速度慢
- 年轻代和年老代比例,Eden和From/to比例:-XX:NewRatio 年轻代和年老代比例,设置比例扩展时候会产生消耗,如果知道具体值设置;-XX:SurvivorRatio Eden和一个Survivor比例,Eden小的时候,会增加GC次数了 ; -XX:NewSize 年轻代大小不能比年老代大 ;-XX:MaxNewSize
- 年龄问题:GC一次年龄加1;Eden,15岁晋升;Survivor中,有些对象年龄相同,所有年龄相同对象总和大于空间一半,进入年老代,动态调整
- 新生代收集担保:在一次理想化的minor gc中,活跃对象会从Eden和First Survivor中被复制到Second Survivor。然而,Second Survivor不一定能容纳所有的活跃对象。为了确保minor gc能够顺利完成,需要在年老代中保留一块足以容纳所有活跃对象的内存空间。这个预留的操作,被称之为新生代收集担保(New Generation Guarantee)。当预留操作无法完成时,就会触发major gc(full gc)。
4.Minor GC 日志
- PSYounGen:新生代收集算法
- 2336k:新生代收集前占用的内存
- 288:新生代收集后占用的内存
- 2560k:新生代总共大小
- 8274k:收集前堆的大小
- 6418k:收集后堆大小
- 9728k:整个堆大小
- 0.01129926:整个minor GC消耗时间
- Times:user 用户空间; sys 系统空间; real 真实时间 占user和sys 比较多,调度比较多,有可能并发
5.内存逃逸技术
简介:对象分配不在heap区域,降低GC发生次数,负担,提升回收效率
在java stack:
局部实例方法里面,但是被外部成员引用,会发生逃逸
class Worker{
public Worker worker;
public Worker getWorker(){
//局部对象创建,对象逃逸
return null==worker?new Worker():worker;
}
public void useWorker(){
//这个不是,这是方法本身
Worker obj=new Worker();
}}
6.对象标记算法
引用计数算法:被引用,加一,不使用减一,问题:两个都已经,两个或多个相互引用,都已经死亡,但计数不是0,一直不释放,会产生内存泄露。
根搜索算法:树根连接目标对象,如果不到达目标对象,已经死亡;树里面具体对象,栈中引用,本地方法栈,常量池,静态,所有对象。
三种基本算法:
复制算法:MinorGC(普通GC)
年轻代中使用的是 Minor GC,这种GC算法采用的是复制算法(Copying)
Minor GC会把Eden中的所有活的对象都移到 Survivor 区域中,如果 Survivor区中放不下,那么剩下的活的对象就被移到Oldgeneration中,也即一旦收集后,Eden是就变成空的了。
当对象在Eden(包括一个 Survivor区域,这里假设是from区域)出生后,在经过一次 Minor GC后,如果对象还存活,并且能够被另外一块 Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor区域(即to区域)中,然后清理所使用过的Eden以及 Survivor区域(即fom区域),并且将这些对象的年龄设置为1,以后对象在 Survivor区每熬过一次 Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,通过 -XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。
解释: 年轻代中的GC,主要是复制算法(Copying) Hotspot JVM把年轻代分为了三部分:1个Eden区和2个 Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到Survivor区。对象在 Survivor区中每熬过一次 Minor GC ,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。 在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”区被填满之后,会将所有对象移动到年老代中。
GC算法[通俗易懂]
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%(From)的活动区间与另外80%中存活的对象转移到10%(To)的空闲区间,接下来,将之前90%的内存全部释放,以此类推 。
口诀:
谁空谁是 to,复制要交换。
总结:
- 优点:不会产生内存碎片,完整度极高。
- 缺点:浪费了这10%(To)的内存空间。
复制算法弥补了标记/清除算法中,内存布局混乱的缺点。不过与此同时,它的缺点也是相当明显的
- 它浪费了一半(1:1的这个一半,就是那10%)的内存,这太要命了。
- 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
标记清除/标记整理算法:FullGC又叫 MajorGC(全局GC)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
标记清除(Mark-Sweep)
GC算法[通俗易懂]
GC算法[通俗易懂]
GC算法[通俗易懂]
GC算法[通俗易懂]
缺点:
- 首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
- 其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随机的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM 就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记整理(Mark-Compact)
GC算法[通俗易懂]
注释:上面标记的是活的。
GC算法[通俗易懂]
GC算法[通俗易懂]
GC算法[通俗易懂]
缺点:
- 标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
垃圾回收器
串行收集器:
最古老,最稳定
效率高
可能会产生较长的停顿,只使用一个线程。
-XX:+UseSerialGC:新生代、老年代使用串行回收;新生代复制算法;老年代标记-压缩;
应用程序需要GC时候,程序暂停,单线程开始GC
--------------------------------------------------------------------------------------
parNew(并行收集器)
-XX:+UseParNewGC:新生代并行;老年代串行。
Serial收集器新生代的并行版本。
复制算法。
多线程,需要多核支持,多线程不一定快。
-XX:ParallelGCThreads 限制线程数量。
应用程序需要GC时候,程序暂停,多线程开始GC。
--------------------------------------------------------------------------------------
parallel(并行收集器)
类似ParNew
新生代复制算法
老年代 标记-压缩
更加关注吞吐量
-XX:+UseParallelGC:使用parallel 收集器 老年代串行
-XX:+UseParallelOldGC:使用Parallel收集器 并行老年代
应用程序需要GC时候,程序暂停,多线程开始GC
-XX:MaxGCPauseMills:最大停顿时间,单位毫秒;GC尽力保证回收时间不超过设定值。
-XX:GCTimeRatio:0-100的取值范围;垃圾收集的时间占总时间的比;默认99,即最大允许1%时间做GC。
-XX:MaxGCPauseMills和-XX:GCTimeRatio这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优。
CMS收集器
Concurrent Mark Sweep 并发标记清除
标记-清除算法。
并发阶段会降低吞吐量:跟应用程序线程一起执行,交替执行;并行是多个线程执行。
停顿时间少。
老年代收集器(新生代会使用ParNew)。
-XX:+UseConcMarkSweepGC。
CMS运行过程比较复杂,着重实现标记的过程,可分为:
初始标记:根可以直接关联到的对象;速度快,全局停顿。
并发标记(和用户线程一起):主要标记过程,标记全部对象,是否是垃圾。
重新标记:由于并发标记时,用户线程依然运行,因此正式清理前,再做修正,也会产生停顿。
并发清除(和用户线程一起):基于标记结果,直接清理对象。
标记过程,把全局停顿尽可能的缩小。
-XX:UseCMSCompactAtFullCollection Full GC后,进行一次整理,整理过程是独占的,会引起停顿时间变长。
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC,后进行一次碎片整理。
-XX:ParallelCMSThreads 设定CMS线程数量。
特点:
尽可能降低停顿,更加关注停顿。
会影响系统整体吞吐量和性能。比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半。
清理不彻底:因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理。
因为和用户线程一起运行,不能在空间快满时候再清理:-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值;如果不幸内存预留空间不够,就会引起concurrent mode failure;
G1收集器
-XX:+UseG1GC 开启G1垃圾收集器
G1将新生代,老年代的物理空间划分取消了,取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
适用时间 Full GC次数太频繁或者消耗时间太长。对象分配的频率或代数提升显著。太长垃圾回收或内存整理(超过0.5~1秒)。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
------------------------------------------------------------------------------------------------
有关碎片
标记-清除和标记-压缩:
标记-清除产生大量碎片,连续空间没有。
标记-清除算法之后会进行,标记-压缩算法整理。
--------------------------------------------------------------------------------------
减轻GC压力
- 软件如何设计架构
- 代码如何去写
- 堆空间如何分配
--------------------------------------------------------------------------------------
GC参数整理
-XX:UseSerialGC 设置新生代和老年代使用串行收集器
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:NewRatio 新生代和老年代的比
-XX:+UseParNewGC 新生代使用并行收集器
-XX:+UseParallelGC 新生代使用并行回收收集器
-XX:+UseParallelOldGC 老年代使用并行回收收集器
-XX:ParallelGCThreads 设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads 设定CMS线程数量
-XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理
-XX:CMSFullGCsBeforeCompaction 设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到这一百分比时,启动CMS回收
-XX:UseCMSInitiatingOccupancyOnly 表示只在到达阀值的时候,才进行CMS回收