首先说一下:C/C++ 和 Java 一个非常主要的区别就是内存的管理,C/C++ 是 手动开辟和释放的,而Java是依赖于JVM来进行管理
注:跟内存模型JMM不是一件事
1. 程序计数器:
- 理解:是一块很小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。它标识了下一条需要执行的字节码指令,通过它来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要这个计数器来完成。
- 作用:
-
- 字节码解释器通过改变程序计数器来读取指令,从而实现代码的流程控制。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程切换回来之后能知道该线程上次运行到哪了
注:唯一不会出现oom的内存区域
- 总结:线程私有的,一个线程的执行就是在该计数器的不断变化推动下一步一步完成的。
2. Java虚拟机栈:
- 理解:线程私有,除了Native方法之外,所有的Java方法调用通过调用栈来实现的(当然也需要其他运行时数据区进行配合),生命周期和线程一致
- 工作过程: 每一个方法调用都会有对应的栈帧被压入栈中,抛出异常和return指令都会导致栈帧弹出
- 栈帧:包含局部变量表、操作数栈、动态链接、方法返回地址
- 说明:如果请求的栈深度大于虚拟机允许的深度,就会StackOverFlowError异常;如果栈容量可以动态扩容,当栈拓展到无法申请足够的内存会抛出OurOfMemoryError
2.1关于栈帧的介绍★
线程调用一个方法就对应着一个栈帧的入栈和出栈。栈的顶部第一个栈帧叫做当前栈帧,对应的是一个线程需要执行的最新的方法;栈帧内部主要包括局部变量表,操作数栈,方法返回地址,动态链接等。
2.1.1局部变量表
介绍:是一组变量值的存储空间,用于存放方法参数,和方法内部定义的局部变量,主要包括编译器就可知道的各种基本数据类型,对象引用等,所以局部变量表所需要的内存大小编译器就能确定下来,并且在整个方法运行期间都不会改变
变量槽:(了解即可)
- 局部变量表的容量以变量槽为最小单位
- Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始致局部变量表最大的变量槽数量
- 如果执行的是实例方法,那局部变量表中的第0位索引的变量槽默认是用于传递方法所属对象实例的引用”this“
- 为了尽可能节省栈帧用的内存空间,局部变量表中的变量槽是可以重用的,如果当前字节码PC计数器的值已经超过某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来使用
2.1.2操作数栈
介绍:主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。当一个方法执行过程中,会有各种字节码指令对各种操作数栈出栈和入栈操作。比如字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入两个int型的数值,当执行这个指令时,会把两个int型值出栈并相加,然后将相加的结果重新入栈
2.1.3动态链接
每个栈帧都包含一个指向运行时常量池中该“栈帧所属方法”的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在 Class 文件的常量池中。比如: 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
2.1.4方法返回地址
- 方法正常退出: 一个方法正常执行完成之后,会遇到返回指令。这种情况会有一个预先定义好类型的返回值返回给调用方法。
- 方法异常退出:一个方法执行过程中,如果发生了异常,并且异常没有再方法中妥善的捕获处理,那也会触发方法的退出。这种情况是不会有返回值返回给调用方的。
方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方去的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去
附加信息
栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息,但这些信息取决于具体的虚拟机实现
3. 本地方法栈:
- 理解:为虚拟机使用的Native方法服务,作用原理跟虚拟机栈相似
- 就是一个Java调用非Java代码的接口
4. 堆 ★
- 理解:Java虚拟机所管理内存最大的一部分,Java堆时所有线程共享的一块内存区域,在虚拟机启动时创建。是垃圾收集器的主要管理区域
- 作用:存放对象实例,几乎所有的对象实例以及数组都是在这里分配内存
- 组成:在Java 8和7之间,内存堆的一些重要变化包括以下几点:
- 永久代的移除:Java 8取消了永久代(PermGen),取而代之的是元空间(Metaspace)。永久代在Java 7中用于存储类的元数据和常量池等信息,而在Java 8中,这些信息被迁移到了元空间中。
- 字符串常量池的变化:在Java 8中,字符串常量池不再位于永久代中,而是被移到堆中。
- 元空间的垃圾回收:元空间不再使用传统的垃圾回收机制,而是使用了基于本地内存管理的垃圾回收。这种方式减少了对垃圾回收造成的应用程序停顿时间。
- 元空间的存储位置:在Java 7中,永久代位于堆中的一部分,而在Java 8中,元空间与堆是完全分离的,它可以在物理上独立存在。
- 元空间的动态分配:在Java 8中,元空间的大小可以动态调整,不再使用预定义的固定大小。这使得元空间可以根据应用程序的需求进行自动扩展和收缩。
总的来说,Java 8对内存堆的变化主要集中在永久代的取消、元空间的引入和动态分配,以及字符串常量池的位置变化等方面,这些变化都旨在提高内存管理的灵活性和性能。
5.方法区:
- 理解:是一块存储类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 常量池:一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型、域和方法的符号引用。
一些变化:
- Java 7 中,方法区确实位于永久代 (PermGen),但 PermGen 的大小是可变的,可以使用 JVM 参数进行调整;同时,PermGen 中存放的除了方法区外,还包括类的元数据信息、字符串常量池、静态变量等信息。
- Java 7 中,确实将部分静态变量的存放位置从 PermGen 移到了堆中,并且可以通过 JVM 参数
-XX:PermSize
和-XX:MaxPermSize
来调整 PermGen 的大小。 - Java 8 中废除了永久代,并将方法区迁移到了元空间 (Metaspace),元空间与堆不相连,但确实与堆共享物理内存。另外,Java 8 中还引入了一个新参数
-XX:MaxMetaspaceSize
用于控制元空间最大可用内存
运行时常量池
运行时常量池(Runtime Constant Pool) 是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池表(ConstantPool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放,JVM 为每个已加载的类型(类或接口)都维护一个运行时常量池,在加载类和接口到虚拟机后创建。所以运行时常量池相对于Class文件常量池的另一重要特性: 具备动态性
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutofMemoryError异常
6.本地内存和直接内存
- 本地内存(Native Memory) 并不是虚拟机运行时数据区的一部分,它也不是Java虚拟机规范定义的内存区域。我们可以看到在 HotSpot 中,JDK1.8就将方法区移除了,用元数据区来代替,并且将元数据区从虚拟机运行时数据区移除了,转到了本地内存中,也就是说这块区域是受本机物理内存的限制,当申请的内存超过了本机物理内存,才会抛出 OutofMemoryError 异常。
- 直接内存(Direct Memory) 也是受本机物理内存的限制,在JDK1.4中新加入的NIO (new input/output) ,引入了一种基于通道 (Channel) 与缓冲区(Buffer)的I/0方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用操作,这样避免了在Java堆和Native堆中来回复制数据,显著提高性能
Java 程序内存 = JVM 内存 + 本地内存
本地内存 = 元空间 + 直接内存
额外补充
对象的创建过程:new的动作后JVM做的事
- 类加载检查:先去常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过、解析和初始化过。如果没有那必须先器进行类加载
- 分配内存:类加载之后就能确定对象的大小,为对象分配空间的任务等同于把一块确认大小的内存从堆中划出来,有两种方式:
- 指针碰撞:没有内存碎片
- 空闲列表:有内存碎片
- 初始化零值:将分配到的内存空间都初始化为零值
- 设置对象头:相当于这个对象的一个标签,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息
- 执行init方法,把对象按照程序员的意愿进行初始化