JVM内存模型详解

JDK、JRE、JVM三者的区别

JDK(Java Development Kit)就是java 开发工具,从图中可以看到,JDK包含JRE,Java基础类库和Java工具,这是给开发者使用的产品。

JRE(Java Runtime Environment)Java运行时环境,在运行java程序代码时,提供Java运行的所有环境集合,包括核心类库,JVM实现等,在安装jdk时,安装目录下会有一个jre的目录,jre目录下有一个lib目录和一个bin目录,lib目录中存放的是Java运行是所需要的核心类库,bin中的就是JVM的实现。

JVM是包含在Jre中的bin目录下,他里面为我们封装了一些命令,比如:javac,是Java语言运行的虚拟机,提供了编译器,执行器等等。市面上最常用的JVM是HotSpot。

Java号称一款跨平台的语言,一次编译到处运行,原因就是JVM在软件层面帮我们封装了不同操作系统的指令,可以让Java开发者无需关注底层操作系统实现。

换句话说,你只需要按照Java的语法开发你的程序就行了,至于怎么在操作系统上运行起来,怎么与操作系统交互等等交给JVM去做就行了。

JVM内存模型

 

JVM内存模型主要包含线程私有程序计数器java虚拟机栈本地方法栈线程共享堆空间元数据区直接内存。 

Java 虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着 Java 进程产生和消失。

根据 JVM 规范,JVM 运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分。

程序计数器(PC 寄存器、计数器)

程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。

在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。

可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

倘若执行的是 native 方法,则程序计数器中为空

Java 虚拟机栈(JVM Stacks)

虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。

不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

每个栈帧主要包含的内容如下:

局部变量表

存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用

注意:这里的基本数据类型指的是方法内的局部变量

局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。

操作数栈

动态连接

方法返回地址

虚拟机栈可能会抛出两种异常:
栈溢出(StackOverFlowError):

若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常

内存溢出(OutOfMemoryError):

若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常

本地方法栈(Native Method Stacks)

本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。

本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。

本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

虚拟机栈和本地方法栈的主要区别:

虚拟机栈执行的是 java 方法
本地方法栈执行的是 native 方法

Java 堆(Java Heap)

Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。

Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。

Java 堆的分区:

在 jdk1.8 之前,分为新生代、老年代、永久代

在 jdk1.8 及之后,只分为新生代、老年代

永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代

Java 堆内存大小:

堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)
既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)
如果堆无法扩展或者无法分配内存时报 OOM

主要存储的内容是:

对象实例

类初始化生成的对象

基本数据类型的数组也是对象实例

字符串常量池

字符串常量池原本存放在方法区,jdk8 开始放置于堆中

字符串常量池存储的是 string 对象的直接引用,而不是直接存放的对象,是一张 string table

静态变量

static 修饰的静态变量,jdk8 时从方法区迁移至堆中

线程分配缓冲区(Thread Local Allocation Buffer)

线程私有,但是不影响 java 堆的共性

增加线程分配缓冲区是为了提升对象分配时的效率

堆和栈的区别:

管理方式,堆需要GC,栈自动释放
大小不同,堆比栈大
碎片相关:栈产生的碎片远小于堆,因为GC不是实时的
分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配
效率:栈的效率比堆高

方法区(1.7) 

方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)

方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区

各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。

Java8 以前是放在 JVM 内存中的,由堆空间中的永久代实现,受 JVM 内存大小参数限制
Java8 移除了永久代和方法区,引入了元空间

元空间(1.8)

元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。

元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM

元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

类元信息(Class)

类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表

常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

运行时常量池(Runtime Constant Pool)

运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些

运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值