话不多说,先上简图(<JDK1.8):
从图中可以看出JVM的运行时数据区大致可以分为数据和指令两块内容,指令这块本质上也属于数据,不过大部分数据跟指令有关系。右边有3个部分都是线程私有的,计数器存储了当前线程执行的字节码指令的地址,不过这仅限于java方法,如果时native方法那这个计数器时为null的。(查看字节码可以在命令行使用javap -v class文件名),而在虚拟机栈里面,一个java方法对应一个栈帧,每个栈帧存储了该方法的局部变量表和操作数栈以及动态链接等等,因此线程执行的过程相当于栈帧出栈的过程。在方法执行的过程中,数据在栈帧中时如何运转的呢,下面来举个简单的例子:
public void add(int i){ int a=0; int b=1; a = b+i; }
在运行该方法前,局部变量表先把变量i,a,b(位置为0,1,2)存储,然后执行相关的命令时就从变量表移到操作数栈,每次指令到来的时候就从操作数栈弹出数据并运算,再将结果压入操作数栈。具体的字节码如下:
stack=2, locals=3, args_size=1 0: iconst_0 //把整数 0 压入操作数栈 1: istore_1 2: iconst_1 //把整数 1 压入操作数栈 3: istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中 4: iload_2 // 把局部变量表索引为 2 的 slot 中存放的变量值(b)加载至操作数栈 5: iload_0 // 把局部变量表索引为 0 的 slot 中存放的变量值(i)加载至操作数栈 6: iadd //栈顶的两个数出栈后相加,结果入栈 7: istore_1 8: return //有兴趣的同学可以去查找一下相关指令的说明
本地方法栈与虚拟机栈的作用类似。方法区又被称为堆的逻辑部分(JDK1.8移除了它),它主要存储静态变量以及已加载的类信息还有一些编译后的代码等等。堆空间是我们比较关心的部分,也是GC工作的主要部分,它主要存放了实例对象以及数组对象和常量池。
还有一部分内存是直接内存,但是它并不是JVM运行时数据区的一部分,所以它并不在GC收集器的执行范围内。下面我们来追踪这部分内存的创建和回收过程。因为笔者在之前参加过jni的开发工作,所以我是知道创建的语句如下:
ByteBuffer a = ByteBuffer.allocate(1024);
不能直接使用DirectByteBuffer的原因是它所有的构造器都没有前缀,也就是访问权限是default,这里我们来复习一下java的访问修饰符(上三角矩阵):
访问权限 类 包 子类 其他包 public 1 1 1 1 (对任何人都是可用的) protect 1 1 1 0 (继承的类可以访问以及和private一样的权限) default 1 1 0 0 (包访问权限,即在整个包内均可被访问) private 1 0 0 0 (除类型创建者和类型的内部方法之外的任何人都不能访问的元素)
只有该类的对象和同包下的其他类可以访问到。下面是它的构造器,当然它的构造器有多个,原理都差不多:
DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size);// 分配堆外内存(由C的malloc实现),并返回堆外内存的地址 } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //为它构建一个Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收 att = null; }
Cleaner对象的声明如下:
public class Cleaner extends PhantomReference<Object>
因此可以确定在回收堆外内存的时候是使用了虚引用的方式,我们知道虚引用是作用和名字描述一样,一旦GC碰到了就会将其回收并加入ReferenceQueue,通常是用来追踪GC回收的过程的。构建这个Cleaner对象的时候还引入了一个Deallocator对象,它的实现是一个线程Runnable对象,它的run方法如下:
public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); //使用本地方法将该地址指向的内存释放 address = 0; Bits.unreserveMemory(size, capacity);//在系统中保存总分配内存(按页分配)的大小和实际内存的大小。 }
在Cleaner类的方法里面存在clean方法:
if (remove(this)) { try { this.thunk.run(); //这个thunk就是传入的Deallocator对象 } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); }
综上可以得出结论,直接内存是通过本地方法创建,由GC回收实例引用,再由Cleaner的调用clean方法释放内存。下篇将会介绍JDK1.8与JDK1.7的数据区差异以及GC回收算法