JVM的基础概念
JVM即Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机。
JVM用于识别class文件,我们都知道Java的代码需要经过编译器,生成.class文件后,JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。
当前市面上使用范围最广的,是Sun/OracleJDK或者OpenJDK中默认的 HotSpot 虚拟机,这个一款由官方开发和主推的虚拟机,我们在网络上,或者购买的书籍中讲解应该都是这个虚拟机。
市面上还存在很多非常优秀的Java虚拟机,它们都遵循Java虚拟机规范,但具体的实现上各有特点,在某一个领域拥有突出的性能优势是它们最重要的卖点。例如IBM的J9,Azul Systems 的 Azul VM 等等。
Class文件
Class文件由Java编译器生成,我们创建的.Java文件在经过编译器后,会变成.Class的文件,这样才能被JVM所识别并运行。
Class文件的核心设计思想是 平台无关性,它存储的不是操作系统可以直接识别的二进制本地机器码,而是根据Java虚拟机规范所自定义的指令集、符号表和一些其他信息,所以只要任何一个操作系统下开发有对应的Java虚拟机,开发者的Java程序就能跑起来。
JVM的生命周期
JVM在Java程序开始运行的时候,它才运行,程序结束的时它就停止。
一个Java程序会开启一个JVM进程,如果一台机器上运行3个Java程序,那么就会有3个运行中的JVM进程。
JVM中的线程分为两种:守护线程和普通线程
守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。
普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。
结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
- 执行了System.exit() 方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而终止进程
- 由于操作系统出现错误而导致Java虚拟机进程终止
JDK和JRE的区别
JDK是面向开发人员使用的SDK,它提供了Java的开发环境和运行环境,JDK中包含了JRE。
JRE是Java的运行环境,是面向所有Java程序的使用者,包括开发者。
JVM是包含在JRE里面的。
JVM的结构体系
堆,栈,方法区
堆(JVM堆,java heap)
- 存储的是对象,每个对象都包含一个与之对应的class。
- JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
- 在程序运行中,可以动态的分配堆的内存大小。
- 堆的存取方式为管道类型,先进先出。
- 对象实体由垃圾回收器负责回收,因此大小和生命周期不需要确定。
虚拟机栈(JVM栈、VM Stack)
- 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象实体)。当一个线程创建运行的时候,与之对应的栈就创建了,每个栈中的数据都是私有的,其他线程不能访问。
- 每个线程都会建立一个栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,栈帧包含了三个部分:
-
-
-
- 局部变量区(方法内基本类型变量、对象实例的引用)
- 操作数栈区(存放方法执行过程中产生的中间结果)
- 运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)
-
-
- 数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失。
-->String、Integer、Byte、Short、Long、Boolean等等包装类型,它们是存放于堆中的
本地方法栈 (Native Method Stack)
- 本地方法栈的功能和JVM栈非常类似,区别在于虚拟机栈执行的是Java方法,本地方法栈执行的是本地(Native)方法服务,存储的也是本地方法的局部变量表,本地方法的操作数栈等信息。
- 栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
- 每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。
- 本地方法栈是在 程序调用 或 JVM调用 本地方法接口(Native) 时候启用。
- 本地方法都不是使用Java语言编写的,它们可能由C或其他语言编写,本地方法也不由JVM去运行,所以本地方法的运行不受JVM管理。
- HotSpot VM将本地方法栈和JVM栈合并了。
- 本地方法栈也会在深度溢出或扩展失败的时候,分别抛出StackOverflowError 和 OutOfMemoryError 异常。
方法区
方法区用于存储JVM加载完成的类型信息、常量、静态变量、即时编译器编译后的代码缓存,方法区和 Java 堆区一样,都是被所有的线程共享。
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
方法区也可以被垃圾回收,但条件非常严苛,必须在该类没有任何引用的情况下,详情见下:
java 中的基本数据类型
Java 中的基本数据类型只有8个:byte、short、int、long、float、double、charboolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
类加载子系统(类加载器)
类加载子系统也可以称之为类加载器,JVM默认提供三个类加载器:
1、Bootstrap ClassLoader :称之为启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等。
2、Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载(环境变量)$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
3、App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。
除了Java默认提供的三个加载器之外,我们还可以根据自身需求自定义ClassLoader,自定义的类加载器必须继承自 java.lang.ClassLoader 类。
除了 BootStrap ClassLoader 之外的两个默认加载器都是继承自 java.lang.ClassLoader ,BootStrap ClassLoader 不是一个普通的Java类,它底层由C++编写,已嵌入到了JVM的内核当中,当JVM启动后,BootStrap ClassLoader 也随之启动,负责加载完核心类库,并构造Extension ClassLoader 和App ClassLoader 类加载器。
程序计数器
- 在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器。
- 程序计数器仅占很小的一块内存空间。
- 当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。
- 程序计数器不会抛出 OutOfMemoryError(内存不足错误)。
JVM执行引擎
Java虚拟机相当于一台虚拟的“物理机”,这两种机器都有代码执行能力,区别主要是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而JVM的执行引擎是自己实现的,因此程序员可以自行制定指令集和执行引擎的结构体系。
执行引擎的主要职责,就是把这些自行制定的指令集翻译成硬件所支持的指令集格式,然后执行。
在JVM规范中制定了虚拟机字节码执行引擎的概念模型,这个模型称之为JVM执行引擎的统一外观,各个Java 虚拟机的发行厂商都需要按照这个规范来实现。
在不同的虚拟机实现中,可能会有两种的执行方式:解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码)。虚拟机可以按自身的需求,采用一种或同时采用多种组合的方式来实现执行引擎。但无论内部怎么实现,都要遵循JVM规范要求:
输入的是字节码文件
处理过程是等效字节码解析过程
输出的是执行结果
JVM GC(垃圾回收机制)
JVM GC只回收堆区和方法区内的基本类型数据和对象。栈区的数据(仅指基本类型数据),在超出作用域后会自动出栈释放掉,所以其不在JVM GC的管理范围内。
回收对象:对象没有引用了或者对象不可达
当内存空间不足时,会触发GC的执行
不同的垃圾回收器,会有不同的回收策略,但大致可以分为两类:
分代回收和局部回收两种策略