前言
从实践中看,Golang(以下简称Go)应用程序比Java占用更少的内存,这与它们的运行时环境有关,其运行时自带了内存动态分配和自动垃圾回收的管理机制,本文通过分析Go与Java在内存管理机制上的差异,以期对两者在运行时内存方面有更进一步的认识。本文以Go(1.12)和当前使用较多的JDK8 HotSpot VM为例进行说明。
本篇文章包含以下内容:
介绍Go与Java的运行时内存结构差异
介绍Go与Java的内存资源占用差异
介绍Go与Java如何为对象分配内存
介绍Go与Java的内存回收策略差异
内存结构差异
应用程序要能在linux系统上运行(其他平台类似),其可执行文件要求符合ELF规范(Executable and Linkable Format,可执行和可链接格式)。操作系统加载目标可执行文件到内存中并以独立进程方式运行程序。
操作系统为每个进程分配一个连续的虚拟内存地址空间,并将该进程内存空间划分成多个不同用途的逻辑区域。JVM进程以及Go进程的内存结构如下图所示:
图1
Java用户程序是运行在Java虚拟机(以下简称“JVM”)之上的,从上图我们看到用户程序的字节码指令并没有存储到JVM进程的堆空间或者text段中。实际上,虚拟机将用户程序字节码放在了使用本地直接内存实现的方法区中,并不占用虚拟机的堆内存。
JVM的堆空间保存Java用户程序运行时创建的对象和字符串常量池等数据,栈空间则为Java线程提供了私有的内存区域。在HotSpot VM的实现中,Java线程栈使用操作系统栈和线程模型表示,且Java方法与本地方法共享同一个栈区。因此虚拟机栈与本地方法栈其实是同一个区域。
Go与Java有较大不同,Go进程空间的text段不但保存了内置的运行时机器指令,而且还有用户程序的机器指令(Go在编译时就已确定)。堆内存区则为用户程序创建对象提供了存储空间。Go天然支持并发编程模型,采用了系统线程与用户线程(goroutine)相结合的实现机制,进程栈空间为系统线程提供了栈内存,而用户线程栈的内存默认从堆中分配。
Java内存结构
- 运行时内存
Java程序的运行时内存被划分为元数据区(方法区)、堆、虚拟机栈、本地方法栈、程序计数器5个部分,这可以看作是JVM对进程可用堆、栈空间进行的二次分配,以满足运行Java用户程序的内存需求。其内存结构如下图所示:
图2
上图中堆内存对应了图1中JVM的堆内存,虚拟机栈、本地方法栈、程序计数器则对应了图1中JVM的栈区,元数据区则是JVM另外开辟的内存块。
· 元数据区是JVM向操作系统申请的堆外内存,用于实现“方法区”,主要存储虚拟机加载class的类信息、JIT编译的代码、运行时常量池等数据,其默认大小由系统的可用物理内存上限限制。
· 堆内存是被所有线程共享的一块内存区域,在虚拟机启动时创建。它存放了包括几乎所有的Java对象以及数组,这也是垃圾收集器关注的主要内存区。由于逃逸分析等优化技术,对象也有可能被分配到栈上。
· 虚拟机栈即我们常说的栈空间,其生命周期与线程相同。线程的栈空间存储了方法调用的栈帧,每个栈帧则存储局部变量表、