内存优化1- JVM和GC基础

Jvm内存模型:

从Jvm内存模型中入手对于理解GC会有很大的帮助,不过这里只需要了解一个大概,说多了反而混淆视线。一般来说说,JVM运行时的数据区域有两个区:线程私有区,共享数据区

线程私有区(每个线程都会开辟一个这样的区)
  1. 程序计数器
    相当于一个执行代码的指示器,用来确认下一行执行的代码的地址
    是没有OOM的区
  2. 虚拟机栈
    我们平时说的栈就是这块区域,java虚拟机规范中定义了outofmemory(OOM)和stackoverflow异常,这块区域中不会产生内存碎片
  3. 本地方法栈
    Native方法区

在hotspotVM把虚拟机栈和本地方法栈合为一个栈区

共享数据区

Jvm(Java虚拟机)主要(这里强调主要二字)管理两种类型内存:堆和非堆。 堆是运行时数据区域,所有类实例和数组的内存均从此处分配。 非堆是JVM留给自己用的,包含方法区、JVM内部处理或优化所需的内存(如 JIT Compiler,Just-in-time Compiler,即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。

方法区:
ClassLoader加载类信息
常量,静态变量
编译后的代码
常量池:字面量public satic final java常量;符号引用 类,接口全名,方法名
堆(虚拟机能管理的最大的一块内存 GC的主战场):对象实例,数组内容等

image.png

image.png

大家一般new的对象和数组都是在堆中的,而GC主要回收的内存也是这块堆内存,所以我们优化之前要先了解堆的内存模型。

堆内存模型

堆内存由垃圾回收器的自动内存管理系统(GC)回收。 堆内存分为两大部分:新生代和老年代。比例为1:2。 老年代主要存放应用程序中生命周期长的存活对象。 新生代又分为三个部分:一个Eden区和两个Survivor区,比例为8:1:1。 Eden区存放新生的对象。 Survivor存放每次垃圾回收后存活的对象。

image.png

GC垃圾回收器

可回收对象的判定

现在市面上有两种算法用来判定对象是否可回收。

1.引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。


image.png

优点是简单,高效,现在的objective-c用的就是这种算法。 缺点是很难处理循环引用,比如图中相互引用的两个对象则无法释放。 这个缺点很致命,有人可能会问,那objective-c不是用的好好的吗? 我个人并没有觉得objective-c好好的处理了这个循环引用问题,它其实是把这个问题抛给了开发者。

2.可达性分析算法(根搜索算法)

为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。 从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。


image.png

这样即使循环引用了,只要没有被GC Roots引用了依然会被回收。这个GC Roots的定义就要考究了,Java语言定义了如下GC Roots对象:

虚拟机栈(帧栈中的本地变量表)中引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。

GC是需要2次扫描才回收对象,所以我们可以用finalize去救活丢失引用的对象
例如:
static App a;
@Override
protected void() throws Throwable{
    super.finalize();
    a=this;
}
Stop The World

有了上面的垃圾对象的判定,我们还要考虑一个问题,请大家做好心里准备,那就是Stop The World。 因为垃圾回收的时候,需要整个的引用状态保持不变,否则判定是垃圾,等我稍后回收的时候它又被引用了,这就全乱套了。所以,GC的时候,其他所有的程序执行处于暂停状态,卡住了。 幸运的是,这个卡顿是非常短(尤其是新生代),对程序的影响微乎其微 (关于其他GC比如并发GC之类的,在此不讨论)。 所以GC的卡顿问题由此而来,也是情有可原,暂时无可避免。

几种垃圾回收算法
1. 标记清除算法 (Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。 优点是简单,容易实现。 缺点是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。 示意图如下:


image.png
2. 复制算法 (Copying)

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。 优缺点就是,实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。 从算法原理我们可以看出,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。 示意图如下:


image.png
3. 标记整理算法 (Mark-Compact)

该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。 所以,特别适用于存活对象多,回收对象少的情况下。 示意图如下:

image.png
4. 分代回收算法

分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的的特点综合而成。这种综合是考虑到java的语言特性的。 这里重复一下两种老算法的适用场景:

复制算法:适用于存活对象很少。回收对象多
标记整理算法: 适用用于存活对象多,回收对象少

刚好互补!不同类型的对象生命周期决定了更适合采用哪种算法。 于是,我们根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 这就是分代回收算法。 现在回头去看堆内存为什么要划分新生代和老年代,是不是觉得如此的清晰和自然了?
PS:Java虚拟机(这里默认指Hotspot)采用的就是分代回收算法,现在是不是明白了前面堆内存为什么要分新生代和老年代了吧。
我们再说的细一点:

对于新生代采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,采用Copying算法效率最高。但是,但是,但是,实际中并不是按照上面算法中说的1:1的比例来划分新生代的空间的,而是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,比例为8:1:1.。为什么?下一节深入分析。
由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

深入理解分代回收算法

对于这个算法,我相信很多人还是有疑问的,我们来各个击破,说清楚了就很简单。

为什么不是一块Survivor空间而是两块?

这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到Survivor A空间,但是第二次GC的时候,Survivor A空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。 所以,这里就需要两块Survivor空间来回倒腾。

为什么Eden空间这么大而Survivor空间要分的少一点?

新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率,缓解了Copying算法的缺点。 我看8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。 新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。

Eden空间和两块Survivor空间的工作流程

这里本来简单的Copying算法被划分为三部分后很多朋友一时理解不了,也确实不好描述,下面我来演示一下Eden空间和两块Survivor空间的工作流程。

现在假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。

// 分配了一个又一个对象
放到Eden区
// 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...

从这段流程中,我相信大家应该有了一个清晰的认识了,当然为了说明原理,这只是最简化版本。

对象回收和引用类型的关系

大致了解一下各个引用类型以及它们对对象回收造成的影响,后面我们会用代码去更详细的讲解一下这方面的知识。有兴趣的同学可以去看看这篇简书:
https://www.jianshu.com/p/ade51a91dfd6
1.强引用:Object obj=new Object();
2.弱引用:弱引用通过WeakReference类实现,弱引用和软引用很像但是弱引用的级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存(立即回收的方式)。
3.软引用:软引用需要通过SoftReference类来实现,当一个对象只具有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统将会回收它。软引用通常用于对内存敏感的程序中。
4.虚引用:PhantomReference,幽灵、幻影引用 不对生存造成任何影响,用于跟踪GC的回收通知。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容

  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,658评论 0 7
  • 转载blog.csdn.net/ning109314/article/details/10411495/ JVM工...
    forever_smile阅读 5,358评论 1 56
  • Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进来,墙里面的人想出来。 对象...
    胡二囧阅读 1,084评论 0 4
  • Java虚拟机整体篇幅如下: Java虚拟机基础——1Java的内存模型Java虚拟机基础——2JVM运行时数据区...
    隔壁老李头阅读 5,574评论 8 25
  • 有人会问我,你~还会写诗?我会反问他,你讲的诗?是什么?我孤陋寡闻,不曾与之相识。 我不以诗为师。对,这是一种态度...
    风声栋作阅读 580评论 22 56