Java内存区域

内存区域

内存区域是计算机中用于存储数据和程序的地方。就像我们的大脑有不同的区域来记忆和处理不同的信息一样,计算机内存也分为不同的部分。

  1. 栈(Stack): 像一摞盘子一样,栈用于存放程序运行时的小片数据。每当函数被调用,就像在盘子堆上叠加一层新的盘子,函数结束后又会一个一个地取出,保证程序的运行顺序。

  2. 堆(Heap): 堆像一个大仓库,用于存放较大的数据,比如我们创建的对象和数组。堆可以分为新的部分和旧的部分,有垃圾回收帮助我们清理不再需要的数据,避免浪费。

  3. 方法区(Method Area): 就像图书馆里存放书籍一样,方法区存储程序的结构和方法信息,比如类的名字、方法的指令。它在不同的编程语言中也有不同的称呼,比如Java中的永久代或元空间。

  4. 程序计数器(Program Counter): 像一本书的页码一样,程序计数器帮助计算机记住当前执行的指令位置。它确保程序顺利执行,处理分支、循环和异常等基础功能。

这些内存区域在计算机的运行中相互配合,帮助程序正确地运行并保存数据。

TODO:此处放一个内存区域图

一、栈

1.1 概述

Java虚拟机栈(JVM Stack)用于存储Java程序运行时的方法调用和局部变量等信息。每个线程都有自己独立的虚拟机栈,方法的调用就像盘子一样,在栈中逐层叠加和弹出。当一个方法被调用时,会创建一个新的栈帧用于保存该方法的信息,当方法执行结束后,栈帧会被弹出,程序回到调用该方法的位置继续执行。虚拟机栈的大小是固定的,在方法调用过程中会动态地进行栈帧的压栈和出栈操作,确保程序的正确执行。

1.2 栈的组成部分

虚拟机栈(Java Virtual Machine Stack)是由栈帧(Stack Frame)组成的。每个线程在运行时都有自己独立的虚拟机栈,而每个方法的执行都对应着一个栈帧。栈帧是用于存储方法执行过程中的数据和信息的数据结构。

  1. 局部变量表(Local Variable Table): 局部变量表用于存储方法中的局部变量,包括基本数据类型和对象引用。局部变量表在方法的调用过程中被创建,存储了方法的参数和在方法内部定义的局部变量。

  2. 操作数栈(Operand Stack): 操作数栈是一个后进先出(LIFO)的栈结构,用于在方法执行过程中进行计算和操作。当方法调用时,参数和局部变量的值被压入局部变量表中,而方法内的计算则在操作数栈上进行。

  3. 动态链接(Dynamic Linking): 动态链接用于支持方法调用时的动态绑定。Java程序使用虚方法表(Virtual Method Table)实现动态绑定。动态链接将方法调用和方法的实际实现关联起来,使得方法调用可以根据对象的实际类型来选择正确的方法。

  4. 返回地址(Return Address): 返回地址用于记录方法调用完成后要返回的位置。当方法执行完毕,控制权需要返回到方法调用的地方,返回地址指示了返回的位置。

栈帧在方法调用时被创建,方法执行结束后被销毁。每个方法的执行都对应着一个新的栈帧的创建,当方法返回时,栈帧会被弹出。虚拟机栈中的栈帧不断地压栈和出栈,保障了方法的正确调用和执行。每个线程在虚拟机栈中拥有自己独立的栈帧,线程之间的栈帧互不干扰,这使得Java程序可以支持多线程并发执行。

1.3 示例

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

public class Example {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 使用父类引用指向子类对象
        animal.makeSound(); // Output: Dog barks
    }
}

1.3.1 说明

在上述示例中,我们定义了一个基类 Animal 和一个子类 Dog,分别覆盖了 makeSound() 方法。

main 方法中,我们创建了一个名为 animal 的局部变量,并使用父类 Animal 的引用指向子类 Dog 的对象。

animal.makeSound() 被调用时,Java虚拟机根据动态链接找到了 animal 对象的实际类型是 Dog,然后在 Dog 类的虚方法表中找到了对应的 makeSound() 方法,输出为 “Dog barks”。

1.3.2 栈帧解析

在Java虚拟机的执行过程中,每个方法的调用都会创建一个栈帧,用于存储方法的局部变量、操作数栈、动态链接和返回地址。

  • main 方法中的局部变量表部分存储了变量 animal 的值,它是一个引用类型,指向子类 Dog 的对象。

  • 在调用 animal.makeSound() 方法时,main 方法的操作数栈部分会将 animal 的引用压入栈中。

  • 动态链接的过程发生在调用 animal.makeSound() 方法时,根据对象的实际类型 Dog,在 Dog 类的虚方法表中找到了对应的 makeSound() 方法。

  • 返回地址部分记录了 animal.makeSound() 方法执行完成后要返回到 main 方法的位置。

二、程序计数器

2.1 概述

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里1,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

2.2 计数器的功能

  1. 线程切换: 在多线程环境中,线程切换是常见的操作。当线程被中断或时间片用完时,Java虚拟机必须确保线程能够恢复到正确的执行位置。程序计数器就是用来记录线程执行的位置,确保线程切换后能够正确地继续执行。

  2. 指令流程控制: 程序计数器是控制指令流程的关键。它通过不断地增加或改变存储的指令地址来使得程序按照正确的顺序执行。

  3. 异常处理: 在Java程序中,可能会遇到异常,如空指针异常、数组越界等。当异常发生时,程序计数器记录了异常处理器的位置,以便在异常处理时能够正确地执行相应的处理代码。

  4. 循环和跳转: 在Java程序中,循环和跳转是常见的控制结构。程序计数器确保循环能够正确地执行,并且在条件满足时能够正确地跳转到目标地址。

2.3 计数器存放在哪里

在Java虚拟机的运行时数据区域中,程序计数器是每个线程私有的一部分,它存放在线程私有的内存区域中。具体来说,程序计数器属于线程栈(Java虚拟机栈)的一部分,每个线程都有自己独立的程序计数器。

三、堆

3.1 概述

Java堆是Java虚拟机管理的最大内存区域,它被所有线程共享,主要用于存放Java程序中创建的对象实例。在Java堆中,可以使用线程私有的分配缓冲区(Thread-Local Allocation Buffer,TLAB)来提高对象分配的效率。Java堆在逻辑上是连续的,但在物理上可以是不连续的。

3.2 Java堆的划分

Java堆可以划分为不同的区域,其中常见的划分包括:

  1. 新生代(Young Generation): 新生代用于存放新创建的对象。它又分为Eden空间和两个Survivor空间(通常是From和To)。在新生代中,对象首先会被分配在Eden空间,当Eden空间满时,会触发Minor GC(年轻代垃圾回收),将仍然存活的对象复制到其中一个Survivor空间,并清理掉已经不再存活的对象。

  2. 老年代(Old Generation): 老年代用于存放经过多次垃圾回收仍然存活的对象。当Survivor空间满时,存活的对象会被晋升到老年代。老年代的垃圾回收称为Major GC(老年代垃圾回收)。

  3. 持久代(Permanent Generation,已在Java 8中被元空间Metaspace替代): 持久代主要用于存放类的元数据、常量池、静态变量和即时编译器(JIT)优化后的代码等。

3.3 Java堆的扩展与OutOfMemoryError

Java堆通常是可扩展的,可以通过设置Java虚拟机的参数来调整大小。如果Java堆中没有足够的内存进行实例分配,并且无法再进行扩展,就会抛出·OutOfMemoryError异常,表示内存不足。

Java堆的合理设置对于Java程序的性能和稳定性至关重要。通过合理划分不同区域以及调整大小,可以优化垃圾回收的效率和对象分配的性能,从而提升Java程序的整体表现。

3.4 TLAB

TLAB(Thread Local Allocation Buffer)是一种用于提高对象分配效率的优化技术。它是Java虚拟机在Java堆中的一块线程私有的缓冲区域。

3.4.1 TLAB的作用和优势

在多线程环境下,多个线程同时进行对象分配可能导致竞争和争夺可用的内存空间,从而降低分配效率。TLAB的作用就是为每个线程预先分配一块私有的、大小适中的内存区域,这样每个线程在分配对象时就可以在自己的TLAB中进行,而不需要竞争和争夺Java堆中的全局内存空间。

TLAB的优势包括:

  1. 减少内存分配的竞争: 由于每个线程有自己的TLAB,所以线程之间不再需要竞争全局内存空间来进行内存分配,减少了竞争的发生。

  2. 提高对象分配的效率: 由于TLAB是线程私有的,因此在分配对象时,线程可以直接在自己的TLAB中进行,无需频繁地和其他线程进行同步和竞争,从而提高了对象分配的效率。

  3. 减少内存回收的同步: 当一个TLAB中的内存空间被用完时,线程会去获取一个新的TLAB。这个过程通常不需要进行同步,因为每个线程都有自己的TLAB,减少了内存回收的同步操作。

3.4.2 TLAB的应用场景

通过使用TLAB,Java虚拟机可以显著地提高对象的分配效率,在多线程环境下减少了竞争和同步的开销,从而提升了整体的性能。尤其在高并发的场景下,TLAB对于减少竞争和提高分配效率的作用更为明显。

四、方法区

4.1 概述

方法区是Java虚拟机管理的一块内存区域,与Java堆类似,它也是各个线程共享的。方法区主要用于存储已被虚拟机加载的类的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

4.2 方法区的组成部分

方法区的组成部分主要包括:

  1. 类型信息(Type Information):方法区存储已被虚拟机加载的类的结构信息,包括类的字段、方法、继承关系等。这些信息在程序运行期间一般是不会发生变化的。

  2. 常量池(Constant Pool):常量池是每个类或接口的一部分,用于存放编译期生成的各种字面量和符号引用。包括字符串常量、类和接口的全限定名、字段和方法的名称和描述符等。

  3. 静态变量(Static Variables):方法区存储类的静态变量,即被 static 修饰的变量。静态变量在类的生命周期内都存在,直到类被卸载。

  4. 即时编译器编译后的代码缓存:方法区可以用来缓存即时编译器(Just-In-Time Compiler,JIT)编译后的本地代码,以提高代码的执行效率。

4.3 方法区与永久代的关系

在早期的Java虚拟机中,方法区经常被称为“永久代”(Permanent Generation)。尤其是在JDK 8之前,许多人习惯将方法区和永久代混为一谈。然而,实际上方法区和永久代并不等价,永久代只是一种特定的实现方式,而方法区是一个更为通用的概念。

4.4 方法区的作用

方法区的作用是存储已加载类的信息,包括类的结构、常量池、静态变量、方法字节码等。它还负责存储编译后的代码,以及进行方法的动态链接和加载,实现Java程序的运行。

4.5 方法区的内存回收

方法区的内存回收主要针对常量池的回收和类型的卸载。但是方法区的回收效果相对较差,特别是对类型的卸载,条件较为苛刻,有时难以令人满意。不过,对于某些情况下的内存泄漏问题,方法区的回收仍然是必要的。

4.6 方法区的内存不足

在Java虚拟机规范中,如果方法区无法满足新的内存分配需求,将抛出OutOfMemoryError异常,表示无法继续分配更多的内存。

4.7 元空间(MetaSpace)

需要注意的是,在JDK 8及以后的版本中,方法区被替换为元空间(MetaSpace)。元空间不再是Java堆的逻辑部分,而是使用本地内存(Native Memory)来实现,从而解决了一些方法区内存回收的问题。

4.7.1 为什么JDK 8及以后的版本中,方法区被替换为元空间

JDK 8将方法区(永久代)改成元空间(MetaSpace)的主要原因是为了解决一些方法区存在的问题和限制。以下是一些主要的原因:

  1. 方法区内存回收问题: 在旧的方法区(永久代)实现中,垃圾回收的效果并不理想,特别是对于常量池和类型的卸载。这导致在某些情况下会发生内存泄漏,无法回收不再使用的类和常量。

  2. 永久代大小限制: 永久代有一个固定的上限,即使不设置参数,也有默认大小,例如在32位系统上的限制为4GB。这可能导致在应用程序使用大量类和常量时,永久代的大小不够用,从而导致OutOfMemoryError异常。

  3. 热部署限制: 在永久代中,对类的加载和卸载有一定的限制,导致无法实现在运行时动态地加载和卸载类,从而影响了一些热部署的功能。

为了解决上述问题,JDK 8引入了元空间(MetaSpace)。元空间不再是Java堆的逻辑部分,而是使用本地内存(Native Memory)来实现,因此不再有永久代的大小限制。同时,元空间的内存回收效果更好,能够更及时地回收不再使用的类和常量,从而避免了内存泄漏的问题。

另外,元空间的实现还引入了一种新的方式来管理类的元数据信息,使得类的加载和卸载更加灵活,可以实现在运行时动态地加载和卸载类,从而支持更好的热部署功能。

总的来说,JDK 8将方法区改成元空间是为了解决方法区存在的问题,并提供更好的内存回收机制和更灵活的类加载和卸载功能,从而提高Java虚拟机的性能和可用性。

QA概要

  1. 什么是内存区域?

    • 内存区域是计算机中用于存储数据和程序的地方,类似于大脑中记忆和处理信息的不同区域,计算机内存分为栈、堆、方法区、程序计数器等部分。
  2. 栈是什么?它的作用是什么?

    • 栈用于存放程序运行时的小片数据,类似一摞盘子,每当函数被调用,就像在盘子堆上叠加一层新的盘子,函数结束后又会一个一个地取出,保证程序的运行顺序。
  3. 堆是什么?它有什么特点?

    • 堆是用于存放较大的数据,如对象和数组的大仓库,可以分为新生代和老年代。有垃圾回收帮助清理不再需要的数据,避免浪费。
  4. 方法区是什么?它存储哪些信息?

    • 方法区存储程序的结构和方法信息,比如类的名字、方法的指令。在不同的编程语言中也有不同的称呼,比如Java中的永久代或元空间。
  5. 程序计数器是什么?它的功能是什么?

    • 程序计数器是当前线程所执行的字节码的行号指示器,在Java虚拟机的概念模型里,字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令,实现程序的控制流程。
  6. 举例说明动态链接在Java中的应用。

    • 动态链接在Java中用于支持方法调用时的动态绑定。例如,当通过父类引用指向子类对象时,根据对象的实际类型,在运行时会选择正确的子类方法进行调用。
  7. TLAB是什么?它的作用和优势是什么?

    • TLAB(Thread Local Allocation Buffer)是Java虚拟机用于提高对象分配效率的优化技术。它为每个线程预先分配一块私有的内存区域,减少内存分配的竞争,提高对象分配效率。
  8. 为什么JDK 8及以后的版本中,方法区被替换为元空间?

    • JDK 8引入元空间来解决方法区存在的问题,如内存回收效果差、永久代大小限制、热部署限制等。元空间使用本地内存实现,没有永久代的大小限制,回收效果更好,并支持更灵活的类加载和卸载,提高性能和可用性。

  1. Java虚拟机的概念模型,指的是Java虚拟机的抽象结构和行为模型,而非具体实现。 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值