@frank-shaw
2015-11-07T10:09:36.000000Z
字数 4444
阅读 2233
java.内存
JVM的内存知识点可以参考:
https://www.zybuluo.com/mdeditor#149352
在主流的语言实现中,对象是否存活的判断标准是基于可达性分析算法的。这个算法的基本思想就是通过一系列的称为"GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达的。下图的对象5 6 7就会因此被标记为可回收对象。
在Java中,可以作为GC Roots的对象包含以下几种:
程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生、随着线程而灭;
栈中的栈帧随着方法的进入和退出而有条不紊地执行着入栈和出栈的操作。每一个栈帧中分配多少内存基本上在类结构定下来的时候就已知,因此这几个区域的内存分配和回收具有确定性,不需要过多考虑垃圾回收的问题。因为方法结束或者线程结束时,内存自然就随着回收了。
剩下的可能的部分就是Java堆和方法区了。我们来详细分析一下:
Java堆自然是我们进行垃圾回收的大户人家了,我们会接下来慢慢介绍。那么方法区里面又有些什么东西呢?
方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。
废弃常量的工作比较简单:以常量池中的字面量回收为例,如果没有其他地方引用了这个字面量,那么该字面量就非常有可能会被回收。判断一个常量是否是废弃常量是不是很简单呢?
但是判断一个类是否是无用的类就比较麻烦了,需要同时满足三个条件:
1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收。
3.该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述三个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不适用了就必然会被回收。
标记清除算法是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:
第一阶段,标记所有的可访问对象.第一阶段叫做标记阶段.
第二阶段,垃圾收集算法扫描堆并回收所有的未标记对象.第二阶段叫做收集阶段.
在使用标记清除算法时,未引用对象并不会被立即回收.取而代之的做法是,垃圾对象将一直累计到内存耗尽为止.当内存耗尽时,程序将会被挂起(Stop The World?),垃圾回收开始执行.当所有的未引用对象被清理完毕时,程序才会继续执行.
优点:
因为标记清除式的垃圾回收跟踪了由根(root)访问的所有对象,所以即使是在有循环引用时,它也可以正确地标记并执行垃圾回收工作。标记清除的第二个优点是,对于引用对象的常规操作不会产生任何的额外开销。
缺点:
效率问题:标记和清除过程效率不高;
空间问题:标记清除之后会产生大量不连续的内存碎片。
看下面这个图的话就能够更快记住啦~~
为了解决效率问题,一种称为"复制"的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次使用其中一块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。由于是对整个半区进行内存回收,内存分配的时候不需要考虑内存碎片等复杂情况。移动堆顶的指针就可以了,按顺序分配内存即可,实现起来很简单,运行高效。
只是这样子的代价不是太大了么,一半的内存都不用了。。。土豪真有钱~~!
此处判断是否为可回收的对象的标准和标记-清除法的标准是一样的--可达性分析算法。应该说,java虚拟机当中用于判断对象是否存活的标准都是这个可达性分析算法。
记得哦,垃圾回收机制中的年轻代回收算法就是这个复制算法哦,只不过啊,它比较醒目,不是使用对半分的方法,内存这么昂贵,哪能如此挥霍?具体如何实现,详细请看后面讲解吧!
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在Minor GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。Eden区在每一次GC之后都是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
如果在对象存活率较高的情况下,使用复制收集算法需要进行较多的复制操作,效率较低。那么这种情况下(存活率较高,指的是老年代),会使用另外一种称之为标记-整理算法来回收对象。
标记的过程和标记清除算法的过程是一样的。但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
哈哈,太好玩啦!~
上面讲到的算法都只是方法论,具体的实现就是如今的各式各样的垃圾收集器。不同把女恩的虚拟机会有不同的垃圾收集器。这里只讲基于HotSpot虚拟机的几个垃圾收集器。
上图中的上半部分表示的是适用于新生代的垃圾收集器,下半部分表示的是适用于老年代的垃圾收集器。上下之间的连线表示这两个垃圾收集器可以配合使用。值得注意的是G1这家伙,特别吊,它一个垃圾收集器就把新生代老年代的活全揽了。
事先声明,没有放之四海皆好用的垃圾收集器。只有根据实际情况来使用不同的垃圾收集器,才能够发挥更好的作用。
老家伙了。JDK1.3之前只有它,其他小家伙都还没有出生呢。
一看名字就知道,哟,单线程呢。注意,它这个单线程的意义不仅仅说明它只能使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是,它在垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束。"Stop The World"!!!屌不屌~~
但是,你这么吊,都"Stop The World"了,让使用计算机的用户怎么办呐?计算机运行半小时你就暂停响应5分钟,还让不让人玩啦?
虚拟机的设计者表示完全理解,但是也很委屈:"你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间里待着去,如果她一边打扫,你一边乱扔纸屑,这房间还能够打扫完么?"
看起来这个Serial收集器效果很不好,但是实际上不是的。到现在为止,他依然是虚拟机运行在Client模式下的默认新生代收集器。它也有着由于其他收集器的地方:简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
实际上就是Serial收集器的多线程版本。它是运行在Server模式下的虚拟机中首选的新生代收集器。
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,同时也是并行的多线程收集器。看上去和ParNew一样,有什么特别的呢?
它关注的点和其他收集器不同,更加关注的是如何尽可能地缩短垃圾收集时用户线程的停顿时间,达到一个可控制的吞吐量。
吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)
它也被称为是吞吐量优先的收集器。还可以通过调整参数执行自适应调节策略以达到最合适的停顿时间或者最大的吞吐量。这也是它和ParNew收集器的重要区别。
Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。
Parallel Old是Parallel Scavenge的老年版本,使用多线程和标记整理算法。
Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间作为目标的收集器。
从名字就可以看出,CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器而言要更加复杂一些。
优点:并发收集、低停顿
缺点:对CPU资源非常敏感。无法处理浮动垃圾。基于标记-清除算法会导致大量空间碎片产生。
特点:
并发并行:充分利用多核优势 并行(垃圾收集线程和其他Java程序一起运行)
分代收集:依然分代收集,针对两个代会有不同的策略
空间整合:不会产生内存空间碎片
可预测的停顿:建立了可预测的停顿时间模型,这个真心牛逼啊
之前的收集器手机的范围是整个新生代或者老年代,而G1不再是这样子。他将整个Java堆划分为多个大小相等的独立区域,虽然还是保留着新生代和老年代的概念,但他们都是一部分独立区域的集合了。--化整为零。