读书笔记《深入理解java虚拟机》——运行时内存区域

238 阅读5分钟

运行时内存区域


java与c++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙内的人却想出来。(附上虚拟机规范官方文档,英文好的可以点击参考)

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在虚拟机启动时创建的,仅在虚拟机退出时销毁。其他数据区域是每个线程各占一部分,这种线程私有区域与线程的生死同步创建销毁。

虚拟机在执行java程序的过程中把管理的内存分为如下若干个数据区域。

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • java堆
  • 方法区
  • 运行时常量池
  • 直接内存

每个区域都有各自的用途,创建时间和销毁时间。


程序计数器

程序计数器占用的内存很小,它用来记录当前线程所执行的字节码的行号。每个处理器内核在任何一个时都会执行一条线程中的指令。为了线程切换后恢复到正确的执行位置,每个线程都需要一个程序计数器,这类内存也叫线程私有的内存。当执行的是native方法时。

线程执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。当执行的是native方式时,这个计数器的值则为空。

虚拟机栈

虚拟机栈就是java程序员口中常说的那个栈,它是线程私有的内存,每个线程都会一个虚拟机栈,它描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储该方法中的局部变量表,操作数栈,动态链接,方法出口等信息。(栈帧内部数据以后细讲)

每一个方法在被调用时,就对应着一个栈帧在虚拟机中入栈和出栈的过程。你肯定见过java的抛异常时的堆栈信息如下(篇幅限制,做了缩减),每一行是一个方法,也代表一个栈帧。

java.io.FileNotFoundException: File doesn't exist: hdfs://[my cluster]/ 
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:356)
at org.apache.hadoop.hbase.ipc.RpcServer.call(RpcServer.java:2080)
at org.apache.hadoop.hbase.ipc.CallRunner.run(CallRunner.java:108)
at org.apache.hadoop.hbase.ipc.RpcExecutor.consumerLoop(RpcExecutor.java:114
at org.apache.hadoop.hbase.ipc.RpcExecutor$1.run(RpcExecutor.java:94)
at java.lang.Thread.run(Thread.java:745)

在虚拟机规范中,这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;在虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈的作用是相同的,只不过本地方法栈是为虚拟机执行native方法服务的。虚拟机规范中并没有对它做出方法使用语言,使用方法和数据结构做强制规定,sun HotSpot虚拟机将本地方法栈和虚拟机方法栈合二为一了。(Sun HotSpot虚拟机就是大家常用的那个)

java堆

堆是虚拟机管理的最大的内存,它被所有线程共享,并在虚拟机启动的时候创建。堆的唯一作用就是存放对象实例。

堆是垃圾收集器管理的主要区域,也被称为GC堆。从GC的角度来看,由于现在GC基本采用分代收集,所以java堆可以细分为:新生代和老年代。再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的堆中可能划分出多个线程私有的分配缓冲区。

java虚拟机规范中规定java堆可以处于物理上的不连续内存空间中,因此可以实现成固定大小的,也可以是可扩展的。可通过-Xmx和-Xms控制。当堆中没有内存可为实例分配时,并且也不能再扩展时将抛出OutOfMemoryError异常。

方法区

线程共享的内存区域,存放已被虚拟机加载的类信息,常量,静态变量,编译后的代码等数据。在HotSpot虚拟机中GC分代收集扩展到方法区,它不代表永久代。这个区域的内存回收主要是针对常量池的回收和类的卸载。当方法区无法满足内存分配时抛出OutOfMemoryError。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了类的版本,字段,方法,接口等描述信息外,还有一项是常量池(class文件中的重要组成部分,以后详细讲,可以理解成字典的形式),用于存放编译后生成的字面量和符号引用,这部分内容就存放在此处。

这片内存具有动态性,运行期间也可以放入新的常量,比如String.intern()方法。调用该方法时,如果运行时常量池已经包含等于此字符串对象的字符串时(由equals方法确定),则返回池中的字符串。 否则,将此String对象添加到池中,并返回对此String对象的引用。

直接内存

直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但也会被频繁使用,也会抛出OOM。比如NIO类,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用来操作,避免在java堆和直接内存中来回复制数据。

这块内存不受堆大小的限制,在配置虚拟机参数时,会根据实际设置-Xmx等参数,但经常忽略直接内存,从而导致内存区域综总和大于物理内存。