Java内存区域
Java内存区域
一、 介绍
Java在运行在JVM中会有一些区域,这些区域是负责在不同的内存中管理数据的,所以被称为运行时的数据区域。一般分为以下几个区域:

这些区域有的是一直存在,有的是随着用户线程启动和结束而建立和销毁。
首先先总览概况一些每个区域的作用:
方法区 : 各个线程之间共享的区域,存储一些已经加载的类型信息、常量、静态变量等。在Java 8及以后的版本中,方法区的概念被更改为元空间(Metaspace),其大小可以根据需要动态调整。
堆:在JVM管理中是最大的一块区域,目的就是存放对象实例和垃圾回收,JVM中用于存储所有对象实例和数组的内存区域。当程序创建一个对象或数组时,这些数据都会被分配到堆中。
虚拟机栈:是线程私有1的,存储每个线程的执行状态,包括方法调用、局部变量、操作数栈和方法返回地址(主要针对Java的字节码)
本地方法栈:和虚拟机栈非常类似,但是是为本地Native方法来服务的。
程序计数器:与绝大多数语言的计数器是一样的,是字节码的行号治时期,通过改变计数器的值来来选取吓一跳需要执行的字节码指令2
二、堆
在JVM内存管理模型中,堆是一个很重要的内存管理模块,主要是垃圾收集器管理的内存区域,所以也叫GC堆。
虽然从回收分代的角度来看,我们又将堆的空间分为了几个区域像:Eden空间、From Survivor空间、To Survivor空间。还有为了缓存划分了代码缓冲堆:编辑即时的JIT本地代码。但是在GC机制不断的发展下,我们在这里其实也出现了不是分代回收的空间划分。这一段具体的GC机制会开一个专栏说一下。
堆是一个很重要的概念,不仅仅存储了所有的对象和数组,而且还提高内存分配和垃圾回收的效率。在物理上不要求连续但是在逻辑上是需要进行连续存储的,有点类似与我们的磁盘空间。
可能导致的问题:如果申请堆的空间不够了(没有实例分配了),会抛出OutOfMemoryError异常。
虽然Java中JVM帮助我们操作了很多东西,但是我们也可以使用一些代码或者配置来监控或者影响堆内存空间使用。例如
1 | java -Xms512m -Xmx1024m -XX:+UseG1GC MyApp |
在JVM开发优化中,也可以使用Unsafe类来操作,包括直接内存分配和操作。不过它绕过了Java的安全机制,官方不建议,除非用于JVM性能开发。
我们也可以使用**直接内存(Direct Memory)**来处理定义的内存区域。直接内存用于在Java程序和本地(非Java)系统之间高效地进行数据交换。直接内存并不是由JVM管理的,而是通过Java的NIO(New Input/Output)库来分配和释放的
一个简单的例子
1 | import java.nio.ByteBuffer; |
运行给定的Java代码会输出一串从0到255的字节数据
三、方法区
方法区是在JVM内存管理模型中又一个非常重要的概念,因为方法区存储了已经被虚拟机加载的类型信息、常量、静态变量和代码缓存等。
在JDK8以前,程序员都把这个方法区称为“永久代”,因为Hotpot虚拟机用永久代来实现方法区,因为永久代的空间是固定的,很容易导致OutOfMemoryError异常,所以JDK8舍弃了永久代的说法,取而代之的是元空间来实现方法区。元空间使用本地内存,能够动态调整大小,更加灵活和高效。在《Java虚拟机规范》中,其实说到方法区就是一个堆的逻辑部分,但是又不是堆,也成为非堆(Non-Heap)。
方法区基本不实现垃圾回收,但是需要实现一些对于常量池的回收和类型的卸载。但是这一块也是最容易出Bug的,就是因为它不实现垃圾回收,导致的无法满足新的内存分配的请求
方法区还维护了一个常量池,称为运行时常量池,这个常量池有类的版本、字段、方法、接口等描述信息,还有一个常量池表,存放一些编译器产生的符号引用和字面量。这里有一个特性:动态性,就是说在编译期产生的常量和运行期间产生的常量都可以自放入常量池中。
四、Java虚拟机栈与本地方法栈
在虚拟机栈中,每个方法的创建都会同时创建一个栈帧,栈帧有局部变量表、操作数栈、动态连接、方法出口信息。每个方法开始与结束都对应了每一个栈帧在虚拟机栈中入栈到出栈的过程。
在虚拟机栈中,其实说的最多的是局部变量表,这个局部变量表维护的是基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向一条字节码指令的地址)。
局部变量表的存储空间是以局部变量槽(Slot)表示,long和double(64位)占两个变量槽,其余的占一个。每个变量槽的具体多大空间实现是由JVM来决定的
本地方法栈主要是为本地(Native)服务的,在有的JVM中是和虚拟机栈合并在一起的。
五、程序计数器
非常小的一块内存空间、对于Java来说,从编译到生产所需的文件,会经历一系列的指令,这些指令需要有一个先后次序,就是字节码解释器来通过改变计数器的值来选取下一条需要执行字节码指令。就像汇编语言,分支、循环、跳转、异常处理、恢复都是依赖于计数器。而且在Java中,一个内核只会执行一条线程中的指令,为了线程切换后能恢复到正确的位置,每一个线程都需要有一个独立的程序计数器,也就是线程私有的。
注意:在执行本地方法(native)的时候,计数器的值是空的,在程序计数器中是没有OutOfMemoryError异常