史上最实用的 JVM 教程 - 02 - Jvm 内存区域

内存结构

Java 中内存的管理是通过 JVM 来自动管理的. 了解 JVM 有利于进一步理解 Java.

JVM 内存结构如下图所示:
这里写图片描述

在 Java 虚拟机规范里,JVM 被分为 7 个内存区域。

虚拟机规范中的 7 个内存区域分别是三个线程私有的和四个线程共享的内存区。

三个私有区:线程私有的内存区域与线程具有相同的生命周期,分别是:指令计数器(程序计数器)、线程栈(虚拟机栈)和本地方法栈
在线程创建时创建, 在线程结束时被回收. 所以不存在垃圾回收问题.

四个共享区:四个共享区是所有线程共享的,在 JVM 启动时就会分配,分别是:方法区、 常量池、直接内存区和堆.

三个线程私有: 程序计数器 线程栈(虚拟机栈)(基本类型变量, 引用类型变量, 执行 Java 方法) 本地方法栈(执行本地方法)
四个共享区 : 方法区(存放class的结构) 常量池(方法区的一部分) 堆(存储Java对象) 直接内存区

程序计数器 / PC 寄存器

程序计数器可以理解为当前线程执行的字节码的行号指示器,字节码解释器就是通过改变这个值来获取需要执行的下一条需要执行的字节码指令。对于多线程来说,每条线程都有自己的程序计数器,这样各线程之间的计数器互不影响,之所以这么设计,是因为在多线程的情况下,完全可能出现线程中断的情况,那么当被中断的线程需要回复执行的时候,怎么知道上次该线程执行到哪里了呢?这就需要程序计数器发挥作用了,由于每个线程都有自己的程序计数器,这样当CPU重新调度该线程的时候,从其计数器中取出下一条的字节码执行指令,于是就可以继续执行了。

有多少线程在编译时是不确定的,因此该区域也没有办法在编译时分配,只能在创建线程时分配, 所以说该区域是线程私有的, 该区域只是指令的计数, 占用的空间非常少, 此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域.

如果线程正在执行的是一个 Java 方法,PC 值为正在执行的虚拟机字节码指令的地址.

当执行本地方法时,指令计数器置为 undefined.

线程栈 / 虚拟机栈 / Java 栈

Java 栈总是和线程联系在一起, 每当创建一个线程时, JVM 就会为这个线程创建一个对应的 Java 栈, 在这个 Java 栈中又包含有多个栈帧, 这些栈帧是与每个 Java 的方法关联起来的, 每运行一个方法就创建一个栈帧, 每个栈帧会保存一个方法的局部变量, 操作数栈和方法返回值等信息.

基本数据类型, 引用类型, 实例方法都是在栈内存中分配的.

虚拟机栈是线程私有的,会抛出 StackOverFlowError (当出现死循环递归调用时) 和 OOM.

抛出 StackOverFlowError 的例子 :

public class Test {
     public static void main(String[] args) {
           sysHello();
     }
     private static void sysHello() {
           sysHello();
     }
}

每当一个方法执行完成时,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并清除这个栈帧,Java 栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧又被创建,这个新创建的栈帧又被放到Java栈的顶部,变为当前的活动栈帧。同样现在只有这个栈帧的本地变量才能被使用,当在这个栈帧中所有指令执行完成时这个栈帧移出Java栈,刚才的那个栈帧又变为活动栈帧,前面的栈帧的返回值又变为这个栈帧的操作栈中的一个操作数。如果前面的栈帧没有返回值,那么当前的栈帧的操作栈的操作数没有变化.
由于Java 栈是与Java线程对应起来的,这个数据不是线程共享的,所以我们不用关心它的数据一致性问题,也不会存在同步锁的问题.

Java 堆 Heap

Java 堆 是存储 Java 对象的地方, 它是 JVM 管理 Java 对象的核心存储区域.

Java 堆在虚拟机启动的时候创建,是Java 虚拟机所管理的内存中最大的一块. 此内存区域的唯一目的就是存放对象的实例. 几乎所有的对象实例都在这里分配内存,例如对象实例和数组.

Java 堆是所有线程共享的.

Java 堆是垃圾收集器管理的主要区域,由于现在的收集器基本都采用分代收集算法,所以Java堆中还可以细分为新生代和老年代。

Java 堆将会抛出 OutOfMemoryError 异常.

堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。
新生代 ( Young ) 又被划分为三个区域:Eden, From Survivor, To Survivor.
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收.

堆的内存模型大致如下图所示:

这里写图片描述

从图中可以看出: 堆大小 = 新生代 + 老年代.

默认情况下, 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2     Eden : from : to = 8 : 1 : 1

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务, 所以无论什么时候, 总是有一块 Survivor 区域是空闲着的.

因此, 新生代实际可用的内存空间为 9/10 ( 即90% ) 的新生代空间.

这里写图片描述

新创建的对象会在 新生区的 Eden Space 诞生, 当 Eden 区内存空间不足时, 发生 Minor GC 垃圾回收时, 幸存下来的对象将会移动到 From Survivor 区, 当 From Survivor 区内存不足时, 发生 GC 垃圾回收, 幸存下来的对象将会移动到 To Survivor 区. 依次类推. 当老年代内存也不足时, 将发生 Full GC.

方法区 Method Area

方法区是用来存放 JVM 装载的 class 的类结构信息的地方, 包括:类信息、常量, 类的方法、静态变量、类型信息(接口/父类),我们使用反射技术时,所需的信息就是从方法去里获取的。

方法区也属于 Java 堆的一部分, 也就是通常说的 Java 堆中的永久区, 这个区域被所有线程共享.

一般来说这个区域的内存回收目标是针对常量池的回收和对类型的卸载。

从 JVM 运行时区域内存模型来看,堆和方法区是两块独立的内存块. ( Java7 时, 方法区移动到了堆中 ). 但从垃圾收集器来看,HotSpot 虚拟机选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区,所以很多人都更愿意把方法区称为“永久代”.

运行时常量池

运行时常量池: Runtime Constant Pool 代表运行时每个 class 文件中的常量表. 包括: 编译期的数字常量, 方法或者域的引用.

运行时常量池是方法区的一部分, 所以会抛出 OutOfMemoryError.

变量用 final 修饰,不能再修改它的值,所以就成为常量,而这常量将会存放在常量区,这些常量在编译时就知道占用空间的大小,但并不是说明该区域编译就固定了,运行期也可以修改常量池的大小,典型的场景是在使用 String 时,你可以调用 String 的 intern(),JVM 会判断当前所创建的 String 对象是否在常量池中,若有,则从常量区取,否则把该字符放入常量池并返回,这时就会修改常量池的大小(常量的大小是不可改变的).

本地方法栈 Native Method Stack

地方法栈与虚拟机栈的作用是非常相似的,它们之间的区别:
不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务.

本地方法栈也会抛出 StackOverFlowError 和 OutOfMemoryError.

该区域主要是给调用本地方法的线程分配的,该区域和线程栈(虚拟机栈)的最大区别就是,在该线程的申请的内存不受GC管理,需要调用者自己管理,JDK中的Math类的大部分方法都是本地方法,一个值得注意的问题是, 在执行本地方法时,并不是运行字节码,所以之前所说的指令计数器是没法记录下一条字节码指令的,当执行本地方法时,指令计数器置为 undefined.

直接内存区

此处的直接内存并不是由 JVM 管理的内存。他是利用本地方法库直接在 java 堆之外申请的内存区域。比如 NIO 中的 DirectByteBuffer 就是操作直接内存的。

直接内存的好处就是避免了在 java 堆和 native 堆直接同步数据的步骤。但是他并不是由 JVM 来管理的。

关注这个公众号, 快速掌握技术

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值