浅谈JAVA内存管理与垃圾收集
当我们谈到JAVA的内存管理,常常只把内存分为堆和栈,这是因为这两个区域是最重要的两个地方,也是程序员们最关注的。事实上JAVA内存的区分要比这复杂得多。
内存管理
在JAVA程序的运行时数据区中,我们按线程共享与线程隔离来分类:
线程共享
- 方法区(人们常称它为永久代):存储已经被虚拟机加载的类信息,运行时常量池,静态变量,即时编译器编译后的代码等数据。这个区域的内存回收目标主要是废弃常量和无用的类。
- 堆:存储对象实例。Java heap是JAVA虚拟机所管理的内存中最大的一块,也是垃圾收集器管理的主要区域。(Java虚拟机规范中指出,堆仅要求放在逻辑上连续的内存中,不一定是物理上的连续内存。)
线程隔离
- 程序计数器:我们知道Java源码经编译后会变成Java虚拟机能看明白的字节码,程序计数器就是当前线程所执行的字节码的行号指示器。每条线程都有各自独立的程序计数器。
- 虚拟机栈:(我们常说的栈实际上就是这里的虚拟机栈)每个方法在执行时都会创建一个栈帧,用于存放局部变量表(在编译一个方法期间,局部变量表需要占用多大内存是确定的),操作数,动态链接,方法出口等信息。
- 本地方法栈:与虚拟机栈相似,只不过虚拟机栈为Java虚拟机中的字节码(即Java方法)服务,而本地方法栈为Native方法服务。(一个Native方法即一个Java调用非Java代码的接口。)
堆与方法区的垃圾收集
上文说到,Java虚拟机管理的内存分为线程隔离和线程共享的两类,其中线程隔离的程序计数器、虚拟机栈和本地方法栈都随线程(方法)而生,随线程(方法)而死,这里不再赘述。下面主要讨论堆和方法区的垃圾收集。
-
堆:
-
我们知道堆中存放的是对象实例。所以首先第一步就是:如何找到对象?
通常我们访问对象,都是通过本地方法栈中reference数据来操作具体的。目前主流的对象的访问定位方式有两种:句柄和直接指针。(对象实例数据在堆中,对象类型数据在方法区中)
句柄:堆中需要专门划分出一块内存作为句柄池,池中存放着到对象类型数据的指针(方法区)和到对象实例数据的指针。reference仅指向堆中的 指向对象实例数据 的句柄,该句柄再指向该对象的实例数据。这样做的好处就是JAVA本地变量表中的reference存储的是稳定的句柄地址
直接指针:reference中存储的直接就是对象地址。速度快捷,节省了一次指针定位的时间开销 -
如何判断对象已死?
2.1 引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1;当引用失效时,计数器-1。计数器为零时说明说明对象不再被使用。很难解决对象之间相互循环引用的问题2.2 可达性分析算法
通过一系列的称为“GC ROOTS”的对象作为起始点,从这些节点开始往下搜索,走过的路径称为引用链,当一个对象没有与任何引用链相连,证明该对象无用。
可作为GC ROOTS的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的变量
- 本地方法栈中Native方法引用的变量
-
-
方法区
永久代的垃圾回收主要为:废弃常量、无用的类。- 如何判断常量已废弃?
没有任何对象引用该常量,其他地方也没有引用该常量,有必要的话,该常量就会被系统清理出常量池。 - 如何判断类已无用?
2.1 该类的所有实例都已经被回收
也就是说Java堆中不存在该类的任何实例。
2.2 加载该类的ClassLoader已经被回收
2.3 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 如何判断常量已废弃?
垃圾收集算法
- 标记-清除算法(一般老年代使用):首先标记出所有需要回收的对象,标记完成后统一回收。缺点:标记和清理的效率都不高;标记清除后会产生大量内存碎片
- 复制算法(一般新生代使用):将可用内存分成大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后再把已经使用过的内存空间一次清理掉。
- 标记-整理算法(一般老年代使用):标记出所有需要回收的对象后,让所有存活着的对象向一端移动,然后直接清理掉端边界以外的内存。
未完待续