前言
如果只是向往,远方依旧是远方。
目录
JVM的内存分配
JVM(java虚拟机)的内存分配主要包括几个部分:程序计数器、java虚拟机栈、本地方法栈、java堆、方法区、运行时常量池、直接内存。这些内存区域在JVM中共同协作,为Java程序的执行提供了必要的支持。
Java虚拟机栈
Java虚拟机栈在内存区域中用来存储方法的局部变量、方法参数、方法调用和返回的状态信息,每个线程在运行过程中都会创建一个独立的栈帧,栈帧中存储了当前线程正在执行的方法的信息。
Java虚拟机栈是如何处理数据的?
1,方法调用和返回:
当一个方法被调用时,虚拟机会为该方法创建一个栈帧,并将这个栈帧压入虚拟机栈中,方法执行过程中,栈帧用于保存方法的执行环境和执行状态,当方法执行完毕后,栈帧会被出栈,程序返回到调用方法的栈帧继续执行。
2,存储局部变量和方法参数:每个方法在执行时,会在方法的栈帧中为局部变量和方法参数分配内存空间,并在方法执行完毕后与栈帧空间一同释放。
3,栈帧的管理:虚拟机栈以出栈的方式进行管理,通过压栈和出栈操作来维护栈帧的创建和销毁。栈帧的闯将和销毁是由Java虚拟机自动完成的。
注意:虚拟机栈的大小是有限的,当线程请求的栈深度超过虚拟机所允许的深度时,会抛出SrackOverFlowError。而当虚拟机栈无法分配足够的内存空间时,会抛出OutOfMemoryError。所以,在编写Java程序时,需要合理设计方法的调用和递归使用。
程序计数器
程序计数器(Program Counter)是用来存储当前正在执行的指令的地址。它是一个特殊的寄存器,在计算机体系结构中起着重要的作用。程序计数器的值会不断地递增,指向下一条将要被执行的指令。当一个指令执行完毕后,程序计数器就会自动增加,指向下一条要执行的指令的地址。通过不断更新程序计数器的值,计算机可以按照正确的顺序执行程序中的指令。
本地方法栈
本地方法栈(Native Method Stack)是用来支持执行本地方法的一块内存区域。本地方法是指在编程语言中调用由底层操作系统或其他外部库提供的函数。本地方法的实现通常使用其他编程语言(如C、C++)编写,而不是使用当前编程语言(如Java)。
当程序调用本地方法时,当前线程的执行状态需要从Java虚拟机(JVM)切换到底层操作系统或外部库中,通过本地方法栈来保存所需的执行环境和数据。本地方法栈的工作方式类似于Java虚拟机栈,但用于执行本地方法的帧和数据。
本地方法栈的大小是在JVM启动时预先确定的,并且可以根据需要进行调整。它通常比Java虚拟机栈的大小要大,因为本地方法的执行可能需要更多的内存空间。
总而言之,本地方法栈是为了支持执行本地方法而存在的,它提供了一个独立于Java虚拟机栈的执行环境,并保存了执行所需的上下文和数据。
什么情况下会执行本地方法?
1,调用底层系统函数或者操作系统API:有些功能需要直接与底层系统进行交互,例如晚间操作、网络通信、同行界面等。这些操作通常需要使用本地方法来调用操作系统提供的函数。
2,提高性能:有些算法或操作在本地编程语言中实现效率更高,因此可以将这些耗时的操作以本地方法的实现方式实现,从而提高整体程序的性能。
3,访问硬件设备:某些硬件设备(如传感器、摄像头等)可能需要直接访问操作系统和底层驱动程序才能获取数据。此时,执行本地方法可以提供对这些硬件设备的访问。
注意:执行本地方法可能需要特定的权限和安全性考虑。在调用本地方法之前,应该确保对其具有足够的授权,并且要小心处理可能的安全风险。
Java堆
Java堆是Java虚拟机管理的最大一块内存区域,用于存储Java应用程序中创建的对象,包括类的实例和数组。在Java堆中,对象的内存空间会动态的分配和释放。
Java的特点是它可以被所有线程共享,并且在JVM启动时被创建。它的大小可以通过JVM启动参数来指定,也可以根据应用程序的需要进行动态调整。堆内存的管理由JVM自动完成,包括对象的分配和回收。
Java堆中的区域是如何划分的?
Java堆主要范围三个区域:
-
新生代(Young Generation):新创建的对象首先被分配到新生代区域。它进一步分为Eden区和两个Survivor区(通常称为S0和S1)。大多数对象在Eden区进行分配,当Eden区满时会触发Minor GC,将存活的对象移动到Survivor区。在Survivor区中经过一定次数的GC后,仍然存活的对象将进入老年代。
-
老年代(Old Generation):老年代主要存放生命周期较长的对象。当新生代进行Minor GC时,若对象在Survivor区经历了一定次数的GC仍然存活,则会晋升到老年代。老年代满时会触发Full GC(Major GC),进行整理和回收。
-
永久代(Permanent Generation):永久代用于存放常量、静态变量、类信息等。在JDK8之前,永久代还用于存放字符串常量池等,但在JDK8中,永久代被元空间(Metaspace)所取代。
方法区
方法区是Java虚拟机的一部分,主要用于存储类的结构信息、常量、静态变量、即时编译器编译后的代码、态变量以及静态方法等。具体用途如下:
-
存储类的结构信息:方法区存储了类的元数据信息,包括类名、父类信息、接口信息、字段和方法的描述符、访问权限等。
-
存储常量:方法区存储了类的常量池,包括字面量和符号引用。字面量包括字符串、数字等常量,而符号引用包括类和接口的全限定名、字段和方法的名称以及描述符等。
-
存储静态变量:静态变量属于类级别的变量,存储在方法区中。当类被加载到内存中时,静态变量会被分配内存并初始化。
-
存储即时编译器编译后的代码:方法区还存储了即时编译器(JIT)编译后的本地机器代码。JIT编译器将热点方法转换为本地机器码以提高执行效率,并将生成的本地机器码存储在方法区供后续调用。
-
态变量:态变量是指在程序执行过程中可以改变其值的变量。它可以存储程序的状态或者进程的状态,并且可以在程序的不同部分被访问和修改。当程序执行时,它会根据不同的条件和操作改变态变量的值,从而影响程序的行为和结果。这么说可能不容易理解态变量有什么用,请看下面例子。
例如,在一个计数器程序中,计数器的值就是一个态变量。每次执行增加或减少操作,计数器的值会发生改变。在另一个例子中,一个游戏中的玩家状态(如生命值、分数等)也可以看作是态变量,因为它们会在游戏过程中随着玩家的行动发生变化。
态变量对于维护程序的状态和实现逻辑非常重要,可以帮助我们跟踪程序的执行过程并作出相应的决策。
什么是即时编译器?
即时编译器(Just-In-Time Compiler,简称JIT Compiler)是一种将代码在运行时动态编译的编译器。与传统的静态编译器不同,静态编译器将源代码一次性转换为机器代码,并生成可执行文件。而JIT编译器在程序运行过程中,将源代码实时地编译成机器代码,然后再执行。
JIT编译器的主要工作流程包括以下几个步骤:
- 解析:将源代码解析成抽象语法树(AST)或中间表示(IR)。
- 优化:对AST或IR进行优化,例如消除冗余代码、执行常量折叠、循环展开等。
- 编译:将优化后的AST或IR编译成机器码。
- 执行:执行生成的机器码。
JIT编译器的优势在于它可以根据程序运行时的环境和数据来进行动态优化,从而提高程序的执行效率。因为它可以获取运行时的信息,利用这些信息进行针对性的优化。例如,它可以根据实际输入数据类型来生成特定的机器码,以提高性能。
运行时常量池
运行时常量池(Runtime Constant Pool)是Java虚拟机在执行类加载过程中,将字面量和符号引用存放的地方。它是Class文件中的一部分,用于存储字符串常量、类和接口的全限定名、字段和方法的名称和描述符等常量信息。
运行时常量池有以下几个作用:
-
提供了一种方便的方式来访问常量。通过索引,可以快速访问常量池中的值,而无需每次都进行解析。
-
优化了内存使用。运行时常量池将字符串常量池、类和接口的全限定名、字段和方法的名称和描述符等常量信息存放在一起,避免了重复的存储,节省了内存空间。
-
支持动态语言特性。在动态语言中,常量池的内容可以在运行时动态地添加或修改,为动态语言提供了一定的灵活性。
运行时常量池是方法区中的一部分。
直接内存
直接内存(Direct Memory)通常在需要高性能数据传输的情况下使用。以下情况可能会选择使用直接内存:
- 需要快速高效地进行大量数据的读写操作,如网络数据的发送和接收,磁盘IO等。
- 需要降低内存拷贝的开销,特别是在频繁进行数据拷贝的场景下,使用直接内存可以减少CPU的负载和提高效率。
- 需要直接与硬件设备进行交互,并且硬件设备支持直接内存访问,如图形处理器(GPU)、网络卡、声卡等。
使用直接内存要注意内存管理的问题,因为直接内存是绕过了Java虚拟机对内存的管理,所以需要手动释放直接内存,否则可能会造成内存泄漏。
我是专注学习的章鱼哥~