一文入门jvm虚拟机
让我们每天「博学」一点点
一文带你理解JVM
1、jdk、jre、jvm的区别与联系
jdk的全称是Java Development kit(java开发工具包),我们可以把程序设计语言、java虚拟机、java类库这三部分统称为jdk,jdk是用于支持java程序开发的最小环境。Developer可以很容易的使用里面的方法以减少代码量,里面同时包含jre和一些开发的小工具(如编译工具javac),同时包含了jre。
jre的全称是Java Running Environment(java运行时环境 ),可以把java类库API中的javaSE的API子集和java虚拟机这两部分统称为JRE,JRE是支持java程序运行的标准环境。
jvm的全称java virtual machine(java 虚拟机),它只认识XXX.class文件,虚拟机可以识别这种文件的字节码指令并调用操作系统上的API,正是这个原因,java才可以跨平台使用。
2、代码是如何执行的
jvm是一个软件,它帮我们屏蔽了底层的操作系统、硬件、CPU指令层的细节
它的口号是Write Once,Run Everywhere.我们来看一下代码的执行流程。
图中的Test.java文件是按照java语法规则编写的源文件,是一种高级语言,.java文件经javac编译后就生成字节码文件,字节码文件是用于给java虚拟机执行用的,该文件的格式规范受到java虚拟机的定义。而jvm的目的就是将字节码文件Test.class翻译为操作系统及硬件的指令,便于在不同的操作系统上执行。
NOTE:jvm虚拟机并不是仅仅只针对java语言,像一些其它编程语言如Groovy、Scala和Kotlin也可以在jvm虚拟机上运行上,这些语言仅仅需要实现一个编译器,通过该编译器把源代码文件编译成JVM能识别的字节码文件即可。
3、JVM的内存结构
3.1类加载子系统
在java虚拟机中,负责查找并装载类的部分称为装载子系统,装载子系统用于定位和加载译码后的class文件。在加载阶段,虚拟机需要完成以下事情
-
通过一个类的全限定名来获取定义此类的二进制字节流 -
将这个字节流所代表的静态存储结构转化为元空间中运行时的数据结构 -
在内存中生成一个代表这个类的java.lang.Class对象,作为方法这个类的各种数据访问入口
加载过程下图所示
类的加载是通过查询路径的方式进行的,加载阶段既可以使用虚拟机里内置的引导类加载器来完成,也可以由用户自定义类加载器来完成。其加载顺序如下
1、Bootstrap ClassLoader:启动类加载器,加载存放在<JAVA_HOME>\lib目录,或者被Xbootclasspath选项指定的jar包,如rt.jar、tools.jar
2、Extension ClassLoader:扩展类加载器,加载<JAVA_HOME>\lib\ext*.jar或者-java.ext.dirs指定目录下的jar包
3、AppClassLoader:应用程序类加载器,加载Classpath或java.class.path所指定的目录下的类和jar包
4、Custom ClassLoader:通过java.lang.ClassLoader的子类自定义加载class
实际上,上面描述的仅仅是类加载过程中的加载过程,类加载的整个过程包括:加载、验证、准备、解析和初始化
字节码--->加载--->验证--->准备--->解析--->初始化,其中验证、准备和解析阶段可以统称为链接阶段。
下面我们讲解每个阶段的作用
-
验证:验证是链接阶段的第一步,这一阶段的目的是确保Class文件的字节流包含的信息符合《java虚拟机规范》的全部约束要求,确保这些信息被当做代码运行后不会危害虚拟机自身的安全 -
准备:正式为类中定义的变量(静态变量)分配内存并设置类变量初始值阶段。 -
解析:java虚拟机将常量池(元数据区的一部分)内的符号引用替换为直接引用过程 -
初始化:类的初始化是类加载过程的最后一步,它的作用是真正开始执行类中编写的java程序代码
类加载会将类的信息加入到元数据空间。
如果一个类型从被加载到虚拟机内存开始,到出卸载为止,它的整个生命周期将在类加载的基础上增加使用和卸载阶段
3.2jvm内存部分(运行时数据区)
jvm在运行时会把它所管理的内存划分为若干不同的数据区域,宏观上可以划分为两部分
1、线程私有数据区(3个部分)
-
程序计数器
①程序计数器是一块内存较小的空间,它可以看做是当前线程执行的字节码的行号指示器
②它是程序控制流的指示器,分支、循环、跳转、异常处理、多线程恢复等基础功能都需要依赖这个计数器来完成
③线程私有,各条线程之间计数器互不影响,独立存储,
④随着线程的结束而结束,不需要垃圾回收
⑤不会出现OutOfMemoryError
-
虚拟机栈
与程序计数器一样,java虚拟机栈也是线程私有的,不会被GC回收,它的生命周期与线程相同,java虚拟机栈描述的是java方法执行线程的内存模型:每个方法被执行的时候(一个方法对应着一个栈帧),java虚拟机栈都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。当栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果java虚拟机的容量可以动态扩展,当栈扩展时无法申请到足够的内存时将会抛出OutOfMemoryError异常。栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放在堆上。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程。
②操作栈:操作栈也被称为操作数栈,它是一个后入先出的栈。当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作,一个完整的方法执行期间往往包含多个这样入栈和出栈的过程。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。
-
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别在于虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError。线程私有,不会被GC回收。
有的虚拟机直接将虚拟机栈和本地方法栈合二为一,不在单独考虑。
总结:线程私有的三个部分都是随着线程执行结束而结束(JVM就销毁了虚拟机栈里面的栈帧)。
2、线程公有数据区(2个部分)
-
元空间
在jdk1.8之前,元空间所在的区域被称为方法区,方法区在jdk1.7时合并到了堆。
jdk1.8时,方法区所在的区域被称为元空间,但是1.8仍然保留着方法区的概念,只不过实现方式不同,元空间与堆不相连,但与堆共享物理内存,逻辑上可以认为在堆中。
元空间的特点
①线程共享
②存储类信息、常量、运行时常量池、静态变量、即时编译器编译后的代码等数据
③在jdk1.7之前,在HotSpot虚拟机上将方法区成为永久代,在jdk1.8时,完全放弃了永久代,改用了元空间
④因为效率问题,无垃圾回收
⑤空间不够时,OutOfMemoryError
⑥设置元空间的大小--XX:Metaspace=10M-XX:MetaspaceSize=10M
运行时常量池的特点
运行时常量池是元空间的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量的符号引用,这部分内容将在类加载后存放到元空间的运行时常量池中。
总结
jdk1.6及之前:有永久代,常量池在方法区
jdk1.7:从某个版本开始去除永久代,常量池1.7放入堆中
jdk1.8及之后:无永久代,常量池1.8在元空间。
-
堆
java堆(heap)是虚拟机所管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例和数组都在堆上分配。
java堆是垃圾收集器管理的内存区域,从回收的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以java堆可以分为新生代和老年代,新生代又可以分为Eden空间、From Survivor空间和To Survivor空间,无论怎样划分,都是为了更好的进行垃圾回收。
java堆可以被实现成固定大小的,也可以是扩展的(通过-Xmx和-Xms设定)。如果在java堆中没有内存完成实例分配,并且堆无法在扩展时,java虚拟机将会抛出OutOfMemory异常。
在堆中加载实例对象的顺序
当老年代空间满时,将会抛出OutOfMemory异常。
java堆溢出
不断创建对象又不释放,当对象到达一定数量时,无堆空间时将会产生堆溢出
内存泄漏:GC Roots到对象之间有可达路径却无法回收(存在对象引用,却没有释放)
内存溢出:内存溢出是指应用系统中存在无法回收的内存或使用内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。在Java虚拟机中,GC Roots到对象之间无可达路径,可以被收集,但对象还存活着,此时可以根据物理机内存适当的调大虚拟机参数-Xms、-Xmx,分析代码是否对象生命周期过长。对象是否持有状态时间过长。
参考文献
[1]周志明.深入理解java虚拟机
[2]https://www.bilibili.com/video/BV13J411n72m?from=search&seid=7875422419866373500
看了这篇文章,你是否「博学」了点个「在看」,是对我最大的鼓励!