当前位置: 首页 > 科技观察

Java虚拟机详解----GC算法与类型

时间:2023-03-17 12:32:49 科技观察

本文主要内容:GC概念GC算法   引用计数方法(不能解决循环引用问题,java不采用)  根搜索算法  现代虚拟机中的垃圾收集算法:      mark-clear      copyalgorithm(newgeneration)      mark-compression(oldgeneration)  generationalcollectionStop-The-World1.GC的概念:GC:GarbageCollection垃圾收集Lisp在1960年就使用了GC在Java中,GC的对象是Java堆和方法区(也就是***区)下面来解释一下上面三句话一个其一:(1)GC:GarbageCollection垃圾收集。这里所谓的垃圾是指系统运行过程中产生的一些无用的对象。这些对象占用一定的内存空间。如果长时间不释放,可能会导致OOM。在C/C++中,内存空间是程序员自己申请、管理和释放的,所以没有GC的概念。在Java中,后台有一个专门负责垃圾回收的线程进行监控和扫描,自动释放一些无用的内存。内存泄漏。(2)其实GC的历史比Java的历史还要长。Lisp于1960年诞生于麻省理工学院,是第一个真正使用内存动态分配和垃圾回收技术的语言。在Lisp还处于萌芽阶段的时候,人们在思考GC需要做的3件事:哪些内存需要回收?什么时候回收?如何回收?(3)内存区中的程序计数器、虚拟机栈、本地方法栈这三个区域伴随着线程诞生,线程被销毁;随着方法的进入和退出,栈中的栈帧有序执行。而入栈的操作,每个栈帧分配多少内存,基本在类结构确定的时候就知道了。在这些区域,回收不用想太多,因为当方法结束或者线程结束的时候,内存自然会被回收。Java堆不同于方法区。一个接口中的多个实现类所需要的内存可能不同,一个方法中的多个分支所需要的内存也可能不同。我们只知道程序何时运行。哪些对象,这部分内存的分配和回收是动态的,GC也会关注这部分内存。如果后面的文章中涉及到“内存”的分配和回收,那只是指一部分内存。#p#2。引用计数算法:(旧的垃圾回收算法。不能处理循环引用,Java没有采用)1.引用计数算法的概念:给对象加上一个引用计数器,只要有地方引用它,计数器的值递增1;当引用无效时,计数器值减1;任何时候计数器为0的对象都不能再使用。2、用户示例:引用计数算法实现简单,判断效率也高。在大多数情况下,这是一个很好的算法。它被应用在许多地方。例如:微软的COM技术:ComputerObjectModel使用ActionScript3的FlashPlayerPython但是主流的java虚拟机并没有使用引用计数算法来管理内存。主要原因是很难解决对象之间的相互循环引用问题。3、引用计数算法的问题:引用和解引用伴随着加减法,影响性能。致命缺陷:被循环引用的对象不能被回收上面三张图中,对于最右边的一张:循环引用的计数器没有一个为0,但是已经无法到达根对象,但是不能被释放。循环引用的代码示例:publicclassObject{Objectfield=null;publicstaticvoidmain(String[]args){Threadthread=newThread(newRunnable(){publicvoidrun(){ObjectobjectA=newObject();ObjectobjectB=newObject();//位置1objectA.field=objectB;objectB.field=objectA;//位置2//todosomethingobjectA=null;objectB=null;//位置3}});thread.start();while(true);}}上面的代码看起来有点故意为之,但实际上,在实际的编程过程中,经常会出现,比如两个具有一对一关系的数据库对象,各自维护着对另一个的引用。***A***循环只是为了让JVM不退出,没有实际意义。代码解释:代码中标出了1、2、3三个数字。当位置1的语句执行时,两个对象的引用计数都为1。当位置2的语句执行时,两个对象的引用计数都变为2。当执行位置3的语句时,即两者都归为空值后,两者的引用计数仍然为1。根据引用计数算法的回收规则,当引用计数还没有归0时,它不会被回收。对于我们现在使用的GC来说,当thread线程运行结束后,objectA和objectB都会被认为是要回收的对象。而如果我们的GC使用上面提到的引用计数算法,这两个对象将永远不会被回收,即使我们在使用后将对象显示为空值也没有任何作用。#p#3。根搜索算法:1、根搜索算法的概念:  由于引用计数算法的缺陷,JVM一般采用一种新的算法,称为根搜索算法。它的处理方式是设置几种根对象,当任意一个根对象对某个对象不可达时,就认为这个对象可以被回收。如上图所示,ObjectD和ObjectE是相互关联的,但是由于GC根无法到达这两个对象,所以D和E最终还是会被认为是GC对象。如果上图中使用了引用计数的方式,那么A-E这五个Object是不会被回收的。2、可达性分析:刚才我们提到了几种根对象的设置。当任何一个根对象对某个对象不可达时,就认为这个对象可以被回收。后面我们介绍标记清理算法/标记整理算法的时候,总会强调从根节点开始,把所有可达对象标记一次,那么什么是可达?这里解释如下:可达性分析:  以根(GCRoots)对象为起点,开始向下搜索。搜索所经过的路径称为“引用链”。当一个对象到达GCRoots而没有任何引用链连接时(利用图论的概念,从GCRoots到这个对象是不可达的),证明这个对象是不可用的。3、根(GCRoots):说到GC根(GCroots),在JAVA语言中,可以作为GC根的对象有以下几种:1、栈中引用的对象(栈中的局部变量表框架)。2.方法区中的静态成员。3、方法区常量引用的对象(全局变量)4、本地方法栈中JNI引用的对象(一般称为Native方法)。注意:***和第四种都是指方法的局部变量表,第二种表达的意思更明确,第三种主要是指声明为final的常量值。在根搜索算法的基础上,在现代虚拟机的实现中,主要有三种垃圾收集算法,即标记-清除算法、复制算法和标记-排序算法。这三种算法都扩展了寻根算法,但它们仍然非常容易理解。#p#4.Mark-sweep算法:1.mark-sweep算法的概念:Mark-sweep算法是现代垃圾回收算法的思想基础。标记清除算法将垃圾收集分为两个阶段:标记阶段和清理阶段。一种可行的实现方式是在标记阶段,先通过根节点,标记从根节点开始的所有可达对象。因此,未标记的对象是未引用的垃圾对象;然后,在清理阶段,清理所有未标记的对象。2、mark-clear算法详解:它的方法是当堆中的可用内存耗尽时,会停止整个程序(也称为stoptheworld),然后执行两个任务,*的**项目被标记,第二个项目被清除。标记:标记的过程其实就是遍历所有的GCRoots,然后将所有GCRoots可达的对象标记为存活对象。清除:清除的过程会遍历堆中的所有对象,清除所有未标记的对象。也就是说,当程序运行过程中可用内存耗尽时,会触发GC线程,挂起程序,然后重新标记存活的对象,最后将堆中所有未标记的对象被标记的对象全部清空,然后让程序恢复运行。看下图:上图代表了程序运行过程中所有对象的状态,它们的标志位都是0(即不标记,默认0不标记,1标记),假设此时当有效内存空间耗尽时,JVM会停止应用程序的运行并启动GC线程,然后开始标记工作。根据根搜索算法,标记之后对象的状态如下图所示:上图中可以看到,根据根搜索算法,从根对象可达的所有对象被标记为存活对象,此时第一阶段标记已经完成。接下来,需要进行第二阶段的清算。清除后剩余的对象和对象的状态如下图所示:从上图可以看出,未标记的对象会被回收清除,标记的对象会被清除。该对象将被留下,并将标记位重置为0。不用说,只需唤醒停止的程序线程,让程序继续运行即可。问题:为什么要停止程序的运行?答:这个其实不难理解。假设我们的程序和GC线程一起运行。想象一下这样的场景。假设我们刚刚在图中标记了最右边的物体,我们暂且记为A。结果此时在程序中新建了一个对象B,A对象可以到达B对象。但是由于此时A对象已经标记结束,此时B对象的标记位还是0,因为它错过了标记阶段。因此,在下一回合清关时,新对象B将被硬清关。这样一来,结果就不难想象了,GC线程会导致程序无法正常工作。以上结果当然是不能接受的。我们刚刚创建了一个新对象,但是经过一次GC,它突然变成了null。我们怎么玩这个?3、mark-clear算法的缺点:(1)首先它的缺点是效率比较低(递归和全堆对象遍历),导致停世界的时间比较长,特别是对于交互式应用.不能接受。试想,你玩一个网站,一个小时网站挂五分钟,你还玩吗?(2)第二点的主要缺点是这种方式清理的空闲内存是不连续的。这不难理解。我们的死亡对象随机出现在记忆的每个角落。清空之后,内存的布局自然就乱了。为了应对这种情况,JVM必须维护一个空闲内存列表,这是另一个开销。而且,在分配数组对象时,也不容易找到连续的内存空间。#p#五、复制算法:(新一代GC)复制算法的概念:将原来的内存空间分成两块,每次只使用其中一块,正在使用的内存中的内存会在使用过程中保存垃圾收集。将对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。与mark-clear算法相比,copy算法是一种相对高效的回收方式,不适合存活对象多的场合,比如oldgeneration(copy算法适合新生代的GC)。复制算法最大的问题是:空间浪费复制算法每次只回收整个半个区域的内存,分配内存时无需考虑内存碎片等复杂情况。只需将指针移到堆顶,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存减少到原来的一半,这就很可怕了。因此,从上面的描述不难看出,如果要使用复制算法,至少对象的存活率必须很低,最重要的是,我们必须克服50%的内存浪费。今天的商业虚拟机使用这种收集算法来回收新生代。新生代中98%的对象都是“生死存亡”,所以没有必要按照1:1的比例划分内存空间,而是将内存划分为一个较大的Eden空间和两个较小的Survivor空间,每次都使用Eden和Survivor空间之一。回收时,一次性将Eden和Survivor中的存活对象复制到另一个Survivor空间,最后清理掉刚刚使用过的Eden和Survivor空间。HotSpot虚拟机默认的Eden和Survivor比例是8:1。也就是说,每个新生代中的可用内存空间是整个新生代容量的90%(80%+10%),只会保留10%的空间。浪费。当然,98%的可回收对象只是一般场景下的数据。我们无法保证每次回收后存活的对象不超过10%。当Survivor空间不够时,我们需要依靠老年代来进行分配保证。对象直接进入老年代。整个过程如下图所示:上图中,绿色箭头所在的位置代表大对象,大对象直接进入老年代。根据上面的复制算法,现在我们看下面gc日志中的数字,应该可以理解:在上面的GC日志中,新生代的可用空间为13824K(12288K在eden区+来自空间中的1536K)。根据内存地址计算,新生代的总空间为15M,这15M空间=13824K+to空间的1536K。#p#六、mark-sorting算法:(老年代GC)介绍:如果在对象存活率高的情况下进行更多的复制操作,效率会变低。更重要的是,如果不想浪费50%的空间,就需要有额外的空间分配保证来应对已用内存中所有对象都是100%存活的极端情况,所以一般不能直接select这在老一代算法中。概念:mark-compression算法适用于存活对象较多的场合,比如老年代。它在mark-sweep算法的基础上做了一些优化。和mark-clear算法一样,mark-compression算法首先需要从根节点开始,对所有可达对象进行一次标记;但是随后,它并没有简单地清理未标记的对象,而是将所有幸存的对象压缩到内存的一端;之后,清理边界外的所有空间。Marking:它的第一阶段和marking/clearing算法完全一样,都是遍历GCRoots,然后标记存活下来的对象。整理:移动所有存活的对象,并按照内存地址的顺序排列,然后回收结束内存地址后的所有内存。因此,第二阶段称为收尾阶段。从上图可以看出,有标记的存活对象会按照内存地址排序排列,而没有标记的内存会被清理掉。这样,当我们需要为一个新的对象分配内存时,JVM只需要保存一段内存的起始地址,显然比维护一个空闲链表的开销要小很多。标记/排序算法不仅可以弥补标记/清除算法中内存区域分散的缺点,还可以消除复制算法中减半内存的高成本。但是,标记/整理算法的唯一缺点是效率不高。不仅要标记所有存活的对象,还要整理所有存活对象的引用地址。在效率方面,标记/排序算法低于复制算法。mark-sweep算法、copy算法、mark-and-sweep算法总结:三种算法都是基于寻根算法来判断一个对象是否应该被回收,寻根算法正常工作的理论基础是语法相关信息中变量的范围。因此,要想防止内存泄漏,最根本的方法就是把握变量的范围,不要使用C/C++的内存管理方式。当GC线程开启时,或者GC进程启动时,它们都必须暂停应用程序(stoptheworld)。它们的区别如下:(>表示前者优于后者,=表示两者效果相同)(1)效率:复制算法>标记/排序算法>标记/清除算法(这里的效率为只是时间复杂度的简单比较,实际情况不一定如此)。(2)内存均匀性:复制算法=标记/排序算法>标记/清除算法。(3)内存利用率:标记/排序算法=标记/清除算法>复制算法。注1:可见mark/clear算法是比较落后的算法,但后两种算法都是建立在这个基础上的。注2:时间和空间不能合并。#p#7。分代收集算法:(新生代GC+老年代GC)目前商用虚拟机的GC采用的是“分代收集算法”。这并不是一个新的想法,而是根据对象生存周期的不同,将内存分成若干块。Java堆一般分为新生代和老年代:寿命短的对象归入新生代,寿命长的对象归入老年代。少量对象存活,适用于复制算法:在新生代中,每次GC发现大量对象死亡,只有少量对象存活,所以采用复制算法,并且只需付出复制少量存活对象的代价就可以完成GC。大量对象存活,适合marking-cleaning/marking-organization:在老年代,由于对象存活率高,没有额外的空间分配保证,需要使用“mark-clean”/“标记-组织”算法GC。注意:老年代中的一小部分对象是进来的对象,因为新生代回收时老年代作为保证;大部分对象进入老年代是因为没有被GC多次回收。8.可达性:所有的算法都需要能够识别垃圾对象,所以需要给出可达性的定义。Reachable:  这个对象可以从根节点到达。  其实就是从根节点开始扫描,只要对象在引用链中,就是可达的。Resurrecable:  一旦所有的引用都被释放,它就被复活  因为对象在finalize()中有可能复活  被回收利用。finalize方法复活对象示例代码:packagetest03;/***Createdbysmyhvaeon2015/8/19.*/publicclassCanReliveObj{publicstaticCanReliveObjobj;//GC执行时会执行finalize方法,只会执行一次@Overrideprotectedvoidfinalize()throwsThrowable{super.finalize();System.out.println("CanReliveObjfinalizecalled");obj=this;//执行GC的时候会执行finalize方法,然后这行代码的作用就是复活null对象,然后变为Accessibility}@OverridepublicStringtoString(){return"IamCanReliveObj";}publicstaticvoidmain(String[]args)throwsInterruptedException{obj=newCanReliveObj();obj=null;//resurrectableSystem.out.println("***gc");System.gc();Thread.sleep(1000);if(obj==null){System.out.println("obj为空");}else{System.out.println("objisavailable");}obj=null;//不能被复活System.out.println("Secondgc");System.gc();Thread.sleep(1000);if(obj==null){System。出去。println("objisnull");}else{System.out.println("objisavailable");}}}需要注意第14行的注释。一开始,我们在第25行将obj设置为null,然后进行了一次GC。我们以为obj会被回收,其实并没有,因为GC的时候会调用11行的finalize方法,然后14行obj会被复活起来。紧接着,在第34行将obj设置为null,然后进行GC。这时候obj就被回收了,因为finalize方法只会执行一次。finalize方法使用总结:经验:避免使用finalize(),粗心的操作可能会导致错误。优先级低,调用的时候,不确定什么时候会发生GC不确定性,自然不知道finalize方法什么时候执行如果想使用finalize释放资源,我们可以使用try-catch-终于要换掉了#p#九、Stop-The-World:1、Stop-The-World概念:  Java中的一种全局暂停现象。globalpause,所有Java代码停止,native代码可以执行,但是不能和JVM交互,大部分时候是GC引起的。在少数情况下,它是由其他情况引起的,例如:Dumpthread、deadlockcheck、heapdump。2、GC时为什么会出现全局暂停?让我们打个比方:就像在聚会上。突然,GC来打扫房间。晚会很乱,又产生了新的垃圾。房间永远不会打扫。只有当每个人都停止活动时,房间才能打扫干净。而且,如果没有全局暂停,会对GC线程造成很大的负担,GC算法的难度也会增加,导致GC很难判断哪些是垃圾。3、Stop-The-World的危害:如果长时间停止服务,HA系统无响应,可能会造成主备倒换,严重危害生产环境。  备注:HA:HighAvailable,高可用集群。比如上面的主机和备机:现在主机在工作,如果主机在GC导致长时间停顿,那么备机会检测到主机没有工作,所以备机开始工作;但是主机不工作只是暂时的,当GC结束后,主机又开始工作了,所以这种情况下,主机和备机是同时工作的。主机和备机同时工作其实是非常危险的。极有可能造成应用程序不一致,无法提供正常服务等,进而影响生产环境。代码示例:(1)打印日志代码:(每100ms打印一条)System.out.println("time:"+t);Thread.sleep(100);}}catch(Exceptione){}}}上面代码中是负责打印log的代码,每100ms打印一条,并计算打印时间。(2)worker线程的代码:(worker线程,专门用来消耗内存的)publicstaticclassMyThreadextendsThread{HashMapmap=newHashMap();@Overridepublicvoidrun(){try{while(true){if(map.size()*512/1024/1024>=450){//如果map的内存消耗大于450,则清理内存System.out.println("=====准备清理=====:"+map.size());map.clear();}for(inti=0;i<1024;i++){map.put(System.nanoTime(),newbyte[512]);}Thread.sleep(1);}}catch(Exceptione){e.printStackTrace();}}}然后,我们设置gc的参数为:-Xmx512M-Xms512M-XX:+UseSerialGC-Xloggc:gc.log-XX:+PrintGCDetails-Xmn1m-XX:PretenureSizeThreshold=50-XX:MaxTenuringThreshold=1打印日志如下:上图中红色字体代表GC。按理来说应该是每100ms打印一条log,但是执行GC的时候会出现grouppause,导致没有按时输出。【声明】欢迎转载,但请保留文章原始出处→_→生活一号:http://www.cnblogs.com/smyhvae/文章来源:http://www.cnblogs.com/smyhvae/p/4744233。html联系方式:smyhvae@163.com