0
点赞
收藏
分享

微信扫一扫

Java虚拟机(JVM)

奋斗De奶爸 2022-02-20 阅读 186

JVM

参考资料:《深入理解Java虚拟机》周志明著

JVM就是Java虚拟机,在学习Java虚拟机时,我们可以先着重的学习两个方面,分别是Java虚拟机的内存的各个区域和一个类文件加载与执行。在了解这些之后,我们还可以进一步学习JVM的调优以及一些命令和工具的使用。这篇文章主要介绍的是前两者也仅介绍前两者的内容。我在介绍的同时希望读者可以跟着我的思路一点一点的扩散,但同时也不要忘记整体的结构。

JVM的内存区域

Java虚拟机中主要的内存区域有程序计数器、虚拟机栈、方法区、堆、本地方法栈。这些区域各自执行着自己的使命,但声明一点,JVM是一种规范,这些区域只是划分出来的,是一个概念上的模型。

img

程序计数器

这是一块非常小的内存,它的作用主要就是用来存放下一条需要执行的字节码指令。每个线程都会有一个独立的程序计数器,即线程私有。因为这个程序计数器只会存放一条字节码指令,所以不会出现内存溢出的异常。这里我们先将字节码指令简单的看作是Java成底层实现的最小指令,后面的介绍也是通用的。

Java虚拟机栈

它也是线程私有的,它的主要作用是描述Java方法执行的线程内存模型。每当执行一个方法时,就会生成一个对应的栈帧,通过栈帧入栈和出栈的过程来描述一个方法执行的开始和结束。当一个虚拟机栈超出了规定的最大深度,就会出现StackOverflowError异常,但如果支持动态扩容的话,当没有足够的内存时,就会出现OutOfMemoryError异常。这里又多出来了一种结构,栈帧。它是用来保存一个方法所需要的一些相关信息,这些信息包括局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

局部变量表是以变量槽为最小单位来存放一个方法的参数和方法中定义的局部变量,《Java虚拟机规范》对于一个变量槽的具体大小并没有明确的规定,而调用局部变量表中的内容主要是使用索引定位的方式。操作数栈就是存储各种字节码指令需要写入和提取的内容,比如在执行一个整数加法的时候,会先依次向操作数栈写入两个整数,然后在当遇到加法这个指令的时候就会将栈顶的两个数提取出来并相加,再将结果放回操作数栈中。动态连接存放的就是一个引用,用来指向运行时常量池中该栈帧所属方法。方法返回地址是用来存放一个方法返回值的地址,一般是PC计数器的值,但也只是在正常退出的情况下,如果是异常退出,就需要交给异常处理器表来确定。附加信息就是一些没有在《Java虚拟机规范》中描述的信息,一般会和动态连接、方法返回地址归为一类,称之为栈帧信息。

本地方法栈

与Java虚拟机的功能相类似,不同的是本地方法栈执行的方法是本地服务,同样的也会出现StackOverflowError和OutOfMemoryError的异常。有的Java虚拟机甚至将这两个栈合二为一。

Java堆

这是Java虚拟机管理中的最大的一块内存,它的作用就是用来存放对象实例的,几乎所有的对象实例都在这里存放,它是所有线程共有的,但是为了提升对象分配的效率,Java堆也为每个线程划分出了多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。于此同时堆也是垃圾回收的重点对象,为了方便回收,又将堆进行了更加细致的划分,就有了新生代、老年代、永久代、伊甸园区、幸存者区等概念。这里就先不过多介绍了,会与后面的垃圾回收一同讲解。

方法区

方法区是线程共有的,主要是用来存放被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码缓存等数据。但是《Java虚拟机规范》中并没有对方法去进行严格的规定,导致不同的虚拟机其实出现了较大的差异。拿HotSpot虚拟机为例,在JDK8以前,方法区的落地实现称之为永久代,因为这种实现方法的种种缺点,在JDK7的时候就已经做出了改变,将运行时常量池和静态变量等移除,到了JDK8,就完全放弃了永久代的概念,改为了像JRockit和J9一样在本地内存中实现的元空间,并将JDK7中永久代剩余的内容移到了元空间内。

这里还提到了另一种结构,运行时常量池,它的作用主要是用来存放编译期生成的各种字面量和符号引用。因为运行时常量池也是方法区中的一部分,自然而然,方法区也存在OutOfMemoryError的异常。

垃圾回收

垃圾回收简单看来就是先判断某个对象是否需要回收,然后又该如何回收。回想一下前面所提到的几个内存区域,你会发现,其实垃圾回收真正需要大动干戈的区域是堆。首先我会介绍如何判断一个对象的死活,然后简单会介绍几种垃圾回收算法,最后介绍堆的更细致的划分。

判断一个对象的死活
  • 引用计数算法:对于一个对象添加一个计数器,每当一个引用指向它的时,计数器的值就加1,当引用失效的时候,计数器的值就会减1,当计数器的值为零的时候就意味着该对象是失效的,需要被回收。
  • 可达性分析算法:先选出一些对象作为根对象,对于每个根对象的引用关系向下搜索,搜索的路径称之为引用链。如果某一个对象没有在任何一条引用链上,则这个对象就需要被回收。
垃圾收集算法
  • 标记-清除算法:首先标记出哪些对象是活着的,或者标记出哪些是死去的。然后再根据标记清除掉已经死掉的对象。
  • 标记-复制算法:首先将内存分为等大的两部分,然后按照标记-清除算法的方式辨别出活着的对象,最后将活着的对象依次整齐的从内存的一部分复制到另一部分。
  • 标记-整理算法:与标记-复制的思路相差不多,区别在于没有将内存分为两部分,而是采用直接整理的方式,使得最后的结果是整齐的。
堆的详细解析

首先将堆划分为老年代和新生代,对于新生代,我们又划分为伊甸园区和幸存者区,幸存者区是有幸存者from和to两个区域。

一开始对象刚创建的时候会将其放在伊甸园区,当内存不足的时候就会触发垃圾回收,然后将活下来的对象放在幸存者区,当幸存者区内存不够的时候又会触发一次垃圾回收,然后将活下来的对象从from区放到to区,经历了多次垃圾回收之后还在幸存者区的话,就会把它放在老年代里。这也就是分代回收的体现。根据不同区域对象的特点,我们也可以选择适当的垃圾回收算法,以提高垃圾回收的效率。
在这里插入图片描述

类加载过程

Java虚拟机会把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被Java虚拟机直接使用的Java类型,执行的过程会交给执行引擎。一个完整的类加载过程包括加载、验证、准备、解析、初始化,其中验证、解析、初始化三个部分统称为连接。
在这里插入图片描述

加载

先通过一个类的全限定名来获取定义此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。但是《Java虚拟机规范》对上面的内容并没做出具体的要求,所以在落地实现过程中就有着很大的灵活性。

验证

这个阶段主要是用来确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求。主要包括以下几种验证:

  • 文件格式验证:验证字节流是否符合Class文件格式规范并能被当前版本的虚拟机处理
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
  • 字节码验证:主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
  • 符号引用验证:就是对类自身以外的各类信息进行匹配性校验,主要是为了确保解析行为能正常进行
准备

准备阶段是正式为类中定义的变量(即静态变量)分配内存并设置类变量初始值的阶段

解析

解析阶段就是将常量池内的符号引用替换为直接引用的过程。符号引用就是用一组符号来描述所引用的目标。而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

初始化

引用的过程。符号引用就是用一组符号来描述所引用的目标。而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

初始化

初始化阶段则是执行()方法的过程,而这个方法是Javac编译器的产物,它会搜索代码中的类的初始化代码,然后对类进行初始化。

举报

相关推荐

java虚拟机(JVM)

java虚拟机 (JVM)

JVM(java虚拟机)

JVM(Java虚拟机)

jvm虚拟机

JVM虚拟机

0 条评论