本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

1,081 阅读6分钟

在Java出现之前,像C/C++这样的编译型语言写出来的代码经过编译后,得到的是可直接在某平台(Windows或Linux)上执行的机器码,即machine code,machine code其实就是native code,它直接和操作系统交互。

对于内存,主要分三部分:

1)存储可执行代码(冯·诺依曼的存储程序的思想),即编译后的machine code;

2)用来保存代码执行时用到的局部变量,即stack;

3)代码执行时,动态找操作系统申请(最终要归还给操作系统)的heap;

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

编译型语言写出现的程序,对于Heap的分配和归还都是由程序代码手工维护的。如下图所示,写一段C++代码,GCC编译后就成为了可以在某具体平台上运行的机器码。Native的代码和内存管理主要带来两个问题:一是编译后的代码无法跨平台,毕竟是native的,只能支持被编译平台的操作系统API和指令集。二是堆空间无法自动GC,因为内存管理是手工和操作系统交互,申请与释放的内存的操作交给程序员来做,操作系统并不支持GC。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

Java是一种解释型语言,解释型语言是相对于编译型语言存在的,它的源代码不是直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。Java为了解决以上两个问题,它提出了虚拟机的思想,在原来的"Native Heap"里大作文章。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

JVM的ByteCode在任何平台都是一样的。所以到了某个具体的平台,被特定平台的JVM Runtime解释成本平台的machine code,得到可执行代码,存储到Native Code区,machine code运行起来之后就会用到Native Stack和Native Heap,这种把源代码先翻译成中间代码(即ByteCode)再由解释器解释成机器码供运行的模式,就实现了“Write Once,Run Anywhere”。这就解决了代码无法跨平台的问题。

因为Native Heap中相当一部分内存是供Java应用程序存储对象实例的,完全由JVM管理,就可以对JVM管理的Heap里的数据的引用关系做记录,然后用GC来自动释放内存,这就解决了上面提到的堆空间无法自动GC的问题。所以一个Java进程启动时,JVM向操作系统要的内存(-Xms与-Xmx),和程序向JVM要的内存是两件不同的事情了。JVM Heap的内部结构与用什么GC算法有关,比如对于传统分代就是由Eden(包括S0与S1)、Tenured和PermGen组成。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

被JVM管理的内存可以总体划分为两部分:Heap Memory和Native Memory。前者我们比较熟悉,其实就是被分成新生代老年代等的JVM heap,是供Java应用程序使用的;后者也称为C-Heap,是供JVM自身进程使用的。Native Memory没有相应的参数来控制大小,其大小依赖于操作系统进程的最大值(对于32位系统就是3~4G,各种系统的实现并不一样)。

Native Memory里存储了什么呢,主要是

  • JNI调用,也就是Native Stack;
  • JIT(即使编译器)编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
  • NIO direct buffer。对于IBM JVM和Hotspot,都可以通过-XX:MaxDirectMemorySize来设置nio直接缓冲区的最大值。默认是64M。超过这个时,会按照32M自动增大。
  • 用于保存类加载器和类信息的MetaSpace,在Native Memory中的。

本地方法栈就是Native Stack,与Java虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。Navtive方法是Java通过JNI直接调用本地C/C++库,可以认为是Native方法相当于C/C++暴露给Java的一个接口,Java通过调用这个接口从而调用到C/C++方法。

JVM也是在不断发展的,永久代(PermGen区)是对JVM规范中方法区的实现,JDK 7的永久代就在JVM Heap中,与新生代老年代一起构成了JVM Heap。在HotSpot JVM中,永久代(PermGen区)用于存放类和方法的元数据以及常量池,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代(PermGen区)中。永久代(PermGen区)是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即
java.lang.OutOfMemoryError: PermGen,由于PermGen内存经常会溢出,因此JVM的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的OOM。于是JDK 8开始把类的元数据放到本地堆内存(native heap)中,这一块区域就叫Metaspace,中文名叫元空间。之前永久代的类的元数据存储在新的元空间,原永久代的静态变量以及运行时常量池则转移到了JVM Heap中。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

Metaspace空间的分配具有和JVM Heap相同的地址空间,使用本地内存有什么好处呢?最直接的表现就是OOM问题将不复存在,本地内存剩余多少理论上Metaspace就可以有多大(容量取决于是32位或是64位操作系统的可用虚拟内存大小),这解决了空间不足的问题。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

如果从线程执行的角度,大概可以这么理解。每个线程从JVM ByteCode开始执行,记录JVM Stack和PC Register,并被解释成Native Code,在Native Stack真正执行。这些线程共享一个JVM Heap,所以访问共享数据时才需要加锁保证安全。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1打破了原有的分代模型,将堆划分为一个个区域。G1将堆分成许多相同大小的区域单元,每个单元称为Region,Region是一块地址连续的内存空间。每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色。其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。

本地方法栈、JVM栈、本地内存和JVM Heap的区别与关系

这么划分的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,尽可能地缩短垃圾回收时的暂停时间。