JVM~内存模型
注意:JDK1.8之前(不含1.8)和之后有些区别
-
线程私有:
- 程序计数器
- 本地方法栈
- 虚拟机栈
-
线程共享
- 堆
- 方法区(运行时常量池,在JDK1.7及之后时已被迁移到堆中)
- 直接内存(非运行时数据区的一部分)
1、程序计数器
1.1、功能描述
记录当前指定程序的所执行的字节码的行号指示器。
-
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
-
在多线程的情况下,为了线程切换后能恢复到正确的执行位置,
每条线程都需要有一个独立的程序计数器
,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 -
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2、Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
-
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是
虚拟机栈中局部变量表
部分。 (实际上,Java虚拟机栈是由一个个栈帧
组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。) -
局部变量表
主要存放了编译器可知的各种数据类型
(boolean、byte、char、short、int、float、long、double)、对象引用
(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) -
Java 虚拟机栈会出现两种异常:
StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
- OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
2.1、方法如何调用?
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。
-
Java方法有两种返回方式:
- return 语句。
- 抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
2.2、栈帧
- 概念:栈帧是一种数据结构,用于虚拟机进行方法的调用和执行。
- 包含数据:局部变量表、操作数栈、动态链接、方法返回地址、附加信息。
- 大小确定:编译程序代码的时候,就已经确定了局部变量表和操作数栈的大小,而且在方法表的Code属性中写好了。不会受到运行期数据的影响。
(他人博客图)
3、本地方法栈
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈
则为虚拟机使用到的 Native 方法服务。
- 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
- 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
4、堆
Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
-
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap),从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存
- 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0(From Survivor) 或者 s1(To Survivor),并且对象的年龄还会加 1(
Eden区->Survivor 区后对象的初始年龄变为1
),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
来设置。
- 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0(From Survivor) 或者 s1(To Survivor),并且对象的年龄还会加 1(
5、方法区(HotSpot 永久代)
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有一个别名叫做 Non-Heap(非堆)。
5.1、方法区和永久代的关系
方法区也称为永久代
《Java虚拟机规范》只是规定了有方法区这个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
5.2、JDK1.8前后 永久代的区别
- 1.8之前,可以使用以下参数来调节方法区的大小
-XX:PermSize=N //方法区(永久代)初始大小
-XX:MaxPermSize=N //方法区(永久代)最大大小,超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
- 1.8之后(含1.8),方法区(HotSpot的永久代)被彻底移除了(JDK1.7就已经开始了),取而代之是
元空间(MetaSpace)
,元空间使用的是直接内存,定义了新的参数来调节方法区的大小。
-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小
元空间(MetaSpace)与永久代(PermGen)很大的不同在于:如果不指定大小的话(默认为unlimited),随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
5.3、将永久代(PermGen)替换为元空间(MetaSpace)的原由?
- 整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。当采用默认元空间大小时,Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
- 另一个原因是为了融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
6、运行时常量池
在JDK1.7之前的版本,运行时常量池属于方法区,在1.7之后,已经移除并迁移到堆中。
7、直接内存
-
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
-
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。