深入理解JVM—gc相关

 

        深入理解JVM—gc相关

周四听了毕玄的JVM分享,对于JVM的相关知识, 有了初步的了解,然后根据现场笔记整理形成以下这篇blog

一 内存管理

    在所有的语言中, 一般是实现了以下两种内存管理的方式之一。

第一种是以c/c++为典型代表的,是需要程序员显示的管理内存,如cmalloc /free  c++new delete。优点非常明显,就是程序员对所有的内存具有完全的控制权,和那些语言层面实现了内存管理的语言相比,效率很高。但是缺点也很明显,就是一不小心,很容易内存泄露, 学习成本较高, 特别是新手,很容易出错。

第二种是以后出现的众多高级语言,例如java python c#等等, 这些语言都不需要主动去管理内存,而是语言层面已经帮你完成了这部分功能,帮你管理内存,对于新手来说,也不容易写出太差的代码。但是同样缺点也很明显,就是你不能很明确的控制什么时候回收内存块。

内存分配和内存的回收是JVMgc主要需要完成的事情, 我们只有通过详细的了解gc相关的触发机制,才可以写出更加建壮的程序。

 

java的内存区域

                                                 

如上图所示, Java内存区域,可以分成以上3个部分。

方法栈: 线程创建时产生,并在方法执行过程中, 一直存在于堆栈中, 在方法中调用新的方法, 那么新的方法栈一样也会被压入堆栈。在方法帧上又保存了方法执行过程中的局部对象和操作数栈。

方法区:存放类的元数据常量。

堆:所有通过new生成的对象都放在这个区域,并通过一个指针指向对应的对象。

Native Memory: 这部分也是堆,一般是通过jni之类的c代码存放对象的地方,而不是通过Java尽心管理。

 

2.1内存区的具体划分

sunjdk上, 一般会分成两个区域,如下图所示,一块是新生代 (new Generation) ,一个是旧生代 (old Generation), 在新生代又分成了Eden,还有一般是一样大小的S0 S1两个可以看成是交换区的部分。 通常对于新生代的回收称为minor GC 或者young GC ,而对于旧生代的回收称为old GC

 

 

 




sun jdk GC方式

对于GC, 无论是使用还是调优, 最关键是知道到底是什么时候触发的。

sunsdk提供了三种GC。分别是串行, 并行和并发。

3.1 串行

串行是最古老的垃圾回收方式,是client默认的垃圾回收方式(貌似sdk根据几核还有多少内存来判断是client或者是server, 虽然现在来看其实不是很靠谱)。缺点是由于使用的是单线程, GC过程比较慢, 没有充分发挥现在的多核多内存的硬件优势。

gc的方式

YGC: Parallel Scavenge(PS)

FGC: Parallel MSC(PSOld),Parallel Compacting(ParOld)

二 内存回收的时机

YGC的时机:

edn空间不足

FGC的时机:

1.old空间不足;

2.perm空间不足;

3.显示调用System.gc() ,包括RMI等的定时触发;

4.YGC时的悲观策略;

5.dump live的内存信息时(jmap –dump:live)

YGC的 触发时机,相当的显而易见,就是eden空间不足, 这时候就肯定会触发ygc

对于FGC的触发时机, old空间不足, 和perm的空间不足, 调用system.gc()这几个都比较显而易见,就是在这种情况下, 一般都会触发GC

最复杂的是所谓的悲观策略,它触发的机制是在首先会计算之前晋升的平均大小,也就是从新生代,通过ygc变成新生代的平均大小,然后如果旧生代剩余的空间小于晋升大小,那么就会触发一次FullGCsdk考虑的策略是, 从平均和长远的情况来看,下次晋升空间不够的可能性非常大, 与其等到那时候在fullGC 不如悲观的认为下次肯定会触发FullGC, 直接先执行一次FullGC。而且从实际使用过程中来看, 也达到了比较稳定的效果。

 

YGC完成的动作

1,将edenfrom中的引用次数为0的对象清除

2,将eden+from中的存活对象复制to

3,如果to放不下或者存活次数超过Tenuring Threshold, 那么将会晋升到old

1from to 实际上就是old区中默认相等的两块区域,同时两块区域的使用是互换,没有固定的from to

2 Tenuring Threshold 是一个对象在new中存活,并经历YGC的次数,而且这个次数是动态调整的。

FGC完成的动作

1,如果配置CollectGen0First, 将会先执行YGC

2,清空heap中所有的引用为0的对象,以及卸载的所有classLoader

3,单线程完成,全过程应用暂停

       3.2并行

所谓的并行就是多线程同时进行GC,这个是在server默认使用的模式。优点是高效, 主要是相对的串行来说, 充分利用了服务器多核多内存的特点,充分利用所有硬件资源,进行GC, 因此相对串行速度快也就是是必然的了。缺点是,如果堆(heap)比较大, 那么暂停的时间可能会比较长,这是现在java语言发展到当今最大的悲剧,没有很好的跟上硬件的发展速度, 当今好的服务器都是几十G的内存,GC一次可能会时间较长, 这对于online的应用是完全无法忍受的。

一触发机制:

YGC 和串行一样。

FGC 和串行基本一样,最大的不同是悲观策略在YGCYGC后会检查两次。

YGC完成的动作是:

基本动作一样, 主要的区别是使用多线程同时gc

另外YGC之后, 会重新计算Edenfrom的大小

FGC完成的动作:

默认ScavengeBeforeFullGC 的参数 会先触发YGC

其他操作基本一样清空没有引用的对象, 卸载class信息

MSC:进行压缩

Compacting进行部分压缩

3.3并发

所谓的并发,就是和之前的串行和并行都需要相对长的暂停时间相比, 并发只需要很短的应用暂停时间,相对来说, 比较时候对延时要求比较高的应用,但是现在的并发并不是很成熟, 存在着一些bug还没有完全fix。并发的优点是:暂停时间短, 延时小。并发的缺点 内存碎片多,在Old区的内存分配的效率很低, 而且由于和应用同时进行, 会出现和应用抢cpu的现象, 导致应用的响应变慢,从而影响用户体验。同时用于没有充分利用硬件资源, 包括内存cpu 所以整个回收的过程会比较久。

一内存回收的时机

YGC 触发时机:

和其他垃圾回收机制一样, 都是eden的空间不足

CMSGC触发时机

oldGC达到一定的使用率的时候,就必须进行FGC,因为这个GC过程中,是不暂停,如果等到空间不足才GC,会导致内存分配失败,而应用无法正常运行 , 这个比例值可以通过参数进行设置。

Hotspot自己估计是否需要触发CMSGC

显式调用了System.gc()

FGC 触发的时机

   FGC触发的时机是CMSGC失败之后,就会调用GC, 并且是调用串行的GC。这样就会非常悲剧,时间很长, 而使用串行的原因是, 并发是继承自串行, 使用了同一套接口,调用比较容易, 而并行是后来重新写的,无法直接使用。

二触发的动作

YGC触发动作

和串行一样, 只是使用了多线程

CMSGC触发动作

1.oldGen达到一定的比例之后就清除所有没有引用的对象

2.perm gen 达到一定比例之后,就清除classload所加载的信息

FGC触发动作

如果并发的GC失败, 会引发一次串行的GC

GC调优

4.1GC调优模式

GC调优, 主要有以下以下衡量尺度, 包括分配内存,GC的频率, GC导致应用暂停的时间 ,应用的响应时间和QPS System load/cpu/IO 等等。

GC调优是一个很看经验的一个大工程。因此就有很多的经验值在里面,

1 对于32位, JVM必须建议控制在2G以下。

2 对于64位, 最好开启指针压缩的选项-XX:+UseCompressedOOPS

3 根据应用的需求, 选择合适的垃圾收集器

4 计算出可能存活的对象数量,并进行相应的设置。

4.2GC的主要模式以及对应的解决方案

一 降低FGC的频率 :

      1,很明显增大old

       2,降低从new->old的晋升频率。

二降低FGC的暂停时间

1 .减小Heap

2 .换成CMS 

3.升级CPU(据说最靠谱)

三降低FGC的频率

1 增大新生代

四降低FGC的暂停时间

1减少新生代,可能造成频繁FGC

2升级CPU

4.3写出GC友好的代码

1.尽量减少使用autobox, 因为在Java在将原始对类型变成对象的过程中, 需要new一个新的对象过程中,在某些时候,可能会使用大量堆的内存造成内存溢出。

2.合理控制动态增长的数据结构的大小,很多时候内存的超出错误(OOM)都是这些动态数据结构造成, 他给我们带来方便的同时, 也同样带来了不可控制性。

3.合理使用reference

4.不要使用Finalizers来回收资源,据写sdk的人说,Finalizers并不能保证一定会被执行,在某些特殊的时刻,可能会被跳过,这样很容易导致内存泄漏。

sun jdk 内存管理的实现

5.1内存管理的方法

对于java sdk的内存管理来说, 主要需要完成的功能就是对内存的管理, 从空间的申请到释放的过程。

在所有的语言中,内存空间的分配和回收, 主要有以下两种做法。

1对于已经释放的空间使用一个freelist的链表进行保存

优点是:效率高

缺点:空间浪费, 形成碎片

2 通过移动合并, 形成连续的内存空间

优点:内存的使用率较高 

缺点移动内存需要时间, 而且需要更新指针

5.2GC的主要算法

GC算法, 需要完成的主要目标是找到垃圾内存块,并进行回收。主要有以下几种经典算法。

一引用计数

这是最老的GC算法, 它具有以下几个明显的缺点, 首先他需要额外的空间, 然后存在一个最主要的问题是无法解决循环引用。

Tracing – Mark-Sweep

需要使用一定的策略遍历所有的对象, 然后进行标记, 之后将没有标记的进行回收。

缺点:随着heap的增大, GC的时间会增长很快

优点:可以清除所有的垃圾

Tracing – Mark-Compact

在标志的基础上,进行移动,使内存块连续。

缺点: 移动需要时间, 需要更新指针

优点,内存使用率高

copying

所谓的复制,就是在遍历过程中 将所有有引用的复制到另外一个分区, 然后剩下的就是没有引用的内存块, 直接对整个内存块进行回收就可以了。 其实就是上面提到的新生代的fromto

优点: 没有垃圾碎片,

缺点: 需要更新指针, 浪费了一块空闲的内存

 

以上这篇日志,都是在分享上听到的或者PPT上的再加上一些我自己的一些理解,再次感谢毕玄这次分享,如果有错,欢迎指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值