java虚拟机之内存划分

java内存区域(运行时数据区)

java 虚拟机在执行java程序的时候会将内存划分为若干个不同的数据区域. 这里特别注意一点, jdk1.8(及以后的版本)和之前的版本略有不同.

概述

jdk 1.8之前:
在这里插入图片描述
jdk1.8
在这里插入图片描述
jdk1.8将方法区移除掉了,元空间成为其替代者.
内存划分的区域中, 线程共享的有两个区域:

  • 方法区

线程私有的有三个区域:

  • 程序计数器(pc寄存器)
  • 虚拟机栈
  • 本地方法栈

程序计数器

程序计数器是一块较小的内存空间, 可以看作是当前线程所执行的字节码的行号指示器.

字节码解释器工作时通过改变这个计数器的值来选取下一个需要执行的字节码指令, 如分支, 循环, 异常处理, 线程恢复等功能都依赖于这个计数器完成

为了线程能恢复到正确的位置, 每一个线程都需要一个独立的程序计数器,各线程之间互不影响,独立存储,这类内存区域就被称为"线程私有"的内存

程序计数器的作用

  • 字节码解释执行时,通过改变程序计数器来依次读取指令, 从而实现程序的流程控制, 例如循环,异常处理等
  • 在多线程的情况下, 程序计数器用来记录当前线程的位置, 所以当线程被切换回来的时候能够知道该线程上次运行到哪里了.

特别注意

程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建随着线程的结束而死亡.

java虚拟机栈

与程序计数器一样, java虚拟机栈也是**线程私有的 .它的生命周期和线程相同, 描述的是java方法执行的内存模型, 每次方法调用的数据都通过栈传递的. **
java的内存大的方向可以分堆(heap)内存和栈(stack)内存, 其中的栈就是指的虚拟机栈,或者说是虚拟机中局部变量表的部分.
实际上, java虚拟机栈是由一个个的栈帧组成的, 而每个栈帧都是由如下的部分构成:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口信息

局部变量表

主要存放了编译器可知的各种数据类型

  • 基本数据类型
  • 对象引用
    • reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用的指针
    • 也可能是指向一个代表对象的句柄或其他与对象相关的位置.

抛出异常

  • StackOverFlowError
    • 若java虚拟机栈的内存大小允许动态扩展,那么当线程请求栈的深度超过当前java虚拟机栈的最大深度时候,就会抛出此异常.
  • OutOfMemoryError
    • 若java虚拟机栈的内存大小允许动态扩展,且当线程请求时内存已经用完了,无法再动态扩展了, 此时抛出OutOfMemoryError

java虚拟机栈也是线程私有的, 每个线程都有各自的虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡.

本地方法栈

和虚拟机栈所发挥的作用非常的相似, 区别是:

  • 虚拟机栈为虚拟机执行java方法(也就是字节码)服务
  • 本地方法栈则是为虚拟机使用到Native方法服务

在HotSpot虚拟机中和java虚拟机栈合二为一

执行过程

本地方法被执行的时候, 在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表,操作数栈,动态链接, 出口信息.

本地方法执行完成之后相应的栈帧也会出栈并释放内存空间.同样,也会出现StackOverFlowError和OutOfMemoryError两种异常

堆(heap)

特点

  • 堆(heap)是虚拟机所管理的内存中最大的一块.
  • 堆(heap)是所有线程共享的一块内存区域, 在虚拟机启动时创建.
  • 堆(heap)唯一的目的就是存放对象实例, 几乎所有的对象实例以及数组都在这里分配内存

垃圾回收

java堆(heap)是垃圾收集器管理的主要区域,因此了被称为**GC堆(Garbage Collected Heap)

从垃圾回收的角度,由于现在的垃圾回收器基本都是采用分代垃圾收集算法,所以java堆还可以细分为:

  • 新生代
  • 老年代

再往下继续划分还可以划分为 Eden空间, From Survivor, To Survivor空间等.进一步划分是为了更好的回收内存, 或者更快地分配内存.
在这里插入图片描述
图中所示的eden, s0, s1区都属于新生代, tentired区属于老年代.

大部分情况下, 对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活, 则会进入到s0区或者s1区,并且对象的年龄还会加1(从Eden 进入到Survivor区后对象的初始年龄变为1), 当它的年龄增加到一定程度(默认为15岁),则会进入到老年代中.
对象进入到老年代的年龄的阈值,可以通过参数:

xx:maxTenuringThreshold // 设置阈值

方法区

方法区与堆(heap)一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息, 常量,静态变量,即时编译后的代码等数据.
虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-heap(非堆),目的应该是与java堆区分开来

特别注意的一点是, 方法区也被称为永久代.

方法区与永久代的关系

「java虚拟机规范」只是规定了有方法区这么个概念及作用,并没有规定如何去实现它.那么,在不同的JVM上方法区的实现肯定是不同的了.

方法区和永久代的关系很像java上接口与类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式.

也就是说, 永久代是HotSpot的概念, 方法区是java虚拟机规范中的定义,是一种规范, 而永久代是一种实现. 一个是标准,一个是实现,其他的虚拟机实现并没有永久代这一说法

常用参数

jdk1.8之前永久代还没有被彻底移除的时候,通常使用如下参数来调节方法区的大小.

-XX:PermSize = N // 方法区(永久代)初始大小
-MaxPermSize = N // 方法区(永久代)最大值. 如果超过这个值则会抛出OutOfMemoryError异常. 
// java.lang.OutOfMemoryError: PermGen

相对而言,垃圾回收行为在这个区域是比较少出现的,但是并非数据进入方法区后就永久存在了.

jdk1.8的时候, 方法区(HotSpot的永久代)被彻底移除了(jdk1.7就开始了), 取而代之的是元空间, 元空间使用的是直接内存, 下边是常用的一些参数:

-XX:MetaspaceSize = N // 设置MetaspaceSize的初始值, 是小值
-XX:MaxMetaspaceSize = N // 设置Metaspace最大值

与永久代不同的是,如果不指定大小的话, 随着更多类的创建,虚拟机会耗尽所有可用的系统内存

为什么将永久代(PermGen)替代为元空间(Metaspace)?

整个永久代有一个jvm本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用的内存的限制,并且永远不会得到java.lang.OutOfMemoryError;

可以使用-XX:MaxMetaspaceSize标志设置最大元空间的大小, 默认值是unlimited, 这意味着它只受系统内存的限制.

-XX​:MetaspaceSize​*调整标志定义元空间的初始大小, 如果未指定此标志, 则Metaspace将根据运行时的应用程序需求动态地重新调整大小.

运行时常量池

运行时常量池也是方法的一部分.class文件中除了有类的版本,字段,方法,接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量符号引用)

既然运行常量池是方法区的一部分, 自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常

jdk1.7及之后的版本的jvm已经将运行时常量池从方法区移了出来.在java堆(heap))中开辟了一块内存区域存放运行时常量池.
在这里插入图片描述

直接内存

直接内存并不是虚拟机运载时数据区域的一部分,也不是虚拟机规范当中定义的内存区域, 但是这部分内存也被频繁的使用.

直接内容也可能导致OutOfMemoryError异常出现.

jdk1.4中新加入了NIO(New input/output)类引入了一种基于通道(Channel)与缓存(Buffer)的I/O方式.它可以直接使用Native函数库直接分配 堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作.这样就能在一些场景中显著提高性能. 因为避免了在java堆(heap)中和Native堆之间来回复制数据.

本机的直接内存不会受到java堆的限制,但是既然是内存就会受到本机总内存大小以及处理器的寻址空间的限制.

### Java虚拟机内存划分详解 Java虚拟机JVM)的内存主要划分为以下几个部分:堆、栈、方法区、程序计数器本地方法栈。以下是对每个部分的功能用途的详细说明。 #### 1. 堆 (Heap) 堆是JVM中最大的一块内存区域,它是线程共享的全局内存,主要用于存储对象实例数组。所有的对象都在堆上分配内存[^3]。 - **特点**:堆是垃圾回收的主要区域,因此也被称为“GC堆”。堆被进一步划分为新生代老年代。 - **作用**: - 新生代(Young Generation):用于存放新创建的对象。 - 老年代(Old Generation):存放经过多次垃圾回收后仍然存活的对象。 - **引用示例**: ```java String str = new String("Hello World"); ``` 上述代码中,`str` 对象会被分配在堆中。 #### 2. 虚拟机栈 (Java Virtual Machine Stacks) 虚拟机栈是线程私有的内存区域,生命周期与线程相同。每个线程运行时都会创建一个虚拟机栈,栈由多个栈帧组成,每个栈帧对应一次方法调用[^1]。 - **特点**:栈的特点是先进后出(LIFO),局部变量存储在此处[^2]。 - **作用**: - 存储方法执行过程中的局部变量、操作数栈、动态链接等信息。 - 每个方法执行时都会创建一个栈帧,方法执行完毕后栈帧弹出。 - **异常**:如果线程请求的栈深度大于虚拟机允许的深度,则会抛出 `StackOverflowError`;如果栈扩展超出限制,则会抛出 `OutOfMemoryError`。 #### 3. 方法区 (Method Area) 方法区是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量以及编译后的代码等数据[^3]。 - **特点**:方法区通常实现为永久代(PermGen)或元空间(Metaspace),具体取决于JVM实现。 - **作用**: - 存储类的结构信息,如字段、方法数据、方法代码等。 - 存储运行时常量池(Runtime Constant Pool),这是方法区的一部分,用于存放编译期生成的各种字面量符号引用。 - **异常**:如果方法区无法满足新的内存分配需求,则会抛出 `OutOfMemoryError`。 #### 4. 程序计数器 (Program Counter Register) 程序计数器是一块较小的内存区域,用于记录当前线程所执行的字节码指令的位置。 - **特点**:每个线程都有独立的程序计数器,它是线程私有的。 - **作用**:指示当前线程正在执行的指令地址。如果线程正在执行的是一个Java方法,则计数器记录的是JVM字节码指令的地址;如果是Native方法,则计数器值为空(Undefined)。 #### 5. 本地方法栈 (Native Method Stacks) 本地方法栈与虚拟机栈类似,但它服务于本地方法(Native Method)。本地方法通常是用其他语言(如C/C++)编写的函数。 - **特点**:与虚拟机栈类似,但专门为本地方法服务。 - **作用**:存储本地方法执行所需的内存。 --- ### 总结 Java虚拟机内存模型主要包括堆、栈、方法区、程序计数器本地方法栈五个部分。各部分的功能如下: - **堆**:存储对象实例数组。 - **虚拟机栈**:存储方法执行过程中的局部变量操作数。 - **方法区**:存储类的结构信息、常量静态变量。 - **程序计数器**:记录当前线程的指令地址。 - **本地方法栈**:为本地方法提供内存支持。 ```java // 示例代码 public class MemoryExample { public static void main(String[] args) { int a = 10; // 局部变量a存储在虚拟机栈中 String str = new String("Hello"); // 字符串对象存储在堆中 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值