JVM深度理解(二):对象回收机制

第一部分的问题,我们最后再来解释,我们先了解下jvm的组成部分。首先我们先搞清楚,每个区存储的是些啥


截图.png

线程私有(这些区域生命周期随着线程结束而结束,也就释放,不需要gc)

程序计数器:Java 虚拟机的多线程是通过线程轮流切换并分配处理器的时间来完成的,一个时间,一个处理器只会执行一个线程的任务,当处理器在一个线程没执行完时,就切换到其他线程时,就会在程序记录器中记录执行到的行号。

我们写一个很简单的测试类Test3,然后将其反编译成字节码文件看下。其中圈起来的0,1,2,5即是行号(图中标的有误),程序计数器会记录执行到的具体行号。


截图2.png
截图3.png

虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型。

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出,下图栈1先进最后出来】

每一个栈帧就表明该方法又调用了另一个方法

局部变量表:1.局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。

2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,

对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址) (针对方法内部的局部变量)

操作栈:要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

通过代码讲个例子,简单的3行java代码,对应了9行的字节码文件

第一步 byte i = 15,首先会将15压入操作数栈,对应字节码bipush 15,然后出栈,将其存在局部变量表的1索引处,对应istore_1。(这点也和方法局部变量存储在局部变量表对应上了)

第二步 int j = 8,同理,会将8先压入操作数栈,然后出栈,存在局部变量表的索引2处。

第三步 int k = i+j,首先将局部变量表索引1,2处的i,j读出来,对应的是iload1,iload2命令。然后将操作数栈中的i和j出栈,进行相加,将相加结果压回操作数栈,对应iadd命令。最后将计算结果出栈,存到局部变量表索引3的位置,对应istore_3.

截图4.png

动态链接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如示例中test方法调用了test2方法,但是字节码文件仅仅是一个invokevirtual #2,#2就是org/redisson/mybatis/Test.3.test2方法的引用。

截图5.png

方法出口:记录当本方法执行完之后,回到主方法时改从哪一行执行。

本地方法栈:结构和虚拟机栈类似,不过是描述的调用native方法的执行的内存模型。

线程共享的区域

堆: Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,也就是我们大部分new的对象存在的地方。

方法区:存放着类的版本、字段、方法、接口和常量池(存储字面量和符号引用)。

截图6.png
截图7.png

了解了jvm各个区域存储的情况后,我们继续了解下jvm的内存回收回收机制。

截图8.png

首先明确一个问题,什么样的对象需要被回收。jvm会通过一些算法判断对象是否存活,那些不存活的对象就是gc回收的目标。

1.引用计数算法 早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。 优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。 缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。 2.可达性分析算法 目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。 它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

在Java语言中,可作为GC Roots的对象包含以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象(可以理解为:引用栈帧中的本地变量表的所有对象)
  2. 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
  3. 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
  4. 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。

(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。

(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

截图9.png

垃圾收集算法

1.标记/清除算法

2.复制算法

3.标记/整理算法

jvm采用分代收集算法对不同区域采用不同的回收算法。(这里的分代收集和上面三种算法不是并列的关系,而是说jvm不同处于不同代的对象,采用以上不同的收集算法,下面会详细描述)

新生代采用复制算法(标记出还在引用的对象,复制到另一块区域,将回收区域全部清空)

新生代中因为对象都是"朝生夕死的",【深入理解JVM虚拟机上说98%的对象,不知道是不是这么多,总之就是存活率很低】,适用于复制算法【复制算法比较适合用于存活率低的内存区域】。它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。

GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。

GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄战4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。

新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。

ps:当然不是所有的对象都是通过这种方式进行回收的

截图10.png

老年代采用标记/清除算法或标记/整理算法

“标记-清除”算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:

(1)效率问题:标记和清除过程的效率都不高,都是遍历实现的;

(2)空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这个算法整体效率会降低,但是可以让老年代的剩余空间尽可能连续,当有需要连续空间的大对象过来时,可以降低full gc的频率。

来个例子

可以通过jstat -gc pid查看应用对应区域的大小,其实s0c 也就是from区,s1c也就是to区,Ec 也就是eden区大小,正如之前说所说的为8:1:1,而老年代与新生代大概为2:1

可以看出,老年代占整个堆内存的2/3大概。

截图11.png

gc发生时间

minorgc:新生代空间不足,触发minor GC

fullgc:如果创建一个大对象,eden区中空间不足,直接保存到老年代中,当老年代空间不足时候,直接触发fullgc.

以下fullgc场景仅作补充,实际发生情况较少。

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。

3. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。1.8之后去掉了永久代,将类信息放置在本地内存空间(元空间)

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

4. Concurrent Mode Failure(CMS收集器产生的内存碎片后,老年代无法放入连续大对象)

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

截图12.png

其中GC就是指minorgc,可以看出minorgc时间很短,0.03秒,对系统影响较小。

而FullGC,则会触发著名的stop the world(stop the world简单来说就是gc的时候,停掉除gc外的java线程。),时间为0.6s,对系统影响较大。

OOM故障分析

可以通过打开如下参数,让程序第一次 oom时生成dump快照

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof

然后可以通过mat打开快照,查看具体的大对象。

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

推荐阅读更多精彩内容