Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
具体分为以下几个部分:
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是 当前线程所执行的字节码的行号指示器 。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈描述的是Java方法执行的线程内存模型:
局部变量表内容:编译期可知的各种Java虚拟机的:
局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽。(由虚拟机决定大小)
内存分配时机:局部变量表所需的内存空间在 编译期间 完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
本地方法栈与虚拟机栈所发挥的作用是非常相似的。其区别是:
StackOverflowError和 OutOfMemoryError异常 :本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆是 被所有线程共享 的一块内存区域,在虚拟机启动时创建。
根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
Java堆既可以被实现成固定大小的,也可以是可扩展的。
OutOfMemoryError异常:如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出。
方法区是 各个线程共享 的内存区域。
在JDK 8以前,Java程序员都习惯在HotSpot虚拟机上开发、部署程序,所以更愿意把 方法区 称呼为 永久代 。
本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队选择 把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已 ,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。
在JDK 8中,完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的 元空间 来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table)。
OutOfMemoryError异常:在方法区无法满足新的内存分配需求时
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后 通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用 进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
OutOfMemoryError异常:一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
当HashMap、LinkedList等等这些容器为静态的。
总结: 长生命周期 的对象持有 短生命周期对象 的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
例如:数据库连接、网络连接和IO连接等。
在对数据库进行操作的过程中,首先需要建立与数据库的连接。当不再使用时,需要调用close方法来释放与数据库的连接。
一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。
如果一个 外部类的实例 对象的方法返回了一个 内部类的实例对象 ,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用, 但由于内部类持有外部类的实例对象 ,这个 外部类对象将不会被垃圾回收 ,这也会造成内存泄露。
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
建议使用 final 类型的类(String、Integer)。
这种情况看下面程序。
import java.util.Arrays; public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } } 复制代码
内存泄漏的另一个常见来源是缓存,一旦把对象引用放入到缓存中就很容易遗忘。
此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
内存泄漏常见来源是 监听器和其他回调 ,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。
本篇文章参考自:《深入理解Java虚拟机(第3版)》和部分其他博客。