Java虚拟机把内存划分为若干个不同的数据区,如下图所示:
接下来将会详细介绍这些分区。
程序计数器 是一块较小的内存空间, 它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。
为了线程切换后能恢复到正确的执行位置,保证上下文切换的正确性, 每条线程都需要有一个独立的程序计数器, 各条线程之间计数器互不影响, 独立存储, 我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是Native方法, 这个计数器值则为空( Undefined) 。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
它的生命周期与线程相同。
每个Java方法在执行的同时都会创建一个栈帧用于存局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用到执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了
它与虚拟机栈非常相似的, 区别:
此内存区域的唯一目的就是存放对象实例。
Java堆是垃圾收集器管理的主要区域,Java堆中还可以细分为: 新生代和老年代; 再细致一点的有Eden空间、 From Survivor空间、 To Survivor空间等。
Java Stack内存用于执行线程。
每当调用方法时,都会在磁盘存储中创建一个新块,以容纳该方法的本地原始值并引用该方法中的其他对象。
存放基本类型的变量,对象的引用和方法调用,遵循先入后出的原则。
栈的优势是,栈内存与堆内存相比是非常小的,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。栈有一个很重要的特殊性,就是存在栈中的数据可以共享。
方法区存储已被虚拟机加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。永久代就存放在方法区。
垃圾收集行为在这个区域是比较少出现的, 但并非数据进入了方法区就如永久代的名字一样“永久”存在了。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
运行时常量池( Runtime Constant Pool) 是方法区的一部分。 Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外, 还有一项信息是常量池( Constant Pool Table) , 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放。
主要分两大块,一块是被线程所共享的,另外一块是线程所独享的区域。
其中,在堆内存内部,又会分为两块区域,也就是我们经常听到的,叫做新生代和老年代,新生代又会被进行再划分,划分成一般是三个或者四个区域:
在后续的垃圾收集算法我会继续提到上述概念。
垃圾收集GC需要完成的三件事情:
在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
大多数情况下, 对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时, 虚拟机将发起一次 GC。
GC 主要处理的是对象的回收,什么时候会触发一个对象的回收的呢?
如何决定哪些对象是垃圾?涉及到以下算法:
在JDK1.2之前,使用的是引用计数器算法。通过引用计数来判断一个对象是否可以被回收。如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被用到,那么这个对象就可被回收了。
这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,如下面所示:
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = <