聊聊JVM垃圾收集机制

来源:51CTO  责任编辑:小易  

Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对

【线上直播】11月21日晚8点贝壳技术总监侯圣文《数据安全之数据库安全黄金法则》-->

 

1.垃圾回收目的:Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理

1、运行时数据区域

堆内存分为 年轻代 老年代 和持久代年轻代 又分为 eden 区和 survivor 区

JVM在执行java程序的过程中会把它所管理的内存划分成若干个不同的数据区域。

对的,java具体的gc机制你可以google看看,最简单的计数器的做法就是为每一个对象的一次引用计

(1)程序计数器

内存资源是有限的,垃圾回收只回收“垃圾”,对于你的程序运行有用的对象不会被回收。

程序计数器(Program Counter Register)是一块比较小的内存区域,它可以看作是当前线程所执行的字节码指令的行号计数器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令。

王励勤,先后三次夺得世乒赛男单冠军,超越了中国男乒两大传奇——庄则栋(3金)、郭跃华(2金2银),包括孔令辉(1金1银2铜)和刘国梁(1金1银)两人更是望尘莫及。内行人知道,与奥运会相比,世乒赛参赛的一流高手数量更多,冠军含金量更高,王励勤能三捧勃莱德杯,可以说是当之无愧的“大力”。马琳,这位在北京奥运会囊括两金的乒坛奇才,最辉煌的成绩应该是世界杯,他是公认的“世界杯之王”。马琳拿过4次世界杯单打冠军,虽然他遗憾没能在勃莱德杯上刻上自己的名字,但他两次与王励勤的世乒赛单打对决,场面精彩度和曲折度至今让人难忘。马琳5次获得三大赛男单冠军,也是中国男乒最高纪录。最新的竞聘中,“二王一马”时代的王皓

由于java虚拟机的多线程是通过线程的轮流切换并分配CPU时间片来实现的,在任何一个确定的时刻,一个核只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每一条线程都需要有一个独立的程序计数器,因此,程序计数器是线程私有的内存。

一般情况下,大家都认为吃醋嫉妒是一种负面情绪。可是,在恋爱中,嫉妒是必不可少的。爱情的独占欲和排他性很强。如果心爱的人与其他异性接触甚至有暧昧的举动,我们心中会顿生愤怒甚至仇恨的情愫,这就是所谓的“嫉妒”。可以说,只要我们还爱着对方,就会自然产生嫉妒的情绪。有位心理学家对男女之间的嫉妒进行了长期的调查分析。在长达七年的时间里,他对一定数量的情侣进行了跟踪调查,并最终得出这样的结论:七年前在爱情中嫉妒心比较强的情侣,在七年后结婚的概率很高。因此,我们才说嫉妒是爱情的晴雨表。虽说如此,但总是陷在嫉妒的情绪里,是一件非常痛苦的事情。那么,如果对方有了要移情别恋的念头,我们该怎么办呢?最有效的方法就是

如果线程正在执行的是一个java方法,那么程序计数器中的值是正在执行的虚拟机字节码指令的地址;如果是一个Native方法,这个计数器的值为空(undefined)。此内存区域是java虚拟机规范中唯一一个没有定义任何OutOfMemoryError情况的内存区域。

作为一名武警老兵曾经看了五年看守所,每天都看这里面的人。记得有好几次叫我们去绑死刑犯,当时早上六点多就过去了,干警让里面的人都转过去蹲下,我们直接过去两人一左一右把死刑犯给提出去。绑他的时候他一直都很淡定,给他买的早餐问他吃吗,他说不吃。他说只想抽根烟,干警把烟给他点上然后和他聊家常。他说我卡里还有八百块钱麻烦我走了之后帮我给我老婆一下,干警说你放心吧。当时我在旁边看着心里有一种说不出来的感觉,感觉一个人在强大在罪恶心中也有柔软的地方。感觉到了法律的敬畏。最后看着他被法警开车过来带走了,带去安乐死。这样的事经历过好几次,真的觉得生命很脆弱一闪而逝前一秒还能看到他,下一秒他已经去了别的世界。

(2)虚拟机栈

不懂灯语,你还好意思开车?在通讯基本靠吼的年代,“灯语”在航海领域起到了至关重要的作用,利用灯光的闪烁频率,以二进制的莫尔斯码传递讯息,可以帮助船员在较远的目视距离相互沟通,话题转回到我们现在,我们日常行车时,其实也会遇到很多约定成俗的汽车“灯语”。大灯亮一下超车或者绿灯亮后前车不走大灯闪三下发现邻车有问题两下大灯提醒,亮双跳表示不满夜晚遭遇强灯闪眼大灯闪一下“同意”,大灯连闪“拒绝”行车要并道间断式亮刹车灯后车跟车太近连续闪烁大灯变近光为远光提醒行人或者非机动车注意开双闪紧急制动,开双闪,双闪警示灯可以有效警示后车,面对突发状况时可以让后车以最短时间发现,双闪三秒表示感谢,除了在紧情况下用作

虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个称为栈帧(Stack Frame)的东西,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用开始直至执行完成,都对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

通常所说的栈,一般指的是虚拟机栈中的局部变量表部分。局部变量表存储了编译期可知的基本数据类型、引用类型和returnAddress类型(指向一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的。

如果线程请求的栈深度超过了虚拟机的最大深度,那么就会抛出StackOverFlowError异常;如果虚拟机可以动态拓展并且在拓展时无法申请到足够的内存,将抛出OutOfMemoryError异常。

(3)本地方法栈

本地方法栈(Native Method Stack)和虚拟机栈一样,都是线程私有的,只不过虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机执行native方法服务。

(4)Java堆

对大多数应用程序来说,java堆(Java Heap)都是java虚拟机所管理的内存区域中最大的一块。java堆是被所有线程所共享的一块内存区域,在虚拟机启动时创建。此内存区域存在的唯一目的就是存放对象实例,几乎所有的对象都在此内存区域上进行分配。

根据java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。在实现时,可以实现成固定大小的,也可以实现成可拓展的。当拓展时,如果无法申请到足够的内存,将抛出OutOfMemoryError异常。

(5)方法区

方法区(Method Area)也是被各个线程所共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。

对于习惯在HotSpot虚拟机上开发、部署程序的的开发者来说,更习惯于把方法区称为”永久代“,但本质上两者并不等价。

方法区可以处于不连续的内存空间,也可以选择成可拓展,还可以选择不实现垃圾收集。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

(6)运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载以后进入方法区的运行时常量池中存放。

运行时常量池除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中,因此,运行时常量池相对于class文件常量池的一个重要特征是具备动态性。

符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标不一定加载到内存中。例如org.simple.People类引用了org.simple.Language类,在编译时,People类并不知道Language类的实际内存地址,因此只能使用符号来代替,这就是符号引用。

2、对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包含2部分的信息。第一部分用于存储对象自身运行时数据如哈希码、GC分代年龄、锁状态标志等等,官方称之为“Mark Word”。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,但并不是所有的虚拟机实现都必须在对象头中保留类型指针。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据中确定数组的大小。

实例数据部分是对象真正存储的有效信息。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。

对齐填充并不是必须的。因为HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍(为什么这么要求?),也就是说对象的大小必须是8字节的整数倍。

3、对象的访问定位

栈上的reference数据定位和访问堆中的具体对象的方式取决于虚拟机实现,目前主要有句柄和直接指针两种。

使用句柄访问方式的话,java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例和类型数据各自具体的地址信息。

如果使用直接指针访问,那么java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,而reference中存储的就是对象地址。

使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问的最大好处是速度更快,节省了一次指针定位的时间开销。HotSpot虚拟机采用的是直接指针方式。

4、垃圾收集器

1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期,人们就在思考GC需要完成的三件事:

(1)哪些内存需要回收

(2)什么时候回收

(3)如何回收

对于第一个问题,哪些内存需要回收,就是哪些对象是不可用的。第二个问题,什么时候回收,一句话概述就是内存不够用的时候进行回收。第三个问题,就涉及到回收的具体实现上了。

4.1 对象的存活判定

(1)引用计数算法

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

引用计数算法无法解决对象之间循环引用的问题。

(2)可达性分析

这种算法的基本思想就是通过一系列的称为”GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

在Java中,可以作为GC Roots的对象包括下面几种:

a. 虚拟机栈(栈帧中的本地变量表)中引用的对象

b. 方法区中类静态变量所引用的对象

c. 方法区中常量所引用的对象

d. 本地方法栈中JNI(即Native方法)引用的对象

主流的商用程序语言(java、c#等)都是采用的可达性分析算法来判定对象是否存活。

4.2 垃圾收集算法

(1)标记-清除算法

标记-清除算法(Mark-Sweep)是最基础的收集算法。算法分为”标记”和”清除"两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收被标记的对象。

这种算法存在两个缺点:

a. 效率问题。”标记”和”清除”两个阶段的效率都不高

b. 内存碎片问题。标记清除之后会产生大量不连续的内存碎片,当以后要分配较大的对象时,可能会因为找不到足够的连续内存而不得不提前触发另一次垃圾收集动作。

(2)复制算法

复制算法(Copying)是为了解决标记清除算法的效率问题而提出。该算法将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块内存使用完毕,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行回收,分配内存时,也不用考虑内存碎片的问题,只需要移动堆顶指针即可。

这种算法的缺点是内存利用率只有50%

现代的商业虚拟机都采用这种算法来回收新生代。因为新生代中的对象的存活率比较低,所以并不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另一块Survivor空间中,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比率是8:1,也就是说每次新生代中可用内存空间为整个新生代的90%,只有10%的内存会被浪费掉。如果某一次回收时,对象的存活率超过了10%,也就意味着to survivor空间不够用,那么此时就需要依赖老年代来进行分配担保(Handle Promotion),也即这些还存活的对象将直接进入老年代。

(3)标记-整理算法

标记-整理算法(Mark-Compact)是针对老年代的特点(对象存活率较高、没有额外空间进行分配担保)而提出的。这种算法分为标记和整理两个阶段,标记阶段和标记-清除算法的标记阶段一致,但是整理阶段不是直接对标记对象进行回收,而是让还存活的对象向一端移动,然后一次性清理掉端边界以外的内存。

(4)分代收集

当前商业虚拟机的垃圾收集都采用的是分代收集算法。分代收集并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把java堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。

新生代中,对象的存活率较低,就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象存活率高,也没有额外空间进行分配担保,所以采用标记-清除算法或者标记-整理算法。

4.3 垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

(1)Serial收集器

serial收集器是最基本、发展历史最悠久的收集器。它是一个单线程的收集器,在它工作时,必须暂停所有其他的工作线程,直到它收集结束,这也被称为”Stop The World”。

它是虚拟机运行在client模式下的新生代默认的收集器:因为它简单而高效,对于限定单个CPU的环境来说,Serial收集器因为没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

(2)ParNew收集器

ParNew 收集器是Serial收集器的多线程版本。

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,一个很重要但是与性能无关的原因是:除了Serial收集器外,只有它能与CMS收集器配合工作。

ParNew 收集器默认开启的收集线程数与CPU的数量相同,可以使用参数-XX:ParallelGCThreads来控制垃圾收集的线程数量。

(3)Parallel Scavenge收集器

Parallel Scavenge收集器看起来和ParNew收集器没什么区别,但其实 Parallel Scavenge收集器的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短Stop The World的时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),因此 Parallel Scavenge收集器也称为“吞吐量优先”的收集器 。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

停顿时间越短,就越适合需要与用户交互的程序,良好的响应速度能够提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数和直接设置吞吐量大小的 -XX:GCTimeRatio 参数。

除上述两个参数之外,Parallel Scavenge还提供一个参数 -XX:UseAdaptiveSizePolicy。如果开启了这个参数,就不需要手动指定新生代的大小(-Xmn)、Eden与Survivor的比率( -XX:SurvivorRatio)、晋升老年代对象的年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC自适应调节策略。

(4)Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,使用”标记-整理”算法。这个收集器的主要意义也是给client模式下的虚拟机使用。如果在Server模式下,它还有另外两个作用:一种用途是在JDK 1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在CMS收集器发生Concurrent Mode Failer时使用。

(5)Parallel Old收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,使用”标记-整理”算法。这个收集器是在JDK 1.6 中提供的。在此之前,Parallel Scavenge一直处在比较尴尬的状态。原因是:如果新生代中选择了Parallel Scavenge收集器,那么老年代只能选择Serial Old收集器,而Serial Old收集器在服务端应用性能上不是很突出,导致使用了Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。

直到Parallel Old出现,“吞吐量优先”收集器终于有了比较名副其实的应用组合。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 的组合。

(6)CMS收集器

CMS收集器(Concurrent Mark Sweep,并发标记清除)是一种以获取最短停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运作过程大致分为4个步骤:

a. 初始标记

b. 并发标记

c. 重新标记

d. 并发清除

其中,初始标记和重新标记过程仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。重新标记阶段就是为了修正并发标记阶段因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。

由于整个过程中耗时最长的并发标记和并发清除两个过程,收集器线程都可以和用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

这个收集器存在3个明显的缺点:

a. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分的线程(或者说是CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS收集器默认开启的回收线程数量是 (CPU数量+3)/4,也就是说当CPU在4个以上时,并发回收时垃圾收集线程至少要占用 25% 的CPU资源。

b. CMS无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failer”失败而导致另一次Full GC的产生。并发清理阶段,用户程序还在运行着,伴随着就会产生新的垃圾,这一部分的垃圾出现在标记之后,无法在当次收集中处理掉,只好等待下一次收集时处理,这一部分垃圾称之为“浮动垃圾”。也是因为并发清除阶段,用户程序还在运行,那也就必须要预留一部分内存空间给并发收集时用户程序使用。在JDK1.5的默认设置下,CMS收集器在当老年代使用了68%的空间后就会被激活,在JDK1.6种,这个阈值被提升到了92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failer”失败,这时虚拟机将启动后背预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集。

c. 由标记清除算法引起的空间碎片问题 。

(7)G1收集器

G1(Garbage First)收集器是一款面向服务端应用的垃圾收集器,JDK 1.7中才出现。相比其他的垃圾收集器,G1具有更加明显的优势:

a. G1中虽然还保留分代的概念,但是G1能独立管理整个Java堆,不再需要像以前那样要多个收集器配合。因为G1将整个堆划分成多个大小相等的独立区域(Region),新生代和老年代的概念虽然保留着,但是不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。

b. 空间整合。G1从整体上来看是基于标记-整理算法实现,从局部(两个Region之间)来看是基于复制算法实现,避免了内存碎片的问题。

c. 可预测的停顿。这是G1相对于CMS的另一大优势。G1除了追求低停顿以外,还能建立可预测的停顿时间模型,能让使用者指明在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪每个Region里面的垃圾堆积的价值大小(回收这块Region所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage First名称的由来)。

5、内存分配与回收策略

Java技术体系中所讲的自动内存管理最终可以归结为自动化的解决了两个问题:给对象分配内存以及回收分配给对象的内存。内存的回收上面已经说过原理,下面看一下内存分配。

(1)对象优先在Eden区上分配

对象在新生代Eden区中分配,当Eden区中没有足够的内存进行分配时,虚拟机将发起一次Minor GC。

(2)大对象直接进入老年代

虚拟机提供了一个参数 -XX:PretenureSizeThreshold ,大于这个参数设置值的对象将不会在Eden区域上进行分配,而是直接在老年代进行分配。这样做的目的是为了避免在Eden区以及两个Survivor区之间发生大量的内存复制(当大对象的存活率比较高时)。

(3)长期存活的对象将进入老年代

前面在说对象的内存布局时提到对象头中有一部分存储对象自身运行时所需要的数据,例如哈希码、GC分代年龄,这里面的GC分代年龄指的就是对象在新生代中熬过的GC次数,每熬过一次Minor GC,对象的年龄就增加一岁,当它的年龄增加到一定程度(默认是15岁)就会被晋升到老年代。这个晋升老年代的阈值可以通过参数 -XX:MaxTenuringThreshold 来设置。

(4)动态对象年龄判定

对象的年龄不一定非要达到 MaxTenuringThreshold 设置的值才能晋升老年代。如果在Survivor中相同年龄的对象的大小之和超过了Survivor空间大小的一半,那么年龄大于或等于该年龄的对象将直接进入老年代,无需等到 MaxTenuringThreshold 要求的年龄。

(5)分配担保

在新生代进行Minor GC之前,虚拟机会做如下的事情:

(1)先检查老年代最大可用的连续内存空间是否大于新生代所有对象总空间,如果大于,那么Minor GC就是安全的,因为不会有对象进入老年代。否则进行步骤(2)

(2)查看 HandlePromotionFailure设置值是否允许分配担保失败,如果不允许,那么也要进行一次Full GC。否则进行步骤(3)

(3)检查老年代最大可用连续空间是否大于历次晋升老年代的平均大小,如果大于,会尝试进行一次Minor GC,但是可能会有风险。否则进行步骤(4)

(4)也要进行一次Full GC。

步骤(3)中提到会尝试进行一次Minor GC,但这次GC会存在风险,这句话是什么意思呢?前面在讲复制算法时提到,新生代在进行GC时,如果Eden空间和From Survivor空间中存活的对象大小之和超过了To Survivor空间的大小,那么这些存活对象将通过分配担保机制进入老年代。因为还没有进行Minor GC,所以虚拟机并不知道本次GC中存活对象的大小,所以只好采用历次晋升老年代对象的平均值来作为经验值,如果老年代可用的最大连续内存空间大于经验值,那么很有可能说明老年代可用的最大连续内存空间也大于本次Minor GC存活的对象大小之和,所以会尝试进行Minor GC。但是如果在进行Minor GC后发现存活对象大小之和大于老年代最大可用连续内存空间,那么这时老年代将无法存放这部分存活对象,这就是风险所在,此时老年代就要来一次Full GC以腾出空间。

扩展阅读,根据您访问的内容系统为您准备了以下内容,希望对您有帮助。

jvm的垃圾回收机制详解

1.JVM的gc概述

  gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作。

  在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和jvm支持的垃圾收集算法,便可以进行优化配置垃圾收集器。

  垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。gc首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。

  1.1.引用计数

  引用计数存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,jvm必须适当增减引用数。当某对象的引用数为0时,便可以进行垃圾收集。

  1.2.对象引用遍历

  早期的jvm使用引用计数,现在大多数jvm采用对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,gc必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。

  下一步,gc要删除不可到达的对象。删除时,有些gc只是简单的扫描堆栈,删除未标记的未标记的对象,并释放它们的内存以生成新的对象,这叫做清除(sweeping)。这种方法的问题在于内存会分成好多小段,而它们不足以用于新的对象,但是组合起来却很大。因此,许多gc可以重新组织内存中的对象,并进行压缩(compact),形成可利用的空间。

  为此,gc需要停止其他的活动活动。这种方法意味着所有与应用程序相关的工作停止,只有gc运行。结果,在响应期间增减了许多混杂请求。另外,更复杂的 gc不断增加或同时运行以减少或者清除应用程序的中断。有的gc使用单线程完成这项工作,有的则采用多线程以增加效率。

2.几种垃圾回收机制

  2.1.标记-清除收集器

  这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。

  2.2.标记-压缩收集器

  有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。

  2.3.复制收集器

  这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,jvm生成的新对象则放在另一半空间中。gc运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。

  2.4.增量收集器

  增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾。这会造成较小的应用程序中断。

  2.5.分代收集器

  这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。jvm生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。

  2.6.并发收集器

  并发收集器与应用程序同时运行。这些收集器在某点上(比如压缩时)一般都不得不停止其他操作以完成特定的任务,但是因为其他应用程序可进行其他的后台操作,所以中断其他处理的实际时间大大降低。

  2.7.并行收集器

  并行收集器使用某种传统的算法并使用多线程并行的执行它们的工作。在多cpu机器上使用多线程技术可以显著的提高java应用程序的可扩展性。

java 中垃圾收集机制的原理是???

Java技术提供了一个系统级的线程(Thread),即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。

  

  垃圾收集器线程是一种低优先级的线程,在一个Java程序的生命周期中,它只有在内存空闲的时候才有机会运行。它有效地防止了内存渗漏体的出现,并极大可能地节省了宝贵的内存资源。但是,通过Java虚拟机来执行垃圾收集器的方案可以是多种多样的。

  

  下面介绍垃圾收集器的特点和它的执行机制:

  

  垃圾收集器系统有自己的一套方案来判断哪个内存块是应该被回收的,哪个是不符合要求暂不回收的。垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System. gc 方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

  

  垃圾收集器的主要特点

  1.垃圾收集器的工作目标是回收已经无用的对象的内存空间,从而避免内存渗漏体的产生,节省内存资源,避免程序代码的崩溃。

  2.垃圾收集器判断一个对象的内存空间是否无用的标准是:如果该对象不能再被程序中任何一个"活动的部分"所引用,此时我们就说,该对象的内存空间已经无用。所谓"活动的部分",是指程序中某部分参与程序的调用,正在执行过程中,尚未执行完毕。

  3.垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。

  4.垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。

  5.不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。因此在程序执行过程中被分配出去的内存空间可能会一直保留到该程序执行完毕,除非该空间被重新分配或被其他方法回收。由此可见,完全彻底地根绝内存渗漏体的产生也是不可能的。但是请不要忘记,Java的垃圾收集器毕竟使程序员从手工回收内存空间的繁重工作中解脱了出来。设想一个程序员要用C或C++来编写一段10万行语句的代码,那么他一定会充分体会到Java的垃圾收集器的优点!

  6.同样没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。

  7.循环引用对象不会影响其被垃圾收集器收集。

  8.可以通过将对象的引用变量(reference variables,即句柄handles)初始化为null值,来暗示垃圾收集器来收集该对象。但此时,如果该对象连接有事件*(典型的 AWT组件),那它还是不可以被收集。所以在设一个引用变量为null值之前,应注意该引用变量指向的对象是否被监听,若有,要首先除去*,然后才可以赋空值。

  9.每一个对象都有一个finalize( )方法,这个方法是从Object类继承来的。

  10.finalize( )方法用来回收内存以外的系统资源,就像是文件处理器和网络连接器。该方法的调用顺序和用来调用该方法的对象的创建顺序是无关的。换句话说,书写程序时该方法的顺序和方法的实际调用顺序是不相干的。请注意这只是finalize( )方法的特点。

  11.每个对象只能调用finalize( )方法一次。如果在finalize( )方法执行时产生异常(exception),则该对象仍可以被垃圾收集器收集。

  12.垃圾收集器跟踪每一个对象,收集那些不可到达的对象(即该对象没有被程序的任何"活的部分"所调用),回收其占有的内存空间。但在进行垃圾收集的时候,垃圾收集器会调用finalize( )方法,通过让其他对象知道它的存在,而使不可到达的对象再次"复苏"为可到达的对象。既然每个对象只能调用一次finalize( )方法,所以每个对象也只可能"复苏"一次。

  13.finalize( )方法可以明确地被调用,但它却不能进行垃圾收集。

  14.finalize( )方法可以被重载(overload),但只有具备初始的finalize( )方法特点的方法才可以被垃圾收集器调用。

  15.子类的finalize( )方法可以明确地调用父类的finalize( )方法,作为该子类对象的最后一次适当的操作。但Java编译器却不认为这是一次覆盖操作(overriding),所以也不会对其调用进行检查。

  16.当finalize( )方法尚未被调用时,System. runFinalization( )方法可以用来调用finalize( )方法,并实现相同的效果,对无用对象进行垃圾收集。

  17.当一个方法执行完毕,其中的局部变量就会超出使用范围,此时可以被当作垃圾收集,但以后每当该方法再次被调用时,其中的局部变量便会被重新创建。

  18.Java语言使用了一种"标记交换区的垃圾收集算法"。该算法会遍历程序中每一个对象的句柄,为被引用的对象做标记,然后回收尚未做标记的对象。所谓遍历可以简单地理解为"检查每一个"。

  19.Java语言允许程序员为任何方法添加finalize( )方法,该方法会在垃圾收集器交换回收对象之前被调用。但不要过分依赖该方法对系统资源进行回收和再利用,因为该方法调用后的执行结果是不可预知的。

  通过以上对垃圾收集器特点的了解,你应该可以明确垃圾收集器的作用,和垃圾收集器判断一块内存空间是否无用的标准。简单地说,当你为一个对象赋值为null并且重新定向了该对象的引用者,此时该对象就符合垃圾收集器的收集标准。

在面试时怎么回答java垃圾回收机制

Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。

需要注意的是:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身,很多人来我公司面试时,我都会问这个问题的,70%以上的人回答的含义是回收对象,实际上这是不正确的。

System.gc()

Runtime.getRuntime().gc()

上面的方法调用时用于显式通知JVM可以进行一次垃圾回收,但真正垃圾回收机制具体在什么时间点开始发生动作这同样是不可预料的,这和抢占式的线程在发生作用时的原理一样。

java 技术:jvm垃圾回收机制有哪些

深入理解Java虚拟机:JVM高级特性与最佳实践

在《深入理解Java虚拟机:JVM高级特性与最佳实践》,介绍了几种垃圾回收算法

  • Mark-Sweep(标记-清除)算法

  • Copying(复制)算法

  • Mark-Compact(标记-整理)算法

  • Generational Collection(分代收集)算法

和HotSpot虚拟机中的7种垃圾收集器:

  • Serial

  • ParNew

  • Parallel Scavenge、

  • Serial Old

  • Parallel Old

  • CMS

  • G1

jvm中垃圾回收机制有哪些

Java虚拟机(JVM)的堆中储存着正在运行的应用程序所建立的所有对象,这些对象一般来说,堆的是由垃圾回收 来负责的,尽管JVM规范并不要求特殊的垃圾回收

  • 本文相关:
  • 【线上直播】11月21日晚8点贝壳技术总监侯圣文《数据安全之数据库安全黄金法则》
  • 一文详解JVM内存模型,从线程共享到本地方法栈再到Java堆
  • 十年架构师详解JVM运行原理
  • 虚拟化自动化挑战强调AI重要性
  • 不懂JVM,怎么当架构师,一文带你了解JVM
  • 虚拟机工具VirtualBox、VMWare与Hyper-V大比拼
  • 关于Jvm类加载机制,这一篇就够了
  • 创新推动社会可持续发展 东软解决方案论坛2013
  • VMware Site Recovery Manager (SRM) 5.0 安装配置实战
  • VMware View 5.0 从菜鸟到高手系列
  • VMware不止虚拟化 云计算全方案齐亮相
  • jvm的垃圾回收机制详解
  • java 中垃圾收集机制的原理是???
  • 在面试时怎么回答java垃圾回收机制
  • java 技术:jvm垃圾回收机制有哪些
  • jvm中垃圾回收机制有哪些
  • JAVA垃圾回收机制的工作原理?
  • jvm垃圾回收是什么时候触发的?垃圾回收算法
  • jvm垃圾回收机制,何时触发minorgc等操作
  • 关于JAVA的垃圾回收机制,下面哪些结论是正确
  • JVM有垃圾自动回收机制,那为什么还会有内存泄漏呢?
  • 免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved