从这篇文章开始,我会一直更新这个系列,核心就是面试常用的Java八股系列,我最近在准备面试,阅读了很多资料,这个总结系列要感谢很多人,我觉得非常有必要在这个系列最开始,先感谢这些技术前辈给我带来的帮助。包括JavaGuide,Java-Interview和程序员囧辉的文章面试必问的 JVM 运行时数据区,你懂了吗?。
当然还有很多其他的文章,就不一一列举了,总之感谢这些技术前辈对我的帮助。
当然算法系列我也会更下去,我也一直在学习算法,但是最近太忙了,可能会更的晚一些。我希望我还是能够坚持把Java八股系列和算法系列更完,当然,这有可能是我的幻想。
JVM(Java Virtual Machine)Java 虚拟机,负责将字节码转换为机器码并执行。JVM 由以下三部分组成:
Java虚拟机(Java Virtual Machine,JVM)包含以下三个主要组成部分:
类加载器(ClassLoader)子系统:负责将字节码文件加载到内存中,并生成对应的 Class 对象。
运行时数据区(Runtime Data Area):Java虚拟机运行时用于存放数据的区域,包括:
方法区(Method Area):用于存放类的元数据,如类名、访问修饰符、常量池等信息。
堆内存(Heap):用于存放类的实例对象和数组对象实例。
虚拟机栈(Stack):用于存放线程的方法调用栈。
本地方法栈(Native Method Stack):用于存放Java虚拟机调用本地方法(Native Method)时的参数和返回值。
程序计数器(Program Counter Register):用于保存当前线程执行的字节码指令的地址。
Java 程序的执行过程:
Java 程序的执行过程包括编译、加载和执行三个主要步骤。首先,Java 编译器将 Java 程序源代码编译为字节码文件,
字节码文件中包含了 Java 程序的所有信息。然后,类加载器会将字节码文件加载到内存中,并为其分配内存空间,
在这个过程中进行类的加载、连接和初始化。最后,执行引擎将字节码文件转换为对应的机器码指令,
并在处理器上执行,从而实现 Java 程序的功能。
在执行过程中,类加载器和执行引擎是 Java 编译器和计算机硬件之间的中间件。类加载器将字节码文件转换成运行时数据结构,
包括程序代码、静态变量、类初始化代码等信息,为执行引擎提供了运行时环境。执行引擎负责将字节码文件转换为对应的机器码指令,
然后在处理器上执行该指令,最终实现 Java 程序的功能。
JVM 提供了跨平台的支持,使得同一个 Java 程序可以在不同的操作系统平台上运行,这也是 Java 广泛应用于互联网开发的一个重要因素。
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8 和之前的版本会有一些差异,下面会详细介绍。
线程私有的:
线程共享的:
程序计数器是一块较小的内存空间,用于保存当前线程执行的字节码指令的地址,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
值得一提的是程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
程序计数器主要作用:
Java虚拟机栈是线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法返回地址等信息。当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError。它的生命周期和线程的相同,随着线程的创建而创建,随着线程的结束而死亡。
Java 虚拟机栈是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过 Java 虚拟机栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
Java虚拟机栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是后进先出的数据结构,只支持出栈和入栈两种操作。
Java程序运行过程中栈可能会出现两种错误:
用于存储方法执行过程中的局部变量,包括基本数据类型和对象引用等。
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果和计算过程中产生的临时变量。
主要服务于一个方法需要调用其他方法的场景。在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在class文件常量池中,当一个方法需要调用其他方法时,需要将常量池中指向方法的符号引用转换为其在内存地址中的直接引用,动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
本地方法栈和虚拟机栈所发挥的作用非常相似,它们之间的区别是 Java 虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;新生代还可以分为Eden区和Survivor区(S0和S1)。
如果想要详细了解堆内存的结构,可以参考我的另一篇文章——JVM面试题详解系列——垃圾回收详解。
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区是Java虚拟机的一个模型规范,它的实现主要是永久代和元空间。在JDK1.7及之前,方法区的实现是永久代,JDK1.8及之后,方法区的实现是元空间,而且元空间使用的直接内存,而不是Java虚拟机运行时数据区域。
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
直接内存并不是Java虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 错误出现。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
Java中的Native方法指的是一种可以调用本地代码(即非Java代码,通常是C或C++代码)的Java方法。当Java应用程序需要使用更高层次的抽象层来操作底层的系统资源时,可以使用Native方法来调用由本地代码实现的底层操作系统级别的服务或库。这样就可以更加高效的完成一些特殊的任务,如操作系统调用、网络数据传输、文件I/O等操作。使用Native方法可以提高程序的性能和效率,但也会带来安全和可移植性方面的问题。为了使用Native方法,需要在Java方法中使用关键字native,并且要在本地库中实现该方法。本地库是一个动态链接库(.dll或.so文件),包含具有本地代码实现的方法。