之前看过JVM的相关知识,当时没有留下任何学习成果物,有些遗憾。这次重新复习了下,并通过博客来做下笔记(只能记录一部分,因为写博客真的很花时间),也给其他同行一些知识分享。
Java自动内存管理机制包含两部分:内存分配和内存回收,要想理解内存分配和回收的机制,则需要了解下Java内存区域(Java运行时数据区),这篇随笔将按照下面的线索进行逐步解析:
Java运行时数据区
对象“已死”的判定算法
垃圾收集算法
垃圾收集器
结束语
好,接下来我们一一来看。
一、Java运行时数据区
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括如下几个运行时数据区域
程序计数器:用来记录当前线程所执行的字节码指令的行号指示器。字节码计时器需要通过改变改值来选取下一条需要执行的字节码指定,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个指示器来完成。程序计数器是唯一一个没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型,每个方法执行时都会创建一个栈帧用来存储局部变量表(存放编译器可知的各种基本数据类型、对象引用和returnAddress类型,所需的内存空间在编译器完成分配)、操作数栈、动态链接、方法出口等信息。Java虚拟机栈有两种异常情况:OutOfMemoryError(扩展时无法申请到足够内存)和StackOverflowError(线程请求的栈深度大于虚拟机所允许的深度)。
本地方法栈:同Java虚拟机栈类似,只不过Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用Native方法服务。HotSpot直接将两个栈合二为一。也规定了两种异常:OutOfMemoryError和StackOverflowError。
堆:JVM所管理的内存中最大的一块,也是GC管理的主要区域。理论上所有的对象实例和数组都要在堆上分配。堆的大小是可以扩展的,通过-Xms和-Xms控制,并且堆无法扩展的时候就会报OutOfMemoryError异常。
方法区:用来存储JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是为了和堆区分开来,它也叫Non-Heap(非堆)。方法区无法满足内存分配需求时,报OutOfMemoryError异常。
直接内存:并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是却被经常使用。JDK1.4中新加入的NIO类,引入了基于通道和缓冲区的I/O方式,他可以直接分配对外内存,以提高性能。不收堆大小的限制,但是会受物理内存的约束。也会报OutOfMemoryError异常。
附栈到堆的关联例子(基于HotSpot):
二、对象“已死”的判定算法
由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分。
在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。
有两种算法可以判定对象是否存活:
)引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。
)可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的。
三、垃圾收集算法
1、标记-清除算法
最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。