这一周来比较空闲,读了《深入理解java虚拟机一书》以提高自己对java底层的认知,还没看完,只是挑选了书中自己比较感兴趣的两个章节来看,写下此篇博客一是为了总结,二是为了方便今后回顾。下面是第一部分自动内存管理机制
运行时数据区域
程序计数器
程序计数器时一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程直接的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
java虚拟机栈
与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期与线程相同。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackoverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。。本地方法栈也会抛出StackoverflowError和OutOfMemoryError异常。
java堆
对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。java堆是所以线程共享的一块内存区域,在虚拟机启动时创建。java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆无法再扩展时,将会抛出OutOfMemoryError异常。
方法区
方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。垃圾回收行为在这个区域是比较少出现的,这个区域内存回收目标主要是针对常量池的回收和对类型的卸载。。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
运行时常量池时方法区的一部分。Class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区运行时常量池中。运行期间也可能将新的常量放入池中,如String类的intern()方法。
确定对象是否存活的算法
垃圾回收器在对堆进行回收前,第一件事情就是要确定这些对象有哪些还存活着,哪些已经死去。
引用计数算法
给对象中添加一个引用计算器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。。
java语言没有选用引用计数法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。如下:
ReferenceCountGC objA = new ReferenceCountGC();
ReferenceCountGC objB = new ReferenceCountGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
根搜索算法
在主流的商用程序语言中,都是使用根搜索算法判断对象是否存活的。基本思路是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象待GC Roots没有任何引用链相连,则证明此对象是不可用的。
Java语言里,可作为GC Roots对象包括下面几种:
- 虚拟机栈中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI的引用对象
java中的四种引用
强引用:代码至中普遍存在。类似 Object obj = new Object()。主要强引用还在,垃圾回收器永远不会回收掉被引用的对象。
软引用:当内存不够时,即系统将要发生内存溢出异常之前,将会把这些对象列进回收范围并进行二次回收。java中提供SoftReference类实现软引用。
弱引用:被弱引用关联的对象只能生存到下次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。java中提供WeakReference类实现软引用。
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望这个对象在被收集器回收时收到一个系统通知。java中提供PhantomReference类实现软引用。
对象死亡过程
在跟搜索算法中不可达的对象也并非是非死不可的。这些不可达的对象先会被判断是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机会将这两种情况都视为“没有必要执行”。
finalize()方法是对象逃脱死亡命运的最后一次机会,如果对象想要在finalize()方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。譬如把自己(this)赋值给某个类变量或者某个对象的成员变量。如下:
public
如何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再次被执行。
垃圾回收算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它的主要缺点有两个:
一是效率问题,标记和清楚过程效率都不高
二是空间问题,标记清除后会产生大量不连续的内存碎片。
复制算法
它将可用内存按容量划分为大小相同的两块,每次只使用其中的一块,当一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这种算法的代价是将内存缩小为原来的一半未免太高了一些。
现在的商用虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分空间。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性拷贝到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1。当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。
标记-整理算法
复制算法在对象存活率较高时需要执行较多的复制操作,更关键是如果不想浪费50%空间,就需要额外的空间进行担保,以应对内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。
标记-整理算法的标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
根据对象的存活周期不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时发现大批对象死去,只有少量存活,那就选用复制算法。老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”,“标记-整理”算法来进行回收。
垃圾收集器
如果两个收集器之间存在连线,就说明他们可以搭配使用。下面就只说下Serial收集器:
Serial收集器
Serial收集器是最基本,历史最悠久的收集器,这是一个单线程的收集器。它在进行垃圾收集时,必须暂停其他所有工作线程指到它收集结束。