JVM总结之GC
哪些内存需要回收
在Java堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要知道哪些对象还“存活着”,哪些对象已经”死去“。
引用计数算法
引用计数法的实现:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1,只要计数器为0的对象就是不可能被使用的。
这个算法实现简单,效率也很高,但是当存活对象中,存在相互引用的时候,这算法就解决不了。所以Java中的GC并没有采用引用计数法来管理内存。(后面例子分析会根据GC日志看出相互引用的对象被回收了)
可达性分析算法
以GC Roots对象作为起始点,从这些节点依次向下搜索,如果当前对象到GC Roots没有任何的路径相连(对象不可达)时,那么,当前对象没有引用。
在Java中,以下对象可作为GC Roots:
- Java虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 本地方法栈中引用的对象;
- 方法区中的常量引用的对象;
- 方法区中的静态属性引用的对象。
当这些对象不可达的时候,也并不是就可以宣告这个对象就死亡了。还有对象的最后一次自我救赎——finalize。
finalize是一个方法名,Java允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。在这个操作中如果对象被重新引用,对象就可以活过来了。
判断对象是否有必要执行该方法主要有以下两个依据:
- 对象有没有覆盖finalize方法;
- 对象已覆盖finalize方法,检查finalize方法是否被虚拟机调用过,如果已被调用,就不需要再次执行。
如果该对象有必要执行finalize方法,则该被对象将被放置到F-Queue中,由虚拟机的单独线程Finalizer执行。
注意:finalize方法只能被执行一次,如果面临下一次回收,finalize方法将不会被执行。
何时回收
young gc
对于 young gc,触发条件似乎要简单很多,当 eden 区的内存不够时,就会触发young gc。
full gc
1. old gen 空间不足
当创建一个大对象、大数组时,eden 区不足以分配这么大的空间,会尝试在old gen 中分配,如果这时 old gen 空间也不足时,会触发 full gc,为了避免上述导致的 full gc,调优时应尽量让对象在 young gc 时就能够被回收,还有不要创建过大的对象和数组。
2. 统计得到的 young gc 晋升到 old gen的对象平均总大小大于old gen 的剩余空间
当准备触发一次 young gc时,会判断这次 young gc 是否安全,这里所谓的安全是当前老年代的剩余空间可以容纳之前 young gc 晋升对象的平均大小,或者可以容纳 young gen 的全部对象,如果结果是不安全的,就不会执行这次 young gc,转而执行一次 full gc
3. perm gen 空间不足
如果有perm gen的话,当系统中要加载的类、反射的类和调用的方法较多,而且perm gen没有足够空间时,也会触发一次 full gc
4. ygc出现 promotion failure
promotion failure 发生在 young gc 阶段,即 cms 的 ParNewGC,当对象的gc年龄达到阈值时,或者 eden 的 to 区放不下时,会把该对象复制到 old gen,如果 old gen 空间不足时,会发生 promotion failure,并接下去触发full gc
如何回收
标记-清除算法
标记清除算法分为”标记“和”清除“两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的不足有两个:
- 效率问题,标记和清除两个过程的效率都不高。
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后的程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
为了解决效率问题,”复制“算法出现了,将可用的内存划分为两块,每次只使用其中一块, 当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样就不用考虑内存碎片等复杂情况。
现在的商业虚拟机都采用这这种收集算法来回收新生代。根据研究表明,新生代中98%的对象都是”朝生夕死“的,所以新生代中将内存分为一块较大的Eden空间和两块较小的Survivor空间(from和to),每次使用Eden和其中一块Survivor,当回收时,将Eden和刚才用过的Survivor中还存活的对象一次性的复制到另外一个Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
Hotspot虚拟机默认使用Eden和Survivor的大小比例是8:1,也就是Eden占8,form和to各占1。
当Survivor空间不够的时候,需要依赖老年代进行分配担保。
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的;如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
标记-整理算法
标记整理算法的“标记”过程和标记-清除算法一致,只是后面并不是直接对可回收对象进行整理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商业虚拟机的垃圾收集都采用”分代收集“算法,其主要思想是将Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。
在新生代采用复制算法,上面已经讲过了。而老年代因为对象存活率高,没有额外空间为它进行分配担保,就必须使用”标记清理“或者”标记整理“算法来进行回收。
例子分析(查看GC日志)
GC日志是一个很重要的工具,它准确记录了每一次的GC的执行时间和执行结果,通过分析GC日志可以优化堆设置和GC设置,或者改进应用程序的对象分配模式。
JVM的GC日志的主要参数包括如下几个:
- -XX:+PrintGC 输出GC日志
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -Xloggc:../logs/gc.log 日志文件的输出路径
引用计数算法
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假设在这行发生了GC,objA和ojbB是否被回收
System.gc();
}
}
设置JVM运行参数:
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-verbose:gc
-Xloggc:gc.log
得到GC日志,我们分析当调用System.gc()的时候,objA和ojbB是否被回收。
2017-07-01T12:23:12.844-0800: 0.268: [Full GC (System.gc()) [PSYoungGen: 624K->0K(38400K)] [ParOldGen: 8K->530K(87552K)] 632K->530K(125952K), [Metaspace: 3043K->3043K(1056768K)], 0.0059705 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap after GC invocations=2 (full 1):
PSYoungGen total 38400K, used 0K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, 0% used [0x0000000795580000,0x0000000795580000,0x0000000797600000)
from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
ParOldGen total 87552K, used 530K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, 0% used [0x0000000740000000,0x0000000740084858,0x0000000745580000)
Metaspace used 3043K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 336K, capacity 386K, committed 512K, reserved 1048576K
}
从日志中[PSYoungGen: 624K->0K(38400K)]可以看出虚拟机并没有因为a和b互相引用就不回收它们,这也说明虚拟机并不是通过引用计数算法来判断对象是否存在引用的。
新生代MInor GC
通过此例可以分析JVM的内存分配和回收策略:对象优先在Eden分配。
public class TestAllocation {
private static final int _1MB = 1024 * 1024;
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
public static void main(String[] args) {
testAllocation();
}
}
设置JVM运行参数:
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
参数解释:在运行时通过-Xms20M、-Xmx20M、-Xmn10M这三个参数限制了Java堆的大小为20M,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。XX:SurvivorRatio=8决定了新生代的中Eden区和一个Survivor区的空间比例为8:1。
运行结果:
Heap
PSYoungGen total 9216K, used 7799K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 95% used [0x00000007bf600000,0x00000007bfd9dda0,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen total 10240K, used 4096K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 40% used [0x00000007bec00000,0x00000007bf000010,0x00000007bf600000)
Metaspace used 3114K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 342K, capacity 386K, committed 512K, reserved 1048576K
执行testAllocation()中分配allocation4对象的语句时会发生一次MinorGC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有占到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,一次发生了MInorGC。GC期间虚拟机又发现已有的3个2MB所需的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只能通过分配担保机制提前转移到老年代中去。
所以这次GC的结果就是4MB的allocation4对象顺利分配在Eden中,Survivor空闲,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。
调优
GC优化是迫不得已才采用的手段,多数导致GC问题的Java应用,都不是因为参数设置不当,而是代码问题。所以在实际使用中,分析GC情况优化代码比优化GC参数要多得多。
GC优化的目的有两个
- 将转移到老年代的对象数量降低到最小;
- 减少full GC的执行时间;
为了达到上面的目的,一般地,你需要做的事情有:
- 减少使用全局变量和大对象;
- 调整新生代的大小到最合适;
- 设置老年代的大小为最合适;
- 选择合适的GC收集器
参考资料:
JVM总结之GC的更多相关文章
- JVM内存管理------GC算法精解(五分钟教你终极算法---分代搜集算法)
引言 何为终极算法? 其实就是现在的JVM采用的算法,并非真正的终极.说不定若干年以后,还会有新的终极算法,而且几乎是一定会有,因为LZ相信高人们的能力. 那么分代搜集算法是怎么处理GC的呢? 对象分 ...
- JVM内存管理------GC简介
为何要了解GC策略与原理? 原因在上一章其实已经有所触及,就是因为在平时的工作和研究当中,不可避免的会遇到内存溢出与内存泄露的问题.如果对GC策略与原理不了解的情况下碰到了前面所说的问题,很多时候会让 ...
- JVM系列二:GC策略&内存申请、对象衰老
JVM里的GC(Garbage Collection)的算法有很多种,如标记清除收集器,压缩收集器,分代收集器等等,详见HotSpot VM GC 的种类 现在比较常用的是分代收集(generatio ...
- JVM学习之GC常用算法
出处:博客园左潇龙的技术博客--http://www.cnblogs.com/zuoxiaolong,多谢分享 GC策略解决了哪些问题? 既然是要进行自动GC,那必然会有相应的策略,而这些策略解决了哪 ...
- jvm系列:Java GC 分析
Java GC就是JVM记录仪,书画了JVM各个分区的表演. 什么是 Java GC Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之 ...
- JVM基础系列第14讲:JVM参数之GC日志配置
说到 Java 虚拟机,不得不提的就是 Java 虚拟机的 GC(Garbage Collection)日志.而对于 GC 日志,我们不仅要学会看懂,而且要学会如何设置对应的 GC 日志参数.今天就让 ...
- 【转载】JVM系列二:GC策略&内存申请、对象衰老
JVM里的GC(Garbage Collection)的算法有很多种,如标记清除收集器,压缩收集器,分代收集器等等,详见HotSpot VM GC 的种类 现在比较常用的是分代收集(generatio ...
- 触发JVM进行Full GC的情况及应对策略
堆内存划分为 Eden.Survivor 和 Tenured/Old 空间,如下图所示: 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为M ...
- JVM架构和GC垃圾回收机制
深入理解系列之JDK8下JVM虚拟机(1)——JVM内存组成 https://blog.csdn.net/u011552404/article/details/80306316 JVM架构和GC垃圾回 ...
- java面试题之----JVM架构和GC垃圾回收机制详解
JVM架构和GC垃圾回收机制详解 jvm,jre,jdk三者之间的关系 JRE (Java Run Environment):JRE包含了java底层的类库,该类库是由c/c++编写实现的 JDK ( ...
随机推荐
- VR全景是继互联网后的第二王朝吗?
VR虚拟现实.VR全景广泛用于游戏中,带上VR眼镜,有身临其境般的感觉.于是近些年围绕着 "下一代计算平台",国内外兴起一股虚拟现实热,在这样的形势下,VR眼镜在国内打的十分火热. ...
- 初码-Azure系列-迁移PHP应用至Azure的一些实践记录和思考
最近客户在逐步迁移应用从阿里云到Azure,这次又轮到一个PHP+MySQL应用了,顺便也记一下流水账. 需求:迁移部署在阿里云上的ECS服务器(系列2,IO优化+2核4G+50G的SSD云盘+10M ...
- vue-cli+webpack在生成的项目中使用bootstrap
在也个html页面中加入bootstrap是很方便,就是一般的将css和js文件通过Link和Script标签就行. 那么在一个用vue-vli生成的前端项目中如何加入?因为框架不一样了,略微要适应一 ...
- neutron flat和vxlan网络访问外网流量走向
OpenStack版本:Mitaka 物理节点: Hostname Management IP Tunnel IP Role test-ctrl-01 192.168.100.11 192.168.1 ...
- 全景技术大揭秘,市场核心早洞悉——VR全景加盟
未来已来,未来已见.2017是3D全景创业的天时,全景行业逐步走向成熟.全景智慧城市专注vr全景6年,技术国内遥遥领先.全景智慧城市市场总监常诚,透漏3D全景技术和市场的核心. 拍摄全景必备的设备:单 ...
- Spring读取xml配置文件的原理与实现
本篇博文的目录: 一:前言 二:spring的配置文件 三:依赖的第三方库.使用技术.代码布局 四:Document实现 五:获取Element的实现 六:解析Element元素 七:Bean创造器 ...
- [python标准库]Pickle模块
Pickle-------python对象序列化 本文主要阐述以下几点: 1.pickle模块简介 2.pickle模块提供的方法 3.注意事项 4.实例解析 1.pickle模块简介 The pic ...
- 是时候开始用C#快速开发移动应用了
从2015年接触Xamarin到至今已经2个年头,我对Xamarin的技能没有长进多少,但它却已经足够成熟到在跨平台移动开发工具中占有一席之地.在扫了一些资料之后,突然发现国外有很多移动端的应用已经是 ...
- django使用model创建数据库表使用的字段
Django通过model层不可以创建数据库,但可以创建数据库表,以下是创建表的字段以及表字段的参数.一.字段1.models.AutoField 自增列= int(11) 如果没有的话,默认会生成一 ...
- 安装并配置Apache
从今天开始,我将开始Web开发的学习.本系列博客将陆续记录我学习过程中的收获和困惑,从前端到后端,一探Web开发的流程和内容.我目前掌握的有C/C++,有一些使用C进行嵌入式开发的经验,C++就马马虎 ...