栈与堆
HotSpot 在虚拟机栈和本地方法栈的实现上,直接将二者合二为一,也就是说使用同一个栈来支持 Java 方法和本地方法的执行,下文以 Java 栈代称。
并且在 HotSpot 的实现中 Java 栈是不支持动态扩展的,也就是说 Java 栈通常只会抛出 SOF(StackOverflowError)异常,除非在启动线程申请内存时就因无法获取足够的内存而出现 OOM(OutOfMemoryError)异常。
上一篇说过“几乎”所有的对象实例都在堆里分配内存,那么为什么说是“几乎”呢?
刨除逻辑上归属于方法区的静态变量不谈,HotSopt 虚拟机中存在 JIT 即时编译,在即时编译的过程中,会进行逃逸分析,当发现一个局部对象并没有逃逸到方法和线程之外,那么这个对象就可能不在堆上分配内存,而是在栈上分配内存。
方法区的不同版本实现
方法区是 JVM 规范中定义的一个概念,不同的厂商在实现虚拟机的时候有不同的落地实现。
即使在 HotSpot 虚拟机中,不同的版本也有不同的实现方式,在 JDK6 的时候方法区的落地实现是永久代(PermGen)。
图片
在 JDK7 的时候方法区的落地实现仍是永久代,但是发生了一些变化,JDK7 将存储在永久代的字符串常量池、静态变量迁出,存储到了堆区。
图片
在 JDK8 的时候 HotSpot 虚拟机完全舍弃了永久代的落地实现,改用元空间落地实现。并且 JDK8 将元空间从虚拟机运行时数据区迁到了本地内存中。
图片
个人理解,JDK8 之前方法区采用永久代实现,因为永久代有 -XX:MaxPermSize 上限,并且这个参数即使不设置也会有默认值,所以容易发生 OOM 异常。
于是 JDK7 就将永久代中的字符串常量池、静态变量迁出,但 OOM 问题处理可能仍未达到预期,最终在 JDK8 采用在本地内存中实现的元空间作为方法区的落地实现。
在这个过程中,-XX:PermSize 和 --XX:MaxPermSize JVM 参数也随之失效,改为通过--XX: MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置元空间参数。
本地内存和直接内存
最后,我们来介绍一下本地内存和直接内存。个人理解,截止 JDK8,Java 程序内存应该是包含 JVM 内存和本地内存,本地内存狭义上又包含元空间和直接内存(二者存储在同一块区域,只是作用上不一致)。
本地内存
本地内存并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。
这块区域直接受本机物理内存限制,当申请的内存超过了本机物理内存,才会抛出 OOM 异常。
直接内存
直接内存也是受本机物理内存限制,在 JDK4 中引入了基于通道与缓冲区的 NIO,它可以利用 Native 函数库直接分配堆外内存,然后通过堆内的 DirectByteBuffer 对象引用这块堆外内存。
避免了传输的数据在堆和堆外来回复制,显著的提高了 IO 性能。这块堆外内存就是直接内存。