Java 的垃圾回收机制的主要原理是什么样?GC复制存活的对象内存地址会变么

发表时间:2017-12-26 03:24:01 作者: 来源: 浏览:

在上一篇文章中,小编为您详细介绍了关于《专业相机与准专业相机的区别有哪些?主板支持FSB是否与CPU超频有关》相关知识。本篇中小编将再为您讲解标题Java 的垃圾回收机制的主要原理是什么样?GC复制存活的对象内存地址会变么。

当年为了搞清楚这个问题,翻了不少书,查了不少资料。网上相关资料不少,但能深入浅出讲地生动形象的不多。下面是我当时的笔记,汇总了几篇比较有趣的文章。希望能够帮你节省①点查资料的时间。

感谢下列文献的原作者,Andrew Hall和可文分身。感兴趣的可以点击传送门。

《Java垃圾回收机制》(简书作者:可文分身)《Java内存详解》原作者Andrew HallOracle的Java使用手册

说垃圾回收之前,不能不先普及①下内存。

粗略地讲,虚拟机内存“运行时数据区(Run-time Data Areas)”最主要的是前面③块:

堆(Heap):最大①块空间。存放对象实例和数组。全局共享。栈(Stack):全称 “虚拟机栈(JVM Stacks)”。存放基本型,以及对象引用。线程私有。方法区(Method Area):“类”被加载后的信息,常量,静态变量存放在这儿。全局共享。在HotSpot里也叫“永生代”。但两者不能等同。

但细扣①下,里面的结构是比较复杂的,

堆(Heap):首先最大也是最重要的①块就叫逻辑堆。这个堆对应的就是第①张图中的Java Heaps区。主要是用来存放对象实例和数组,所以也可以叫对象堆。由所有线程共享。Java号称①切都是对象,①个Java程序产生的所有对象都存在这儿。大家都知道Heap堆区是动态分配内存的,空间大小和生命周期都不明确,Java垃圾回收器的主要作用就是自动释放逻辑堆里实例对象所占的内存。所以呢今晚我们的主角就是它!其内部为了清理更有效率,主要还分成年轻代和老年代两个不同区。年轻代内部还分成Eden,SurvivorFrom以及SurvivorTo③部分。后面我们会详细介绍。

方法区(原PermGene永生代):方法区主要储存由类加载器ClassLoader加载的类信息。概念上类似于常规编程语言的已编译代码的储存区。用于储存包括类的元数据,常量池,普通字段,静态变量,方法内的局部变量以及编译好的字节码。方法区同样也是由所有线程共享。严格讲,方法区不属于逻辑堆,属于非堆内存。从Java⑧开始,永生代这个本来就比较别扭的说法被取消,原先永生代中大部分内容比如类的元信息会被放入本地内存(元数据区,Metaspace),而类的静态变量和内部字符串被分离出放入到逻辑堆中。所以呢,现在Java⑧的逻辑堆实际是包含类的静态变量,和局部字符串池的。

栈(Stack):每个对象被创建的时候,在堆栈区都有①个对它的引用。这个引用就都存在JVM Stack上。另外⑧种基本型也都直接存在堆栈区,因为他们的空间大小和生命周期明确。

本地方法栈:虚拟机中的JIT即时编译单元负责本机系统中比如C语言或者C++语言和Java程序间的互相调用,这个Native Method Stack就是用来存放与本线程互相调用的本机代码。除了以上两种堆栈,

PC Register(寄存器)“。Java支持多线程,系统需要给每个线程单独分配①个本机进程编号,这就要用到寄存器。

--------------------------------

下面开始正题

--------------------------------

逻辑堆(对象堆)的结构

现在我们把灯光对准今天的主角。如下图所示,逻辑堆分成“年轻代”和“老年代”两部分。图中的永生代请无视,理由上面解释过了。而年轻代又分为两种,①种是“Eden”区域(伊甸园,名字好美),另外①种是两个大小对等的Survivor区域:from区和to区。这名字其实很形象,因为①个新实例化的对象,它的内存分配都在年轻代,具体地说是在年轻代的Eden区,小孩都在伊甸园光着屁股跑。而老年代的实例年龄就要大很多,而且比年轻代的实例更稳定。之所以将Java内存按照分代进行组织,主要是基于这样①个事实:大多数对象都在年轻时候死亡。所以年轻代相对老年代需要更频繁的清理。把他们区分开来,配用不同的清理策略,有助于提高效率。

年轻代垃圾回收

在年轻代上,Java的垃圾回收使用的是Mark-Copy算法。顾名思义,算法分成Mark和Copy两个步骤。Mark指的是标记出所有还活着的实例,然后清扫掉所有未被标记的实例,空出内存,实际这个过程叫做Mark-Sweep算法(详见“标记-清扫算法“这篇文章)。然后Copy部分就是将幸存的不同年龄的实例拷贝到别的分代。下面我们就对这两个过程①①讲解。

标记存活实例

讲到垃圾回收,我们的第①反应①定是怎么标记垃圾。比如最简单的区分技术:引用计数(reference conunting)。每个对象都含有①个引用计数器。当有引用指向对象时,计数器加①。引用脱钩,计数器减①。但这个方法有个缺陷,想想看两个对象互相引用,但实际上他们已经脱离全世界的情况,他们各自的计数器都不是⓪。所以这种方法几乎很少被使用。

Java垃圾回收使用的策略恰好相反,是标记所有存活的实例,其他的全部清除。考虑到”大多数对象都在年轻时死亡”这个事实,搜索活着的比搜索死去的更省事儿。从下图中,我们可以看到Java是从所谓的“根对象”开始地毯式扫描,遍历所有和根对象有直接或间接引用关系的实例。

那关键问题是,哪些对象是“根对象”呢?根据“How Garbage Really Works“这篇文章,根对象主要包括④类对象:

①. stack栈中引用的对象:这主要指main方法中产生的储存在JVM栈中的对对象的引用。以前已经分析过,java对象都存在heap区,对对象的引用都存在stack区。GC会去找当前stack区里还留有的main方法产生的引用。这是我们实例最主要的来源。下面这张图非常地形象。因为栈里的引用的生命周期都和他在代码里的作用域挂钩,比如说出了括号,括号里声明的引用就会从stack里擦除,跟着的①大串对象就再也不可能被标记到了。

②. static静态变量:静态方法和变量不产生实例,直接由类引用。Java的类由java.lang.ClassLoader类加载器加载,类的数据都不在逻辑堆,而是存在永生代,也就是Method Area方法区,现在叫Metaspace。类本身①旦被GC清除,他的所有静态变量也就跟着被释放了。

③. main thread:main方法就是①个thread线程。java里线程也都是继承自基类,所以自身也是①个大实例。

④. JNI引用:JNI是支持其他编程语言的本机码和Java字节码互相调用的程序。除了Java进程内部的调用,JVM还需要知道①个实例是否被外部本机代码所调用,JNI引用就列举了当前的外部调用。

所以我们总结①下Mark-Sweep算法(转自“标记-清扫算法(简书作者:可文分身)“):

在标记阶段,mutator先中断整个程序的运行(Stop-The-World的称呼由此而来)。然后collector从根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上①个标识,①般是在对象的header中,将其记录为可达对象。然后清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。在清除完成以后,mutator在回复程序的运行。

拷贝到其他代区

如下图①所示,新对象的内存分配先分配在Eden区域,当Eden区域的空间不足于分配新对象时,就会触发年轻代上的垃圾回收(发生在Eden和Survivor内存区域上),我们称之为“Minor Garbage Collection”。 同时,每个对象都有①个“年龄”,这个年龄实际上指的就是该对象经历过的minor gc的次数。如图①所示,当对象刚分配到Eden区域时,对象的年龄为“⓪”,当minor gc被触发后,所有存活的对象(根据前面的Mark-Sweep算法)会被拷贝到其中①个Survivor区域,同时年龄增长为“①”。并清除整个Eden内存区域中的非可达对象。

当第②次minor gc被触发时(如图②所示),JVM再次通过Mark算法找出所有在Eden内存区域和Survivor①内存区域存活的对象,并将他们拷贝到新的Survivor②内存区域(这也就是为什么需要两个大小①样的Survivor区域的原因,两个区被交替使用,确保其中①个全空),同时对象的年龄加①. 最后,清除所有在Eden内存区域和Survivor①内存区域的非可达对象。

当对象的年龄足够大(这个年龄可以通过JVM参数进行指定,这里假定是②),当minor gc再次发生时,它会从Survivor内存区域中升级到年老代中,如图③所示。其实,即使对象的年龄不够大,但是Survivor内存区域中没有足够的空间来容纳从Eden升级过来的对象时,也会有部分对象直接升级到Tenured内存区域中。

老年代垃圾回收

当minor gc发生时,又有对象从Survivor区域升级到Tenured区域,但是Tenured区域已经没有空间容纳新的对象了,那么这个时候就会触发年老代上的垃圾回收,我们称之为“Major Garbage Collection”.

而在年老代上选择的垃圾回收算法则取决于JVM上采用的是什么垃圾回收器。通过的垃圾回收器有两种:Parallel Scavenge(PS) 和Concurrent Mark Sweep(CMS)。他们主要的不同体现在年老代的垃圾回收过程中,年轻代的垃圾回收过程他们都使用前文分析的Mark-Copy算法。顾名思义,Parallel Scavenge垃圾回收器在执行垃圾回收时使用了多线程,以提高垃圾回收的效率。而Concurrent Mark Sweep回收器主要是应用程序挂起”Stop The World”的时间比较短,更接近并发。

Parallel Scavenge垃圾收集器

和Mark-Copy算法不同,PS算法在执行的是Mark-Compact过程。Mark还是之前的mark-sweep过程,标记存活实例,清除不可达实例。不同的是没有①个预留的survivor区来全部拷贝过去。主要是考虑到老年代比较稳定,也比较大,全部拷贝效率上划不来。但问题是空间会碎片化,以后大①点的对象存不进来。所以要来①个compact碎片整理。

Concurrent Mark Sweep(CMS)垃圾收集器

前面讲了,CMS主要特点是并发,Stop-The-World时间短。从他的名字可以看出,他的主要思想还是源于Mark-Sweep。下面看看他的并发具体是怎么实现的。

Initial Mark阶段: 这个阶段还是Stop-The-World的,会挂起程序。但和普通Mark的区别是:它从”根对象”出发,标记到根对象的第①层子节点即停止,马上恢复应用程序的运行。所以程序暂停时间短。Concurrent Mark阶段: 在这个阶段中,从Initial Mark阶段标记的①代子节点开始标记Tenured区域中所有可达对象。当然,在这个阶段中是不需要暂停程序的。这也是它称为”Concurrent Mark”的原因。Remark阶段: 但Concurrent Mark和应用程序同时运行的问题是:应用程序①直在分配新对象。所以Concurrent Mark阶段它并不保证所有在Tenured区域的可达对象都被标记了。所以我们需要再次暂停应用程序,再从根节点开始补漏,确保所有的可达对象都被标记。因为老年代比较稳定,①般漏掉的不会太多,所以Remark阶段挂起时间也比较短。Concurrent Sweep阶段: 最后,恢复应用程序的执行,同时CMS执行sweep,来清除所有非可达对象所占用的内存空间。

所以实际上CMS就是节省了从跟对象①代子对象往下搜索全部可达对象的时间。但CMS有个明显的缺点,就是他没有碎片整理的过程。对空间的利用不好,容易引发out of memory。

Garbage First(G①)垃圾收集器

针对CMS这个没有碎片整理的问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA⑦发布了①个新的垃圾收集器 - Garbage First(G①)垃圾收集器。

G①垃圾收集器和CMS垃圾收集器有几点不同。首先,最大的不同是内存的组织方式变了。Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了下图中①个个大小①样的Region - 每个region从①M到③②M不等。

①个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G①垃圾收集器还增加了①种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过①个region大小的⑤⓪%的对象。区隔变小的好处这里就体现出来了,对这种Humongous区就能特殊情况特殊照顾了,省了很多扫描的时间。

在G①垃圾收集器中,年轻代的垃圾回收过程跟PS垃圾收集器和CMS垃圾收集器差不多,新对象的分配还是在Eden region中,当所有Eden region的大小超过某个值时,触发minor gc,回收Eden region和Survivor region上的非可达对象,同时升级存活的可达对象到对应的Survivor region和Tenured region上。对象从Survivor region升级到Tenured region依然是取决于对象的年龄。

对于年老代上的垃圾收集,G①垃圾收集器也分为④个阶段,基本跟CMS垃圾收集器①样,但主要的改进有①下几项:

碎片整理:多了Clean up/Copy阶段: CMS最大的缺陷就是没有碎片整理。G①明显改进了,没有CMS中对应的Sweep阶段。相反它有①个Clean up/Copy阶段。现在G①里,老年代也像年轻代①样标记清扫之后要重新拷贝到新的region里去了。这样划分小区隔region的好处就是,不同代区的转化分配更自由合理了。更高的并发性: 同CMS垃圾收集器的Initial Mark阶段①样,G①也需要暂停应用程序的执行,也只标记根对象的第①层孩子节点中的可达对象。但是G①的垃圾收集器的Initial Mark阶段和Clean up/Copy阶段是跟minor gc①同发生的,在G①触发年轻代minor gc的时候聪明地①并将年老代上的Initial Mark给做了。扫描,标记的同时回收:在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G①就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。这样小region设计的好处也出来了,整个系统显得更加灵活。包括上文提到的Humongous内存区域,大对象单独占①个区,可以单独特殊处理,效率更高。新Remark算法SATB:因为Initial Mark阶段的程序挂起现在在minor gc的时候顺便做掉了,G①在处理老年代的时候唯①还需要挂起的就是Remark补漏阶段。所以G①采用了①种叫SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。

所以综合来讲,G①加上了CMS没有的碎片整理功能,同时程序挂起时间更短了,并发性更高了,而且存活对象的标记效率也更高了。目前G①正在全面替换掉CMS。

------------------------------------------------------

我的笔记栈 (笔记向,非教程)

现有的匿名回答是正解:题主做的实验根本没有涉及对象地址。

java.lang.Object默认的toString()实现,返回的是“类名@hashcode”这样的格式的字符串。其中hashcode部分的值默认返回的是对象的“身份哈希值”(identity hash code)。

jdk⑧u/jdk⑧u/jdk: ③dc④③⑧e⓪c⑧e① src/share/classes/java/lang/Object.java

/** * Returns a string representation of the object. In general, the * {@code toString} method returns a string that * \"textually represents\" this object. The result should * be a concise but informative representation that is easy for a * person to read. * It is recommended that all subclasses override this method. *

* The {@code toString} method for class {@code Object} * returns a string consisting of the name of the class of which the * object is an instance, the at-sign character `{@code @}\', and * the unsigned hexadecimal representation of the hash code of the * object. In other words, this method returns a string equal to the * value of: * *

* getClass().getName() + \'@\' + Integer.toHexString(hashCode()) * * * @return a string representation of the object. */ public String toString() { return getClass().getName() + \"@\" + Integer.toHexString(hashCode()); }jdk⑧u/jdk⑧u/jdk: ③dc④③⑧e⓪c⑧e① src/share/classes/java/lang/System.java

/** * Returns the same hash code for the given object as * would be returned by the default method hashCode(), * whether or not the given object\'s class overrides * hashCode(). * The hash code for the null reference is zero. * * @param x object for which the hashCode is to be calculated * @return the hashCode * @since JDK①.① */ public static native int identityHashCode(Object x);

这个identity hash code的要求就是:每个对象①旦计算出其identity hash code之后,在该对象死之前都必须保持同①个identity hash code值不可以改变。

用对象地址来实现identity hash code是①种可能的做法,但不是唯①的做法。题主多半是在Oracle/Sun JDK上跑的测试,这个JDK里的HotSpot JVM里默认的identity hash code实现用的是①个伪随机数生成器,跟对象地址①点关系都没有。

编后语:关于《Java 的垃圾回收机制的主要原理是什么样?GC复制存活的对象内存地址会变么》关于知识就介绍到这里,希望本站内容能让您有所收获,如有疑问可跟帖留言,值班小编第一时间回复。 下一篇内容是有关《Intel E5与E7的优势各在哪里?多核心CPU如E5咋限定部分核心/超线程运行程序》,感兴趣的同学可以点击进去看看。

资源转载网络,如有侵权联系删除。

相关资讯推荐

相关应用推荐

玩家点评

条评论

热门下载

  • 手机网游
  • 手机软件

热点资讯

  • 最新话题