JVM——堆、栈

目录

JVM

JVM内存结构

类加载器

类加载器的作用

类加载器分类

双亲委派机制

打破双亲委派机制

方法区(Method Area)

Metaspace 元空间

堆(Heap)

虚拟机栈

堆和栈的区别

Native

程序计数器


JVM

Java虚拟机即Java Virtual Machine。

跨平台特性:一次编写、到处运行

自动实现垃圾回收

Java源代码(.java)需要经过编译器编译成字节码(.class),JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。

JVM内存结构

Java虚拟机(一般用的是HotSpot)

类加载器

类加载器的作用

就是将字节码文件(class文件)加载到JVM中。当一个类被使用的时候,才会加载到内存

类加载的执行过程(类的生命周期)其中准备、验证、解析3个部分统称为连接

  • 加载:查找和导入class文件
  • 验证:保证加载类的准确性
  • 准备:为类变量分配内存并设置类变量初始值
  • 解析:把类中的符号引用转换为直接引用
  • 初始化:对类的静态变量,静态代码块执行初始化操作
  • 使用:JVM 开始从入口方法开始执行用户的程序代码
  • 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。

类加载器分类

  • 启动类加载器:用来加载java核心类库,无法被java程序直接引用;
  • 扩展类加载器:用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
  • 应用程序类加载器:它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
  • 自定义类加载器:由java语言实现,继承自ClassLoader;

双亲委派机制

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终达到顶层的启动类加载器。

如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。

  • 优点
    • 避免类的重复加载
    • 保护程序安全,防止核心API被随意篡改
  • 缺点
    • 在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。例如:SPI机制

打破双亲委派机制

自定义类加载器

  • 自定义类加载器加载一个类需要:继承ClassLoader,重写findClass,如果不想打破双亲委派模型,那么只需要重写findClass;如果想打破双亲委派模型,那么就重写整个loadClass方法,设定自己的类加载逻辑,想要打破 即重写的时候让自己去加载 不让父加载器去加载

线程上下文类加载器

  • JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。​

Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。

方法区(Method Area)

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单来说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。

存放静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。尤其是类信息,可以理解为实例化变量的模板化信息,因为user对象我们可以new很多个出来,但是用的类信息是User.class类,这种模板类信息按道理只需要加载一次的,所以这部分信息单独放在方法区。

Metaspace 元空间

元空间是方法区的实现。方法区的实现,JDK1.7之前是永久代,JDK1.8之后是元空间。

  • 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

堆(Heap)

一个JVM只有一个堆内存,堆内存大小可以调节

存放几乎所有的对象实例和数组。堆是 OOM 故障(内存溢出)最主要的发生区域

GC: 主要的垃圾回收工作发生在堆中,回收无用的对象,释放内存空间。

从内存回收的角度,可划分为新生代和老年代,可进一步细分为Eden空间、From Survivor空间、To Survivor

  • 新生代(1/3 堆空间)
    • 伊甸区(Eden)(8/10)JAVA中的对象一般先分配到eden区,
    • 幸存0区(from)(1/10)
    • 幸存1区(to)(1/10)
  • 老年代(2/3 堆空间)

虚拟机栈

对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈。也就是说虚拟机栈的生命周期和线程是一致。并且是线程私有的。Java 虚拟机栈中出栈入栈的元素就称为「栈帧」。

线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

线程请求的栈深度超过虚拟机允许的最大深度,抛出StackOverFlowError异常(栈内存溢出)(递归)。

堆和栈的区别

1、申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;

2、申请大小的不同。栈获得的空间较小,大小是固定的,而堆获得的空间较大,可以根据需要进行动态调整;

3、申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;

4、底层不同。栈是连续的空间,而堆是不连续的空间。

5、共享性的不同 。栈内存是线程私有的。 堆内存是所有线程共有的。

6、功能不同。栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。

7、存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;

Native

凡是带了native 关键字的,说明java的作用范围达不到了,会去调用底层C语言的库

会进入本地方法栈

调用本地方法本地接口(JNI)

JNI作用:扩展Java的使用,融合不同的编程语言为Java所用,通过JNI加载本地方法库中的方法

在内存中专门开辟了一块标记区域:本地方法栈(Native Method Stack),来登记Native方法

程序计数器

看作是当前线程所执行的字节码的行号指示器

记录一下方法执行到哪了

<think>嗯,用户这次提出了一个非常具体的JVM内存模型问题,聚焦在privatefinal变量的存储位置和原理上。从对话历史看,用户已经对Java类方法和实例方法有较深理解,现在想深入内存分配机制,尤其是final变量的特殊性。用户可能正在研究Java性能优化或内存管理,因为final变量在并发编程和内存效率上有重要意义。ta引用的资料显示已经查阅过、方法区的基本概念,但需要更精确的定位——这属于进阶开发者才会关注的问题。关于privatefinal变量的存储,需要分三个层面解释:首先是基本类型和引用类型的区别,然后要看变量属于类成员还是实例成员,最后结合JVM规范说明原理。尤其要注意final在编译期和运行期的不同表现,比如字符串常量池的特殊处理。从技术深度考虑,应该强调这些关键点:1)实例final变量必然在中,因其生命周期绑定对象2)类(static)final变量在方法区,但JDK8后元空间的变化3)基本类型的final变量可能被内联优化,这关系到字节码特性4)final引用类型和不可变性(immutable)的区别,用户可能混淆这两者内存模型图很有必要,可以用mermaid展示//方法区的关系,并标注不同final变量的位置。同时补充一个代码示例对比普通变量和final变量,比如用javap反编译展示常量折叠现象。相关问题设计要延伸这个知识点的应用场景:多线程安全、类加载机制、内存优化。特别是嵌入式开发中final的价值,这可能是用户没明说但实际需要的方向。</think> ### JVM内存模型与`private final`变量存储解析 #### **一、JVM核心内存区域存储内容** | **内存区域** | 存储内容 | 生命周期 | 线程共享性 | |--------------|----------|----------|------------| | **(Heap)** | 所有`new`创建的对象实例、数组 | 对象可达性决定 | ✓ 全局共享 | | **(Stack)** | 方法局部变量、基本类型值、对象引用 | 线程或方法生命周期 | ✗ 线程私有 | | **方法区(Method Area)** | 类元信息(字段/方法/常量池)、运行时常量池、**静态变量(`static`修饰)** | JVM进程周期 | ✓ 全局共享 | | **元空间(Metaspace)<sup>*</sup>** | 类元数据(JDK8+替代方法区) | 类加载/卸载决定 | ✓ 全局共享 | > <sup>*</sup>JDK8开始方法区被元空间替代(使用本地内存)[^3] #### **二、`private final`变量的存储位置与原理** ##### **1. 存储位置规则** ```mermaid graph TD A[private final变量] --> B{变量类型} B -->|基本类型| C[] B -->|引用类型| D[] A --> E{是否static} E -->|是| F[方法区/元空间] E -->|否| G[中的对象内部] ``` - **实例变量(非static)**: 存储在**内存**的对象实例内部。例如: ```java class User { private final int id; // 存于内存User对象内部 User(int id) { this.id = id; } } ``` - **类变量(static修饰)**: 存储在**方法区(JDK7)或元空间(JDK8+)**。例如: ```java class Config { private static final String KEY = "API_KEY"; // 存于方法区 } ``` ##### **2. `final`的特殊机制** - **编译期优化(常量折叠)**: 若基本类型/字符串满足常量条件(如:`private final int MAX=100;`),编译器会直接替换为字面值,**无需内存访问**[^2]。 - **运行期不可变性**: ```java class Example { private final List<String> data = new ArrayList<>(); void addData(String item) { data.add(item); // ✓ 允许:final约束引用而非对象内容 } } ``` ##### **3. 内存分配原理** - **对象头开销优势**: `final`变量使JVM可优化对象头内存布局(如消除偏向锁标志) $$ \text{对象内存} = \text{对象头} + \text{实例数据} + \text{对齐填充} $$ `final`变量在类加载时即确定偏移量,减少内存计算开销[^1]。 - **逃逸分析优化**: JVM可能将包含`final`局部变量的对象**上分配**,避免内存压力: ```java void calculate() { final int bufferSize = 1024; // 可能直接分配在帧 byte[] buffer = new byte[bufferSize]; } ``` #### **三、关键对比** | **变量类型** | 存储位置 | 是否线程安全 | 可修改性 | |---------------------|-------------------|--------------|------------------| | `private final` 实例变量 | 内存对象内部 | ✓ 初始化安全 | ✗ 引用不可变 | | `private final` 类变量 | 方法区/元空间 | ✓ 绝对安全 | ✗ 完全不可变 | | `private` 普通变量 | 依据作用域确定 | ✗ 需同步 | ✓ 可任意修改 | > 注意:字符串常量(如`final String s="abc"`)存储于**运行时常量池**,被所有对象共享[^2] --- ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ta是个码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值