Java的内存区域划分

Java的内存区域划分
Mr.Man1. 概述
Java和C++的一个关键区别就是对内存的控制,C++需要在编码时直接管理内存,需要开发者对内存的知识了如指掌。而Java却将管理内存的权利交给了Java虚拟机(JVM),使得开发人员不需要执行分配/释放内存等操作,提升了开发效率。但是一旦出现问题,很容易让人摸不清头脑,因此,只有掌握了JVM如何管理内存,处理问题才能如鱼得水,这篇文章从最基本的讲起,讲一讲JVM都包含哪些区域。
注:本篇文章为个人学习所用,可能存在不严谨或出错的地方,还请谅解。
2. 运行时区域划分
2.1 整体架构图
本文以JDK1.8版本为例,展开讲解。
2.2 程序计数器
程序计数器所占用的内存空间很小,可以将他看做线程执行字节码的行号指示器,表示当前执行的具体位置,程序执行过程中,通过修改程序计数器的值来指定下一条需要执行的字节码指令,因此程序的顺序执行、循环、跳转、异常执行等功能都是依赖程序计数器完成的。
同时,程序在多线程的情况下,需要在多个线程之间轮流切换,为了实现线程切换回来后还能从原来的位置继续执行,每个线程都会有一个独立的程序计数器,不同线程之间互不影响,因此程序计数器是“线程私有”的。
2.3 Java虚拟机栈
Java虚拟机栈表示Java方法的执行过程,一个方法被执行,会创建一个对应的栈帧被压入栈中,栈帧存储方法执行过程中的信息,包括局部变量表、操作数栈、动态链接、方法返回地址。当方法执行完成后,对应的栈帧就会被执行出栈操作,一个方法从调用到执行完毕,必然伴随着一个栈帧入栈到出栈的过程。
Java虚拟机栈和结构和数据结构中的栈相似,先进后出的思想,只有入栈和出栈两种操作。
2.4 本地方法栈
本地方法栈和Java虚拟机栈类似,区别只是虚拟机栈为Java方法服务,本地方法栈是为本地方法服务。
2.5 堆
堆是虚拟机管理区域中占用内存最大的一块,堆的核心概念为:Java中“几乎”所有对象都是存在堆中的,由堆来给对象实例分配内存。需要注意的是,这里强调的是“几乎”,因为随着即时编译技术的发展,栈上分配等手段导致出现了新的情况。
堆的唯一作用就是存放对象实例,也就导致堆成为了垃圾回收(GC)的内存区域,因此堆也被称为“GC堆”。
在一些资料中,会将堆做细致的区域划分,出现了“新生代”,“老年代”等概念,其实这种区域划分并不是虚拟机实现的具体布局,而是从垃圾分代回收的角度考虑,这种划分是一部分垃圾收集器的共同特性或设计风格。无论如何,堆的唯一作用就是存放对象实例,将堆做区域划分,唯一的目的是为了更好的回收内存,或更快的分配内存。
2.6 方法区
方法区也是线程共享的,但方法区只是《Java虚拟机规范》规定的一个概念,至于如何实现方法区,不同虚拟机有自己的方式。
方法区的作用是存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。JDK8之前,HotSpot虚拟机使用永久代来实现方法区,在永久代同样采用收集器的分代设计,这样就可以像管理堆一样管理方法区。但是这种方式会导致更容易造成内存溢出的问题,因为永久代是存在上限的,由-XX:MaxPermSize
参数控制。到了JDK8,完全废弃了永久代这个概念,改为了元空间实现方法区。
元空间直接占用本地内存,不再受JVM参数的限制,一方面降低了内存溢出的风险,另一方面,元空间的大小由系统的实际空间控制,这样可以加载的类就更多了。
2.7 运行时常量池
运行时常量池是方法区的一部分,Class文件中除了类的版本、字段、方法等描述信息外,还有一项信息是常量池表,用来存储编译器间产生的各种字面量和符号引用,这部分信息会在类加载完成后,存储到运行时常量池中。
2.8 直接内存
JDK1.4 中新加入的 NIO(Non-Blocking I/O,也被称为 New I/O),引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
直接内存并不是虚拟机运行时数据区的一部分,它分配在本地内存上。他的核心作用是由Java堆中的对象直接操作堆外内存,不需要将其复制到Java堆中,提高了性能。直接内存不回收Java堆大小的限制,会收到本地内存的影响,也有可能出现OOM的情况。
3. 参考
- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第三版)》
- https://javaguide.cn/java/jvm/memory-area.html