运行时的数据区(Runtime Data Area)
==========================
本文主要看 JVM 内存模型主要指运行时的数据区,包括 5 个部分,如下图所示。
虚拟机栈:也叫方法栈,是线程私有的,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。
本地方法栈:与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行 Java 方法使用栈,而执行 native 方法使用本地方法栈。
程序计数器:保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空(Undefined)。
栈、本地方法栈、程序计数器这三个部分都是线程独占的。
堆:是 JVM 管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出 OOM 异常。根据对象存活的周期不同,JVM 把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。
方法区:也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK 1.7 中的永久代和 JDK 1.8 中的 Metaspace 都是方法区的一种实现。
注意:面试回答这个问题时,要答出两个要点:一个是各部分的功能,另一个是哪些线程共享,哪些独占。
下面咱们详细来了解这五个部分。
1. 虚拟机栈(Stack)
===============
虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常状况:
-
StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
-
OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。
在我们学习 Java 虚拟机的的过程当中,经常会看到一句话:
JVM 基于栈,而 Dalvik(DVM) 基于寄存器。
这里的"基于栈"指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,下面咱们来看看这个栈帧?
栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。
我们可以这样理解:一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等。
局部变量表
局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。
注意:系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值),也就是说不存在类变量那样的准备阶段。
操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。
同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中。栈中的元素可以是任意Java数据类型,包括long和double。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈(比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中)。
动态链接
动态链接的主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。
Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接(Dynamic Linking)。
返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:
-
正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出,没有抛出任何异常。
-
异常退出:指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
无论当前方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。
一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
2. 本地方法栈(Native Method Stack)
==============================
本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。
3. 程序计数器(Program Counter Register)
===================================
Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。
"程序计数器"是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。
实际上一些我们熟悉的恢复线程操作、分支操作、循环操作、跳转、异常处理等都需要依赖这个计数器来完成。
关于程序计数器还有几点需要格外注意:
-
在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。
-
线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
-
当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。
4.堆(Heap)
=========
Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫作"GC 堆"。
同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。
按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)和老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。具体如下图所示:
- 堆空间=新生代(1/3)+老年代(2/3)
- 新生代= Eden(8/10)+from(1/10)+to(1/10)
图中不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,进而提高垃圾回收效率。
Ecen区
大多数情况下,对象会在新生代 Eden区中进行分配,当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC,Minor GC 相比 Major GC更频繁,回收速度也更快。
通过Minor GC之后,Eden 会被清空,Eden区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到Survivor 的 From区(若 From区不够,则直接进入Old 区)。
Survivor区
Survivor区相当于是 Eden区和 Old区的一个缓冲,类似于我们交通灯中的黄灯。
难道这样就够了吗?不,远远不够!
提前多熟悉阿里往年的面试题肯定是对面试有很大的帮助的,但是作为技术性职业,手里有实打实的技术才是你面对面试官最有用的利器,这是从内在散发出来的自信。
备战阿里时我花的最多的时间就是在学习技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记
我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。
今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!
技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记
我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。
今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!
[外链图片转存中…(img-5lwZgrsn-1714311869851)]
[外链图片转存中…(img-vaJqR2fq-1714311869852)]
[外链图片转存中…(img-pfKV6jsl-1714311869853)]