详解Java内存管理中的JVM垃圾回收
一.概述
相比起c和c++的自己回收内存,java要方便得多,因为jvm会为我们自动分配内存以及回收内存。
在之前的jvm 之内存管理 中,我们介绍了jvm内存管理的几个区域,其中程序计数器以及虚拟机栈是线程私有的,随线程而灭,故而它是不用考虑垃圾回收的,因为线程结束其内存空间即释放。
而java堆和方法区则不一样,java堆和方法区时存放的是对象的实例信息以及对象的其他信息,这部分是垃圾回收的主要地点。
二.java堆垃圾回收
垃圾回收主要考虑的问题有两个:一个是效率问题,一个是空间碎片问题。
而java堆中的垃圾回收可以分为两个区域,一个是新生代,一个是老年代。其中新生代又分为一块比较大的eden空间和两块较小的survivor空间。因为新生代和老年代所存储的对象群体是不一样的,为了在效率和空间碎片问题中取得平衡,新生代和老年代所使用的垃圾回收算法是不一样。
新生代 -复制算法
从名字上就知道,新生代主要存放的是比较新的对象,回收多次之后仍然存活的对象,就会被送到老年代中区。由此可知新生代的垃圾回收是比较频繁的,所以为解决效率问题,新生代使用了复制算法。复制算法可以将内存分为大小相等的两块,每次分配时使用其中一块,当这一块用完时,就将还存活的对象复制到另一块内存上面区。此时已使用过的这一块内存就可以一次清理掉,这样也不用担心内存碎片的问题。当然这种算法的一个缺点就是内存使用率比较低,只有一半(每次只能一半用来分配出去)。
而ibm公司的研究表明,新生代中的对象98%都是”照生夕死“,所以不需要按照1:1划分,故而会将内存分为一块较大的eden空间和两块小的survivor空间。
那么为什么会有两块survivor呢,复制算法不是只需要一块eden和一块survivor就够了吗?
其实这主要还是为了解决碎片化的问题。假设只有一个survivor区,当eden区满的时候,进行gc,存活对象被分配到了survivor区,清空eden区。当再一次gc完成后,存活的对象继续放在survivor区,这样不是很美好吗,不会有内存碎片啊!但是别忘了,第一次存到survivor区的对象很可能在第二次gc的时候就失活了,清理掉survivor失活对象不就会产生内存碎片了吗?
所以java堆使用了两个survivor区,一个from survivro和一个tosurvivor,第一次eden满的时候,复制算法将存活对象放到from survivor区,清空eden。第二次,eden满时,将eden和from survivor区存活的对象放到to survivor区,清空eden和from survivor,然后重要的一步,将from survivor和to survivor角色互换!这样就解决了内存碎片化的问题。
老年代 -标记/整理算法
首先要明白老年代存放的都是会存活得比较久的对象,所以如果老年代也使用复制算法的话,那么复制对象的开销时比较大的,因为老年代的对象基本上都会存活。
标记/整理算法很好理解,主要也就是”标记“,”整理“两个步骤,先将要回收的对象标记,然后让存活对象向着一端移动,最后将边界以外的内存,然后gc完成。
三.方法区垃圾回收
在某些地方的解释中,方法区也会被叫做“永久代”,与java堆不同,这里存放的是类的信息以及一些常量信息,故而这个区域中被分配的内存一般比较难以被回收,所以才有有”永久代“之名。
虽然方法区中垃圾回收效率较低,但被分配的内存却也并非真的就永不被回收,其主要回收的有两部分内容:废弃常量和无用的类。废弃常量的回收与java堆中类实例回收类似,当常量池中一个常量没有被引用时,就有可能被回收。比如常量池中有一个字符串常量“abc”,当没有任何一个string对象值为"abc"时,那么下一次垃圾回收"abc"常量就有可能会被回收。
而对于无用的类的回收,首先需要判断什么样的类才是”无用的类“:
- 该类所有的实例都已被回收,即java堆中没有该类的实例。
- 加载类的classloader已经被回收。
- 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可能会堆满足这三个条件的”无用的类“进行回收,仅仅是可能,并非必然。
上一篇: Java内部类原理与用法实例总结
下一篇: java实现高效的枚举元素集合示例