3

Android 字节跳动技术二面:程序运行时,内存到底是如何进行分配的

 2 years ago
source link: https://blog.csdn.net/m0_63737792/article/details/121259276
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

关于程序计数器还有几点需要格外注意:

  • 在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。

  • 线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

  • 当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

二、虚拟机栈

虚拟机栈也是线程私有的,与线程的生命周期同步。

在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

  1. StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。

  2. OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。

JVM 是基于栈(就是指虚拟机栈)的解释器执行的,DVM 是基于寄存器解释器执行的。

虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧。

1. 栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。

我们可以这样理解:一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等。如下图所示:

  • 局部变量表

局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。

使用 javap -v 反编译

注意:系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值),也就是说不存在类变量那样的准备阶段。

  • 操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。

同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的 Code 属性表中的 max_stacks 数据项中。栈中的元素可以是任意Java数据类型,包括 long 和 double。

  • 动态链接
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

浏览器打开:qq.cn.hn/FTe 免费领取

态链接的主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。

在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。

  • 返回地址

无论当前方法采用正常退出或异常退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。

一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

三、本地方法栈

本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。

Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫作“GC 堆”。同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。

按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。具体如下图所示:

五、方法区

方法区(Method Area)主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

注意:关于方法区,很多开发者会将其跟“永久区”混淆。

  • 方法区是规范层面的东西,规定了这一个区域要存放哪些数据。

  • 永久区或者是 metaspace 是对方法区的不同实现,是实现层面的东西。

六、异常再现

  • StackOverflowError 栈溢出异常

递归调用是造成 StackOverflowError 的一个常见场景,原因就是每调用一次method方法时,都会在虚拟机栈中创建出一个栈帧。因为是递归调用,method方法并不会退出,也不会将栈帧销毁,所以必然会导致StackOverflowError

  • OutOfMemoryError 内存溢出异常

理论上,虚拟机栈、堆、方法区都有发生 OutOfMemoryError 的可能。但是实际项目中,大多发生于堆当中。

在一个无限循环中,动态的向 ArrayList 中添加新的对象。这会不断的占用堆中的内存,当堆内存不够时,必然会产生 OutOfMemoryError,也就是内存溢出异常。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK