本文在个人技术博客不同步发布,详情可用力戳

亦可扫描屏幕右侧二维码关注个人公众号,公众号内有个人联系方式,等你来撩...

相关链接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8)

1、 你必须了解的java内存管理机制-运行时数据区

2、 你必须了解的java内存管理机制-内存分配

3、 你必须了解的java内存管理机制-垃圾标记

前言

  前面花了两篇文章对JVM的内存管理机制做了较多的介绍,通过第一篇文章先了解了JVM的运行时数据区,然后在第二篇文章中通过一个创建对象的实例介绍了JVM的内存分配的相关内容!那么,万众瞩目的JVM垃圾回收是时候登场了!JVM垃圾回收这块的内容相对较多、较复杂。但是,想要做好JVM的性能调优,这块的内容又必须了解和掌握!

正文

1、怎么找到存活对象?

  通过上篇文章我们知道,JVM创建对象时会通过某种方式从内存中划分一块区域进行分配。那么当我们服务器源源不断的接收请求的时候,就会频繁的需要进行内存分配的操作,但是我们服务器的内存确是非常有限的呢!所以对不再使用的内存进行回收再利用就成了JVM肩负的重任了! 那么,摆在JVM面前的问题来了,怎么判断哪些内存不再使用了?怎么合理、高效的进行回收操作?既然要回收,那第一步就是要找到需要回收的对象!

1.1、引用计数法

  实现思路:给对象添加一个引用计数器,每当有一个地方引用它,计数器加1。当引用失效,计数器值减1。任何时刻计数器值为0,则认为对象是不再被使用的。举个小栗子,我们有一个People的类,People类有id和bestFriend的属性。我们用People类来造两个小人:

      People p1 = new People();
People p2 = new People();

  通过上篇文章的知识我们知道,当方法执行的时候,方法的局部变量表和堆的关系应该是如下图的(注意堆中对象头中红色括号内的数字,就是引用计数器,这里只是举栗,实际实现可能会有差异):

  

  造出来的p1和p2两个人,我想让他们互为最好的朋友,于是代码如下:

	People p1 = new People();
People p2 = new People();
p1.setBestFriend(p2);
p2.setBestFriend(p1);

  对应的引用关系图应该如下(注意引用计数器值的变化):

  

  然后我们再做一些处理,去除变量和堆中对象的引用关系。

		People p1 = new People();
People p2 = new People(); p1.setBestFriend(p2);
p2.setBestFriend(p1); p1 = null;
p2 = null;

  这时候引用关系图就变成如下了,由于p1和p2对象还相互引用着,所以引用计数器的值还为1。

  

  优点:实现简单,效率高。

  缺点:很难解决对象之间的相互循环引用。且开销较大,频繁的引用变化会带来大量的额外运算。在谈实现思路的时候有这样一句话“任何时刻计数器值为0,则认为对象是不再被使用的”。但是通过上面的例子我们可以看到,虽然对象已经不再使用了,但计数器的值仍然是1,所以这两个对象不会被标记为垃圾。

  现状:主流的JVM都没有选用引用计数法来管理内存。

1.2、可达性分析

  实现思路:通过GC Roots的对象作为起始点,从这些节点向下搜索,搜索走过的路径成为引用链,当一个对象到GC Root没有任何引用链相连时,则证明对象是不可用的。如下图,红色的几个对象由于没有跟GC Root没有任何引用链相连,所以会进行标记。

  

  优点:可以很好的解决对象相互循环引用的问题。

  缺点:实现比较复杂;需要分析大量数据,消耗大量时间;

  现状:主流的JVM(如HotSpot)都选用可达性分析来管理内存。

2、标记死亡对象

  通过可达性分析可以对需要回收的对象进行标记,是否标记的对象一定会被回收呢?并不是呢!要真正宣告一个对象的死亡,至少要经历两次的标记过程!

2.1、第一次标记

  在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记。并且判断此对象是否必要执行finalize()方法!如果对象没有覆盖finalize()方法或者finalize()已经被JVM调用过,则这个对象就会认为是垃圾,可以回收。对于覆盖了finalize()方法,且finalize()方法没有被JVM调用过时,对象会被放入一个成为F-Queue的队列中,等待着被触发调用对象的finalize()方法。

2.2、第二次标记

  执行完第一次的标记后,GC将对F-Queue队列中的对象进行第二次小规模标记。也就是执行对象的finalize()方法!如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合。如果对象没有,也可以认为对象已死,可以回收了。

  finalize()方法是被第一次标记对象的逃脱死亡的最后一次机会。在jvm中,一个对象的finalize()方法只会被系统调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用。由于该方法是在对象进行回收的时候调用,所以可以在该方法中实现资源关闭的操作。但是,由于该方法执行的时间是不确定的,甚至,在java程序不正常退出的情况下该方法都不一定会执行!所以在正常情况下,尽量避免使用!如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用,如File相关类的close()方法。下面我们看一个在finalize中逃脱死亡的栗子吧:

public class GCDemo {
public static GCDemo gcDemo = null; public static void main(String[] args) throws InterruptedException { gcDemo = new GCDemo();
System.out.println("------------对象刚创建------------");
if (gcDemo != null) {
System.out.println("我还活得好好的!");
} else {
System.out.println("我死了!");
} gcDemo = null;
System.gc();
System.out.println("------------对象第一次被回收后------------");
Thread.sleep(500);// 由于finalize方法的调用时间不确定(F-Queue线程调用),所以休眠一会儿确保方法完成调用
if (gcDemo != null) {
System.out.println("我还活得好好的!");
} else {
System.out.println("我死了!");
} gcDemo = null;
System.gc();
System.out.println("------------对象第二次被回收后------------");
Thread.sleep(500);
if (gcDemo != null) {
System.out.println("我还活得好好的!");
} else {
System.out.println("我死了!");
} // 后面无论多少次GC都不会再执行对象的finalize方法
} @Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("execute method finalize()");
gcDemo = this;
}
}

  执行结果如下,具体就不多说啦,不明白的就自己动手去试试吧!

  

3、枚举根节点

  通过上面可达性分析我们了解了有哪些GC Root,了解了通过这些GC Root去搜寻并标记对象是生存还是死亡的思路。但是具体的实现就是那张图显示的那么简单吗?当然不是,因为我们的堆是分代收集的,那GC Root连接的对象可能在新生代,也可能在老年代,新生代的对象可能会引用老年代的对象,老年代的对象也可能引用新生代。如果直接通过GC Root去搜寻,则每次都会遍历整个堆,那分代收集就没法实现了呢!并且,枚举整个根节点的时候是需要线程停顿的(保证一致性,不能出现正在枚举 GC Roots,而程序还在跑的情况,这会导致 GC Roots 不断变化,产生数据不一致导致统计不准确的情况),而枚举根节点又比较耗时,这在大并发高访问量情况下,分分钟就会导致系统瘫痪!啥意思呢,下面一张图感受一下:

  

  如果是进行根节点枚举,我们先要全栈扫描,找到变量表中存放为reference类型的变量,然后找到堆中对应的对象,最后遍历对象的数据(如属性等),找到对象数据中存放为指向其他reference的对象……这样的开销无疑是非常大的!

  为解决上述问题,HotSpot 采用了一种 “准确式GC” 的技术,该技术主要功能就是让虚拟机可以准确的知道内存中某个位置的数据类型是什么,比如某个内存位置到底是一个整型的变量,还是对某个对象的reference,这样在进行 GC Roots枚举时,只需要枚举reference类型的即可。那怎么让虚拟机准确的知道哪些位置存在的是reference类型数据呢?OopMap+RememberedSet!

  OopMap记录了栈上本地变量到堆上对象的引用关系,在GC发生时,线程会运行到最近的一个安全点停下来,然后更新自己的OopMap,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的OopMap,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。这样,OopMap就避免了全栈扫描,加快枚举根节点的速度。

  OopMap解决了枚举根节点耗时的问题,但是分代收集的问题依然存在!这时候就需要另一利器了- RememberedSet。对于位于不同年代对象之间的引用关系,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是RememberedSet!所以“新生代的 GC Roots ” + “ RememberedSet存储的内容”,才是新生代收集时真正的GC Roots(G1 收集器也使用了 RememberedSet 这种技术)。

3.1、安全点

  HotSpot在OopMap的帮助下可以快速且准确的完成GC Roots枚举,但是在运行过程中,非常多的指令都会导致引用关系变化,如果为这些指令都生成对应的OopMap,需要的空间成本太高。所以只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint)。如何在GC发生时让所有线程(不包括JNI线程)运行到其所在最近的安全点上再停顿下来?这里有两种方案:

  1、抢先式中断:不需要线程的执行代码去主动配合,当发生GC时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断。这样一直等待所有线程都在安全点后开始GC。

  2、主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时主动轮询这个标记,一旦发现标记被改变(出现中断标记)时,就将自己中断挂起。目前所有商用虚拟机全部采用主动式中断。

  安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点(在主动式中断中,轮询标志的地方和安全点是重合的,所以线程在遇到这些指令时都会去轮询中断标志!)。

3.2、安全区域

  使用安全点似乎已经完美解决如何进入GC的问题了,但是GC发生的时候,某个线程正在睡觉(sleep),无法响应JVM的中断请求,这时候线程一旦醒来就会继续执行了,这会导致引用关系发生变化呢!所以需要安全区域的思路来解决这个问题。线程执行进入安全区域,首先标识自己已经进入安全区域。线程被唤醒离开安全区域时,其需要检查系统是否已经完成根节点枚举(或整个GC)。如果已经完成,就继续执行,否则必须等待,直到收到可以安全离开Safe Region的信号通知!

你必须了解的java内存管理机制(三)-垃圾标记的更多相关文章

  1. 你必须了解的java内存管理机制(二)-内存分配

    前言 在上一篇文章中,我们花了较大的篇幅去介绍了JVM的运行时数据区,并且重点介绍了栈区的结构及作用,相关内容请猛戳!在本文中,我们将主要介绍对象的创建过程及在堆中的分配方式. 相关链接(注:文章讲解 ...

  2. 你必须了解的java内存管理机制(一)-运行时数据区

    前言 本打算花一篇文章来聊聊JVM内存管理机制,结果发现越扯越多,于是分了四遍文章(文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8),本文为其中第一篇.from 你必须了解的java内存 ...

  3. 你必须了解的java内存管理机制(四)-垃圾回收

    本文在个人技术博客不同步发布,详情可用力戳 亦可扫描屏幕右侧二维码关注个人公众号,公众号内有个人联系方式,等你来撩... 相关链接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8) ...

  4. JVM内存管理机制和垃圾回收机制

    JVM内存管理机制和垃圾回收机制 JVM结构 图片描述: java源码编译成class文件 class文件通过类加载器加载到内存 其中方法区存放的是运行时的常量.静态变量.类信息等,被所有线程共享 堆 ...

  5. java内存管理机制

    JAVA 内存管理总结 1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 ( ...

  6. 浅析java内存管理机制

    内存管理是计算机编程中的一个重要问题,一般来说,内存管理主要包括内存分配和内存回收两个部分.不同的编程语言有不同的内存管理机制,本文在对比C++和Java语言内存管理机制的不同的基础上,浅析java中 ...

  7. java内存管理机制剖析(一)

    最近利用工作之余学习研究了一下java的内存管理机制,在这里记录总结一下. 1-1.java内存区域 当java程序运行时,java虚拟机会将内存划分为若干个不同的数据区域,这些内存区域创建和销毁的时 ...

  8. php内存管理机制与垃圾回收机制

    PHP内存管理机制 1 var_dump(memory_get_usage()); //获取内存 2 $a = "laruence"; //定义一个变量 3 var_dump(me ...

  9. php内存管理机制、垃圾回收机制

    一.内存管理机制 先看一段代码: <?php //内存管理机制 var_dump(memory_get_usage());//获取内存方法,加上true返回实际内存,不加则返回表现内存 $a = ...

随机推荐

  1. [Example of Sklearn] - 分类对比

    refrence :http://cloga.info/python/2014/02/07/classify_use_Sklearn/ 加载数据集 这里我使用pandas来加载数据集,数据集采用kag ...

  2. 简明Python3教程 11.数据结构

    简介 数据结构基本上就是 – 可以将一些数据结合到一起的结构,换言之用于存储一组相关的数据. python拥有4种内建数据结构 – 列表,元组(tuple),字典和集合. 我们将看到如何它们,它们又是 ...

  3. SQList3 and SQL入门学习笔记

    SQL 这是一个标准的计算机语言进行访问和操作数据库. 什么是 SQL? ·       SQL 指结构化查询语言 ·       SQL 使我们有能力訪问数据库 ·       SQL 是一种 AN ...

  4. x:Static , StaticResource 和DynamicResource等XAML 扩展用法

    原文:x:Static , StaticResource 和DynamicResource等XAML 扩展用法 前提: <system:String x:Key="{Component ...

  5. WPF ScrollViewer(滚动条) 自定义样式表制作 (改良+美化)

    原文:WPF ScrollViewer(滚动条) 自定义样式表制作 (改良+美化) 注释直接写在代码里了   不太理解意思的 可以先去看看我上一篇  WPF ScrollViewer(滚动条)  自定 ...

  6. Win8 Metro(C#)数字图像处理--2.40二值图像轮廓提取算法

    原文:Win8 Metro(C#)数字图像处理--2.40二值图像轮廓提取算法  [函数名称]   二值图像轮廓提取         ContourExtraction(WriteableBitm ...

  7. ubuntu Linux 操作系统安装与配置

    Ubuntu是一个以桌面应用为主的Linux操作系统.Ubuntu每六个月发布一个新版本(一般是4和10月份,命名为YY.MM),每一个普通版本都将被支持 18个月,长期支持版(Long Term S ...

  8. 使用内核对象Mutex可以防止同一个进程运行两次

    用互斥法实现防止程序重复运行,使用内核对象Mutex可以防止同一个进程运行两次.注意:是名称相同的进程,而不是exe,因为exe程序可以改名. using System.Threading; publ ...

  9. MongoDB centos安装问题 error while loading shared libraries: libnetsnmpmibs.so.31

    安装mongodb-linux-x86_64-enterprise-rhel70-4.0.5 cd  /usr/mongodb tar -zxvf mongodb-linux-x86_64-enter ...

  10. 使用Visual Studio Code创建第一个ASP.NET Core应用程序

    全文翻译自:Your First ASP.NET Core Application on a Mac Using Visual Studio Code 这篇文章将向你展示如何在Mac上写出你的第一个A ...