与c和c++不同,java的虚拟机拥有自动垃圾回收的机制,使得程序员不必去关注垃圾回收的具体过程,然而或许也正因为如此,一旦出现了java内存泄露或者内存溢出时,排查工作讲变的麻烦,所以,理解jvm还是很有意义的。
一、jvm运行时数据区域:分为方法区,栈(本地方法栈、虚拟机栈),程序计数器,堆。
程序计数器:由于cpu处理线程时采用的是轮询的方式,所以,计数器会帮助cpu完成下一个线程的调度工作,当然每个线程都有一个独立的程序计数器,以便cpu能够找到相应的线程。
栈:jvm中的栈分为虚拟机栈和本地方法栈,在程序运行的过程中,会产生很多局部变量,也就是很多基本类型和对象的引用。当栈的引用深度超过虚拟机允许的深度,就会抛出StackOverflowError,而当虚拟机扩展内存超出事先规定的内存时,会抛出OutOfMemoryError。
堆:所有线程共享的一篇内存区域,保存对象的实例,同样当扩展内存越界时会抛出OutOfMemoryError。
方法区:一般称为非堆,为了把它和堆区分开,主要包括运行时常量池,保存一些静态常量。
二、对象访问
分为两种方式,句柄和直接指针
句柄方式中,引用保存的是句柄池中句柄的内存地址,然后根据句柄地址,再去寻找对象实例的地址,优点是当对象实例的地址发生变化时,只需要更改句柄的地址,而引用的地址不用改变。
直接指针:保存对象实例的内存地址,优点是减少一次寻址过程。
三、垃圾收集器与内存分配策略
GC主要完成三件事情:
1、那些内存需要回收
2、什么时候回收
3、如何回收
我们知道,程序计数器和栈是紧紧跟随线程的生命周期的,在线程的生命周期内,栈中的栈帧也有条不紊的出栈和入栈,所以,这部分的垃圾回收变的自然和有序,基本上在类确定后,这部分的内存回收和分配都是确定的。但是堆却恰恰相反,具有一系列的不确定性,而GC关注的也是这部分内存的分配和回收。
在jdk1.2之后,java将引用分为四种:强引用、软引用、若引用、虚引用四种。
强引用是指程序中普遍存在的类似Object obj = new Object(),这样的,只要强引用存在,那么对象就永远不会被回收
一个对象实例被GC所回收,要经历两个阶段。在判断根路径不可达后,它会被第一次标记并且进行筛选,筛选的条件是此对象有无必要执行finalize方法,如果对象没有复写finalize方法,或者改方法已经被虚拟机调用过,那么就判定是没有必要执行的。如果有必要执行,那么对象会被放入F-Queue的队列中,而finalize方法是对象逃脱回收的最后机会。
package com.struts.jvm; public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; //证明还活着 public void isAlive(){ System.out.println("yes,i am still alive"); } //重生方法 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Exception { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次调用重生方法,重新获得引用,拯救自己 SAVE_HOOK = null; gc(); //但是同样的代码第二次却失效,因为finalize方法已经被调用过,它被回收了 SAVE_HOOK = null; gc(); } private static void gc() throws InterruptedException { System.gc(); Thread.sleep(500); if(SAVE_HOOK != null){ SAVE_HOOK.isAlive(); }else{ System.out.println("no,i am dead:"); } } } //输出为: finalize method executed yes,i am still alive no,i am dead
可见,对象的finalize方法只能被调用一次,如果面临下一次回收,那么它将不会再被调用。