001Java基础之JVM内存模型

一 概述

对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

二 运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同

<JDK1.8的内存模型
[外链图片转存失败,源站可能有防盗在这里插入!链机制,建描述]议将图片上https://传(imbg.csynimg.cn/2027jodK10506235603162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5MTY0MjM0,size_16,color_FFFFFF,t_70)1)]
这是jdk1.8之前的内存模型,其中方法区和堆是是线程共享的,但是在jdk1.8之后
元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存

在这里插入图片描述

三 内存对象

程序计数器(Program Counter Register):

它是一块较小的内存空间,可以看做是指向当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖计数器来完成()。

为什么程序计数器为线程私有呢?

由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此,为了线程切换后能恢复到正常的执行位置,每条线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储,为线程私有的内存。

程序计数器值的问题

如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空(Undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

Java虚拟机栈(Java Virtual Machine Stacks)

java虚拟机栈与程序计数器一样,也是线程私有的,他的生命周期和线程保持一致。他是存储当前线程运行方法时所需要的数据、指令、返回地址。在每个方法执行时,虚拟机栈都会创建一个栈帧(Stack Frame),用于存储:局部变量表、操作数栈、动态链接、方法出口等信息。如下图:
在这里插入图片描述

其中局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同与对象本身,可能是一个指向对象其实地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 和returnAddress类型(指向了一条字节码指令的地址)

局部变量表的存储空间是32位,刚好可以放一个int类型,所以长度为64为的long和double类型的数据会占用2个局部变量空间(Slot),局部变量表的大小在编译器就已经确定了

在java虚拟机规范中,对java虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈(Native Method Stack)

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

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

堆(Heap)

java堆是java虚拟机管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,堆的唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配内存。

 java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们磁盘空间一样。(不过在实现中既可以大小固定,也可以是可扩展,通过-Xmx 和-Xms控制),如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

方法区(Method Area)

方法区和堆一样,是各个线程共享的区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据等
ps:方法区中还包括运行时常量池(Runtime Constant Pool),Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Tabel),用于存放编译期生成的各种字面量常量和符号引用,这部分内容将在类加载后进入方法区的运行时常量中存放,当常量池无法再申请到内存时也会抛出OutOfMemoryError异常

元数据区

元数据区取代了1.7版本的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

JDK1.8在本地内存中新增元数据空间。运行时常量池仍然在堆中。元数据区存放类加载信息。

JDK8中为什么移除方法区?
(1)字符串存在永久代中,容易出现性能问题和内存溢出
(2)永久代大小不容易确定. PermSize指定了大小容易造成OOM(内存用完)
(3)给 GC(垃圾回收机制) 带来不必要的复杂度,且回收效率低

直接内存

直接内存并不是java虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。JDK 1.4引入了NIO,他可以使用Native函数库直接分配堆外内存。

(1)常见于NIO操作时,用于数据缓冲区

(2)分配回收成本较高(属于操作系统内存),但读写性能高

(3)不受JVM内存回收管理(依旧存在内存溢出的问题)

jvm的内存分配与回收是自动的,不需要手动调用任何的方法,但是直接内存需要我们手动调用方法去回收,具体就是用System.gc() 回收掉byteBuffer对象.

四 常见问题

堆和栈的区别

1)堆和栈功能上的区别

以栈帧的方式存储方法调用的过程,并存储方法调用过程中
基本数据类型的变

量(int、short、long、byte、float、double、boolean、char等)以及对象的引

用变量,其内存分配在栈上,
变量出了作用域就会自动释放;

而堆内存用来
存储Java中的对象。无论是成员变量,局部变量,还是类变量,

它们指向的对象都存储在堆内存中;

2)堆和栈在线程共享和线程私有区别

栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成
线程的私有内存。

堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

3)空间大小

栈的内存要远远小于堆内存,栈的深度是有限制的,如果递归没有及时跳出,很可能发生StackOverFlowError问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值