在上一篇文章中,小编为您详细介绍了关于《专业相机与准专业相机的区别有哪些?主板支持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咋限定部分核心/超线程运行程序》,感兴趣的同学可以点击进去看看。
小鹿湾阅读 惠尔仕健康伙伴 阿淘券 南湖人大 铛铛赚 惠加油卡 oppo通 萤石互联 588qp棋牌官网版 兔牙棋牌3最新版 领跑娱乐棋牌官方版 A6娱乐 唯一棋牌官方版 679棋牌 588qp棋牌旧版本 燕晋麻将 蓝月娱乐棋牌官方版 889棋牌官方版 口袋棋牌2933 虎牙棋牌官网版 太阳棋牌旧版 291娱乐棋牌官网版 济南震东棋牌最新版 盛世棋牌娱乐棋牌 虎牙棋牌手机版 889棋牌4.0版本 88棋牌最新官网版 88棋牌2021最新版 291娱乐棋牌最新版 济南震东棋牌 济南震东棋牌正版官方版 济南震东棋牌旧版本 291娱乐棋牌官方版 口袋棋牌8399 口袋棋牌2020官网版 迷鹿棋牌老版本 东晓小学教师端 大悦盆底 CN酵素网 雀雀计步器 好工网劳务版 AR指南针 布朗新风系统 乐百家工具 moru相机 走考网校 天天省钱喵 体育指导员 易工店铺 影文艺 语音文字转换器