读书笔记之二:堆、方法区、运行时常量池和直接内存
一、Java 堆
Java 堆(Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这个内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但随着 JIT 编译器的发展与逃逸技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些变化发生,所有的对象并不都是一定分配在堆上。
为了更好的回收内存和分配内存,从内存回收的角度来看,Java 堆还可以细分新生代和老年代,对于新生代和老年代虚拟机一般都采用不同的垃圾回收算法;从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 –Xmx 和 –Xms 控制)。
如果在堆中没有完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
二、方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存 区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆)。
方法区与永久代:对于习惯在 HotSpot 虚拟机上开发、部署程序的人来说,一般称方法区为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为 HotSpot 虚拟机的设计团队选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样 HotSpot 的垃圾收集器可以像管理 Java 堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(如 BEA JRockit、IBM J9等)来说是不存在永久代的概念的。原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但是使用永久代来实现方法区,这样更容易遇到内存溢出问题(永久代有 -XX:MaxPermSize 的上限,J9 和 JRockit 只要没有触碰到进程可用内存的上限,例如 32 位系统中的 4GB,就不会出现问题),而且有极少数方法(例如 String.intern())会因为这个原因导致不同虚拟机下有不同的表现。因此,在 JDK 1.8 中,HotSpot 虚拟机放弃永久代,采用元空间来实现方法区了。
异常状况:根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
三、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
常量池的动态性:运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产出,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被利用的比较多的就是 String 类的 intern() 方法。
异常状况:当常量池中无法再申请到内存时会抛出 OutOfMemoryError 异常。
四、直接内存
直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO(New Input / Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存分配不会受到 Java 堆大小的限制,但是,会受到本机总内存大小及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 –Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。