目录
1、程序计数器(Program Counter Register)
(2)为什么程序计数器是虚拟机中唯一没有OutOfMemoryError的区域?
(4)为什么需要设计本地方法栈(Native Method Stack)?
JVM 运行时数据区指的是 JVM 在运行时分配的内存区域,主要分为以下几个部分:
- 程序计数器(Program Counter Register):线程私有的内存区域,用于记录当前线程所执行的字节码指令的地址。每个线程都有一个独立的程序计数器。
- Java 虚拟机栈(JVM Stack):线程私有的内存区域,用于存储 Java 方法执行的栈帧。每个方法执行时都会创建一个栈帧,栈帧包括局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,不同的是本地方法栈为本地方法服务。// Native 方法
- Java 堆(Java Heap):线程共享的内存区域,用于存储对象实例及数组。Java 堆是垃圾收集器管理的主要区域,因此也被称为垃圾收集堆。
- 方法区(Method Area):线程共享的内存区域,用于存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JAVA运行时数据区图示:
// 其实总结起来就是堆和栈,重点也就是研究这个两个区域
1、程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它是Java虚拟机中的一部分,每个线程都有自己的程序计数器(线程私有)。程序计数器可以看做是当前线程所执行的字节码的行号指示器,或者字节码解释器工作时的“工作指针”,它存储了下一条将要执行的指令的地址。当Java虚拟机执行线程的方法时,程序计数器记录当前执行的虚拟机字节码指令的地址,如果执行的是Native方法,则程序计数器的值为空(Undefined)。
(1)为什么需要程序计数器?
程序计数器(Program Counter Register)是Java虚拟机中一块比较小的内存区域,它的主要作用是记录当前线程所执行的字节码的行号指示器,即记录当前线程所执行的位置。当执行方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址;当执行完方法返回时,程序计数器保存的是下一条指令的地址。由于Java虚拟机采用的是线程轮流切换的方式来实现多线程,因此每个线程都需要有自己的程序计数器来记录当前线程所执行的字节码指令位置,以便线程在切换后能够恢复到正确的执行位置,继续执行。// 记录程序执行的位置
另外,程序计数器还可以被用于支持Java虚拟机的指令重排序优化等。指令重排序是为了提高程序执行效率,在保证程序语义正确性的前提下,将原本按照程序顺序执行的指令进行重排,以减少指令之间的数据依赖,提高并行度,从而提高程序执行效率。程序计数器可以记录指令重排序后的执行位置,以保证程序执行时的正确性。
(2)为什么程序计数器是虚拟机中唯一没有OutOfMemoryError的区域?
在Java虚拟机中,程序计数器(Program Counter Register)通常是线程私有的,因此内存消耗很小,甚至可以忽略不计。它是一个指向下一条需要执行的指令的地址的指针,对于线程来说,它存储了线程当前执行的字节码指令的地址,随着线程的执行,它的值会不停地变化。(其作用是记录当前线程所执行的字节码指令的地址)// 一条指针数据并不会溢出
程序计数器之所以是虚拟机规范中唯一没有OutOfMemoryError的区域,是因为它的大小是固定的,并且它所存储的数据是线程私有的,不会出现内存溢出的情况。
(3)为什么程序计数器不需要进行垃圾回收?
程序计数器不需要进行垃圾回收的原因是,它是线程私有的,生命周期与线程相同。当线程结束时,程序计数器也会随之销毁,不会对其他线程或应用程序造成影响。
2、Java 虚拟机栈和本地方法栈
(1)Java 虚拟机栈(JVM Stack)
Java 虚拟机栈(JVM Stack)是 Java 虚拟机运行时数据区的一部分,用于存储 Java 方法执行的线程私有的局部变量表、操作数栈、动态链接、方法出口等信息。
每个 Java 方法执行时,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储该方法的局部变量表、操作数栈、动态链接、方法出口等信息。栈帧是 Java 虚拟机栈的元素,当一个方法调用结束时,该方法对应的栈帧被出栈。
局部变量表用于存储该方法中定义的局部变量以及方法参数的值,对于不同的数据类型,需要占用不同的局部变量表槽位。操作数栈用于存储临时变量和操作数,在方法执行时,Java 虚拟机的执行引擎会将操作数栈的栈顶元素出栈,执行相应的操作,再将结果入栈。
除了局部变量表和操作数栈,Java 虚拟机栈中还存储了一些其他信息。其中,动态链接用于指向该方法的运行时常量池中该方法所属的类的符号引用,以便进行动态方法调用。方法出口用于存储该方法执行完毕后,程序执行的下一条指令地址。
JVM Stack 的大小可以通过虚拟机参数 -Xss
来设置,默认值为 1MB。如果栈空间不足,会抛出 StackOverflowError
异常,如果线程请求的栈深度大于虚拟机所允许的深度,会抛出 StackOverflowError
异常。因此,在开发过程中,需要根据实际情况调整 JVM Stack 的大小。
// 总结一下:Java 虚拟机栈主要用来存储执行方法的相关元素。方法要执行时,创建一个栈帧(Stack Frame),方法执行完毕,就进行出栈操作。因此方法中的所有信息都储存在栈中,至于实际的引用的对象,由堆去存储,在栈中只有对象的引用。
(2)Java 虚拟机栈有什么作用?
Java 虚拟机栈(JVM Stack)是一块内存区域,用于存储线程运行时的方法调用和局部变量等信息。每个线程都有自己的 JVM Stack,用于支持线程调用方法时的数据结构。
JVM Stack 的作用可以归纳为以下几点:
- 支持方法调用:当线程执行方法时,需要创建一个新的栈帧,用于存储方法的参数、局部变量和返回值等信息。当方法返回时,栈帧被销毁,恢复上一个栈帧的状态。
- 存储局部变量:JVM Stack 中的栈帧可以存储方法的局部变量,包括基本类型和对象引用。
- 维护方法调用的执行环境:JVM Stack 中的栈帧还包含一些与方法调用相关的信息,例如调用者的栈帧指针、操作数栈等。
// 总之,简单的理解起来就是存储方法中的一些东西
(3)本地方法栈(Native Method Stack)
本地方法栈(Native Stack)是 Java 虚拟机使用的一块内存区域,主要用于存储 Java 虚拟机所调用的本地方法(Native Method)的栈帧信息。本地方法是使用本地语言(如 C、C++等)编写的方法,能够直接调用底层系统的资源,如操作系统或硬件设备等,这些方法不能直接在 Java 代码中实现。
本地方法栈与 Java 虚拟机栈的作用类似,不同的是 Java 虚拟机栈存储的是 Java 方法的栈帧信息,而本地方法栈存储的是本地方法的栈帧信息。
本地方法栈的大小可以通过 -Xss 参数来指定,和 Java 虚拟机栈一样,本地方法栈也是线程私有的,每个线程都有自己的本地方法栈,当线程数量过多或本地方法栈过大时,也可能会导致内存溢出异常。
(4)为什么需要设计本地方法栈(Native Method Stack)?
本地方法栈(Native Stack)专门用于执行本地方法。本地方法是用其他语言(如C、C++)实现的Java方法,它们不是Java字节码,因此不能像Java方法一样直接被Java虚拟机执行。当Java代码调用本地方法时,Java虚拟机会将控制转移到本地方法,这时就需要使用本地方法栈。
本地方法栈的作用是为本地方法提供操作栈的支持,这个操作栈与Java虚拟机栈的操作栈是类似的,但是它是为本地方法服务的,它与Java虚拟机栈的操作栈相对独立。本地方法栈的栈帧和Java虚拟机栈的栈帧是不同的,它们的栈帧结构也不同。
设计本地方法栈的原因是Java虚拟机要支持Java语言与其他语言的互操作性。Java语言虽然功能强大,但有些操作用Java语言实现比较困难,因此可以使用其他语言来实现某些功能,这时就需要使用本地方法来调用其他语言实现的方法。而本地方法栈就是为这种需求而设计的。
(5)虚拟机栈和本地方法栈的合并
在JDK 7及以前的版本中,Java虚拟机栈和本地方法栈是分开的。但在JDK 8及以后的版本中,Java虚拟机栈和本地方法栈已经被合并,成为了一个线程私有的虚拟机栈。这样做的好处有以下几点:
1)线程更加轻量级
Java虚拟机栈和本地方法栈的独立实现,会对虚拟机的内存空间造成较大的浪费。合并之后,可以避免重复的堆栈空间占用,从而减小了每个线程的内存占用,使得线程更加轻量级。
2)程序执行效率更高
线程栈是程序执行的重要组成部分,栈的大小与程序执行的效率密切相关。如果栈空间过小,可能导致程序执行时抛出栈溢出异常。如果栈空间过大,会导致内存的浪费。Java虚拟机栈和本地方法栈合并后,可以根据程序需要动态地分配栈空间大小,从而更加合理地利用内存资源,提高程序的执行效率。
3)程序调试更加方便
在Java虚拟机栈和本地方法栈分离的情况下,如果程序在本地方法中出现错误,会导致调试变得困难。因为本地方法中的栈信息无法在Java虚拟机中被捕获。合并后,可以方便地进行调试,同时也可以减少错误的出现。
4)减少内存泄漏风险
Java虚拟机栈和本地方法栈的独立实现可能会导致内存泄漏的风险。因为在本地方法中可能会申请一些本地资源,这些资源在方法执行完毕之后可能没有被及时释放。在合并之后,可以更加方便地管理本地资源,从而减少内存泄漏的风险。
总之,将Java虚拟机栈和本地方法栈合并可以减少内存空间的浪费,提高程序的执行效率和调试的方便性,同时也可以减少内存泄漏的风险。
3、Java 堆(Java Heap)
Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块,用于存储 Java 对象。
Java 堆可以被所有线程共享,因此 Java 堆的访问需要进行同步处理。Java 堆被分为两个区域:新生代和老年代。
新生代又被分为 Eden 区和两个 Survivor 区。当一个对象被创建时,它被分配在 Eden 区。当 Eden 区满时,所有存活的对象会被复制到其中一个 Survivor 区,而 Eden 区中所有的对象会被清空。这样,每次清理后,只有一部分对象存活。当某个 Survivor 区满时,其中所有存活的对象会被复制到另一个 Survivor 区,而该 Survivor 区中的对象也会被清空。这个过程被称为 Minor GC。
老年代用于存储生命周期较长的对象。当老年代满时,会触发一次 Major GC,也就是 Full GC,这个过程会清理整个堆,而且耗费时间较长。
Java 堆的内存可以通过垃圾回收进行自动的回收和释放。因此,Java 堆的大小对于程序的性能和可靠性都有一定的影响,需要根据应用程序的需求进行适当的设置。
// Java 内存分配机制+垃圾回收机制
堆是Java内存区域中一块用来存放对象实例的区域。同时它也是GC所管理的主要区域,因此常被称为GC堆。
如何分配Java堆的内存大小?
Java堆内存大小是由JVM启动参数中的-Xms
和-Xmx
控制的。其中,-Xms
表示JVM启动时堆内存的初始大小,-Xmx
表示JVM允许堆内存达到的最大大小。这两个参数可以使用相同的值,也可以不同。
JVM启动时,会先将初始大小的内存分配给Java堆。如果应用程序需要更多的内存,JVM会自动扩展Java堆的大小,直到达到最大值为止。当JVM确定不再需要某些内存时,它会将这些内存释放回操作系统,以供其他应用程序使用。这个过程称为垃圾回收。
一般来说,我们需要根据应用程序的实际需求和硬件环境来设置-Xms
和-Xmx
参数。如果分配的内存过小,会导致频繁的垃圾回收和内存不足的错误;如果分配的内存过大,会浪费系统资源。
如果没有显式指定Java堆的大小,JVM会根据当前操作系统的物理内存大小以及JVM启动参数等因素进行自适应的分配。在JDK 8及以后版本中,JVM的默认堆大小取决于物理内存大小,一般为物理内存的1/4或1/3,但不超过最大堆大小(如果指定了)。同时,JVM还会根据具体应用程序的内存需求进行动态调整。
4、方法区(Method Area)
JVM 方法区(Method Area),也称为永久代(PermGen),是一种用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域。它属于 Java 堆的一部分,不过与 Java 堆的垃圾回收机制不同,方法区的垃圾回收主要针对常量池的回收和类的卸载。
方法区内存区域的大小是可以通过 JVM 启动参数进行调整的,如通过 -XX:PermSize
和 -XX:MaxPermSize
来调整永久代的大小。但是自从 Java 8 开始,永久代被移除了,取而代之的是元数据区(Metaspace)。元数据区不再是 JVM 堆内存的一部分,而是直接使用本地内存。
需要注意的是,方法区和 Java 堆虽然都属于 JVM 内存的一部分,但是它们存储的数据内容和生命周期是不同的,因此它们之间并没有直接的联系。方法区主要存储类信息、常量、静态变量等数据,而 Java 堆主要存储对象实例。
// 顾名思义,所谓永久代,就是存储那些不怎么变化的数据(永久),如类信息、常量、静态变量、编译后的代码等这些都是程序执行过程中不易变化的数据。
(1)方法区有什么作用?
JVM方法区是用来存储类的信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域。它的作用主要有以下几个:
- 存储类信息:类的生命周期包括加载、验证、准备、解析、初始化、使用和卸载等阶段,其中加载、验证、准备和解析都需要用到方法区,因为这些阶段都涉及到类信息的处理和存储。
- 存储静态变量和常量:静态变量和常量都属于类级别的变量,因此需要存储在方法区中。当类被加载后,其中的静态变量和常量就可以被访问和使用了。
- 存储即时编译器编译后的代码:即时编译器可以将代码编译成机器码,这些编译后的代码会被存储在方法区中,以便下次执行时直接使用,提高程序的运行效率。
- 存储运行时常量池:运行时常量池是方法区的一部分,用于存储编译时生成的各种字面量和符号引用。在类加载后,将字节码文件中的常量池加载到运行时常量池中,并且可以在运行期动态地添加常量。
因此,可以看出方法区是JVM中非常重要的一个内存区域,它存储了很多与类相关的信息,是JVM正常运行所必需的。
(2)JVM 方法区和 Java 堆的区别
内存分配:Java 堆用于存储对象实例,它的大小可以通过命令行参数或者默认值来指定,并且可以动态调整大小。而 JVM 方法区则用于存储类信息、常量、静态变量等,也可以通过命令行参数或者默认值来指定大小,但是一般情况下不会动态调整大小。
存储内容:Java 堆存储的是对象实例,对象实例包括对象头和实例数据两部分。而 JVM 方法区存储的是类信息、常量、静态变量等,也就是说,方法区存储的是和类相关的数据,不包括实例数据。
管理方式:Java 堆的内存空间由垃圾回收器进行管理和回收,其管理方式主要是根据对象是否被引用来判断是否需要回收。而 JVM 方法区则没有提供垃圾回收机制,但是对于已经被加载的类信息,如果其对应的 ClassLoader 实例已经被回收,那么这些类信息也会被卸载,释放方法区内存。
总之,JVM 方法区和 Java 堆在内存分配、存储的内容以及管理方式等方面存在一些不同。两者都是 Java 虚拟机的重要组成部分,各自承担着不同的任务,合理利用和管理这两部分内存可以有效地提高 Java 程序的性能和稳定性。
// JVM 方法区与 Java 堆的区别在于,方法区存储的是类的结构信息,而 Java 堆存储的是类的对象实例。
(3)Java 方法区对Java性能的影响有哪些?
在应用程序启动时,JVM 需要将类加载到方法区中,这个过程需要消耗一定的时间和内存。同时,方法区也需要进行垃圾回收,清理不再使用的类和元信息,这个过程也会消耗一定的 CPU 时间和内存。
另外,由于方法区是各个线程共享的,因此在多线程环境下,线程需要在方法区中获取类的元信息,这个过程也可能会产生一些竞争和同步开销,影响程序的性能。
在 JDK 8 及之前的版本中,方法区的实现使用永久代(Permanent Generation),这个实现会存在一些性能问题,例如内存泄漏等。在 JDK 8 之后,方法区的实现改为了 Metaspace,这个实现采用了更为灵活的内存管理方式,能够更好地避免内存泄漏等问题,从而提升程序的性能。
(4)什么是元空间(Metaspace)?
// 动态扩展方法区内存,解决方法区的内存溢出问题
Metaspace,也称为元空间,是JDK 8中引入的新的永久代实现方式。它是一块虚拟内存区域,用于存储类元数据,如类名、访问修饰符、字段、方法等信息。
在JDK 7及以前的版本中,永久代是方法区的一种实现方式,用于存储JVM加载的类信息,如常量池、字段、方法、构造函数等。但是永久代在运行时容易发生内存溢出等问题。
为了解决这些问题,JDK 8中引入了Metaspace,它是在本地内存中实现的,并且没有预定义的内存大小限制。Metaspace可以动态地调整大小,随着应用程序的需求而增长或缩小。它的大小只受本地内存的限制。
Metaspace采用了和Java堆类似的垃圾回收方式,使用的是基于垃圾回收器的标记-清除算法和压缩算法。(注:元空间的内存回收由操作系统进行,不需要进行 JVM 垃圾回收,降低了 GC 的负担)
(5)方法区和元空间的内存分配
在JDK7及之前,JVM 方法区是使用永久代来实现的,可以通过-XX:MaxPermSize
参数来设置其最大内存大小,默认大小为64MB。
在JDK8之后,永久代被移除,使用元空间(Metaspace)来实现JVM 方法区,可以通过-XX:MaxMetaspaceSize
参数来设置其最大内存大小,默认大小为无限制。
元空间(Metaspace)的默认大小取决于具体的JVM实现和系统配置,通常会根据JVM的最大堆大小和物理内存大小进行动态调整。在JDK 8及以后的版本中,元空间不再具有固定的最大大小限制,而是可以根据需要动态增长,直到达到了系统的物理内存限制。因此,元空间的默认大小取决于应用程序的需求和系统资源的可用性。
// 因为元空间可以动态的来调整内存大小,所以可以不指定具体值,不过极端情况下仍然可能需要进行JVM调优,与Java堆类似。
(6)Java 方法区的类信息是怎样进行垃圾回收的?
在 Java 8 及以前的版本中,JVM 方法区采用的是永久代作为实现,而永久代使用的垃圾回收器是 CMS(Concurrent Mark and Sweep)和 Serial GC。永久代的垃圾回收会在 Full GC 时才会进行,但是对于无用的类信息却不一定会被回收,容易导致永久代的溢出。
为了解决这个问题,JVM 在 Java 8 中引入了 Metaspace,Metaspace 将类信息存储在本地内存中,而不是 JVM 中的永久代。这样,元数据的垃圾回收就交由操作系统进行,避免了永久代的内存溢出问题。
具体来说,当一个类加载器加载了一个类,JVM 会将该类的元数据存储在 Metaspace 中。当这个类被卸载时,对应的元数据也会被从 Metaspace 中卸载。Metaspace 的垃圾回收是基于标记-清除算法实现的,具体过程如下:
- 标记所有活跃的类和类加载器;
- 清除所有没有标记的类和类加载器占用的内存空间。
Metaspace 的大小不是固定的,它会根据需要动态地分配和释放内存空间。默认情况下,Metaspace 的最大空间是不限制的,当需要使用的时候可以动态地分配内存空间。但是,为了防止 Metaspace 太过于膨胀,可以通过 JVM 参数来限制其最大大小。比如,可以使用 -XX:MaxMetaspaceSize
参数来设置 Metaspace 的最大大小。