二、深究JVM垃圾回收(保姆式讲解,内附大量图解!!!)
作者:互联网
一、内存泄露与内存溢出
1.内存泄漏memory leak :
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
2、内存溢出 out of memory :
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
3、二者的关系
内存泄漏的堆积最终会导致内存溢出 内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
内存溢出:栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错,
4、内存泄漏的分类(按发生方式来分类)
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
5、内存溢出的原因及解决方法:
内存溢出原因:
- 1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 3.代码中存在死循环或循环产生过多重复的对象实体;
- 4.使用的第三方软件中的BUG;
- 5.启动参数内存值设定的过小
内存溢出的解决方案:
- 第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
- 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
- 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
- 第四步,使用内存查看工具动态查看内存使用情况
6.重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。
一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。
List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
7.如何判断对象可以回收
1、引用计数法
如果一个对象被其他对象引用了,就让它的引用计数+1,又被其他对象引用的时候计数 再加1,没结束一个引用,引用计数就-1;当引用计数为0的时候,它就可以被回收了。 缺点:
第一阶段新生代垃圾回收过程:
G1把堆内存划分为若干个区域,每个区域都可做为:伊甸园,幸存区,老年代
- 刚开始创建的对象会被放在伊甸园(E)内
- 当伊甸园的内存逐渐被占满时,会调用新生代垃圾回收 会触发STW
- 幸存的对象会被复制到幸存区(S)
- 当幸存区的内存逐渐占满时,会将超过阈值的对象放入老年代,未超过阈值的对象会复制到另一个幸存区中
第二阶段:新生代的回收和并发标记的阶段
- 在Young GC 时会进行GC root的初始标记(会STW)
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
- XX:InitiatingHeapOccupancyPercent=percet(默认45%) 当老年代内存占所有rejion(区域)的45%时会触发变更标记
第三阶段:混合回收阶段
- E中幸存的对象复制到S区中
- S区中超过阈值的对象复制到O区
- 有一部分O区复制到另一个O区(优先选择的价值性较高的对象复制)
- 老年代中存放的有GC root对象,可以通过老年代中的根对象找到E区中的对象
- 将老年代划分为许多卡表。每个卡表大概512kb
- 引用了新生代中的对象的老年代对象的卡,则称为为脏卡
- 当遍历GC root 的时候只需遍历脏卡区域就行,提高了回收检索的性能
G1_REMAKE 更新标记
- 黑色代表处理过的对象,灰色代表正在处理的对象,白色代表还未处理的对象
- 如果灰色的对黑色的对象有强引用时,则在处理后会变成黑色,未被引用的则依然是黑色,最终被被回收
- 根据黑白色来区别是否被回收
- 当引用的对象发生改变时,就会给它加一个写屏障
- 写屏障将C对象加入一个队列,并将C变为灰色,再进行判断
- 如果有引用,则将C变为黑色
G1_字符串去重
G1_类卸载
所有对象都经过并发标记后,就能知道那些类不在被使用,当一个类加载器的加载的所有类都不在使用,则卸载它所加载的所有类
G1_巨型对象
- 一个对象的大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉 G1动态调整阈值
- 并发标记必须在堆空间占满前完成,否则退化为FULL GC
- JDK 9之前需要使用-XX:InitiatingHeapOccupanyPercent
- JDK 9后可以动态调整 -XX:InitiatingHeapOccupanyPercent(初始堆占用率占比) 用来设置初始值 进行数据采样并动态调整 总会添加一个安全的空档空间
六、垃圾回收调优
调优领域
- 内存
- 锁竞争
- cpu占用
- io
确定目标
追求是高吞吐量还是追求低延迟来选择合适的垃圾回收器
互联网:低延迟
采用:CMS(很少使用)、G1(适合超大内存,集合了CMS与ParallelGC的优点)、ZGC 科学运算
追求高吞吐量
采用:ParallelGC 6.3尽量减少发生GC的概率 查看FullGC前后的内存占用,考虑下面几个问题
数据是不是太多? 例 resultSet=satement.executeQuery("select * from 大表"),这时会将大量数据加载到堆内存中,占用内存资源 解决方法,加上一些限定条件,筛选有用的数据。
是否存在内存泄漏? 内存泄漏导致有些无用的对象所占用的内存无法被回收,久之就会导致内存不足。
对象大小 选择合适的对象的类型 Integer 占用24字节 int占用 4个字节
使用第三方缓存实现 例如:使用redis缓存 6.4新生代调优
新生代的特点
- 所有的new操作的内存分配非常廉价
- 死亡对象的回收代价是零
- 大部分对象用过即死
- MINOR GC 的时间远远低于Full GC
幸存区调优
- 幸存区大到能保留[当前活跃对象+需要晋升对象]
- 晋升阈值配置得当,让长时间存货对象尽快晋升
- 老年代调优 以CMS为例 - 以CMS的老年代内存越大越好 - 先尝试不做调优,如果没有FUll GC,那么说明老年代内存还可 以 - 观察发生FUll gc 时老年代内存占用,将老年代内存预设调大1/4~1/3
上一篇:一、Java虚拟机与跨平台原理讲解(硬怼面试官再也不怂!!!)
下一篇:
标签:泄漏,对象,内附,引用,回收,内存,JVM,溢出,保姆式 来源: https://blog.csdn.net/weixin_44205087/article/details/118654656