Java--基础知识之JVM

一、什么是JVM
1、概念
JVM,即Java Virtual Machine(Java虚拟机),是Java和的核心和基础,是在Java编译器和操作系统平台间的虚拟处理器。JVM是利用软件方法实现的抽象的、计算机基于下层的操作系统和硬件平台可以在上面执行Java程序的字节码程序。
2、特点
JVM有完善的硬件架构(如处理器、堆栈、寄存器),其存在是为了支持与操作系统无关,实现Java跨平台。
3、Java的跨平台性
真正跨平台的是Java程序而非JVM。不同平台下安装了不同版本的JVM。编写的Java源码在编译后生成class文件(字节码文件),JVM是负责将这些字节码文件翻译成特定平台下的机器码然后运行,即在不同平台下安装对应的JVM,就可以运行编写的Java程序。而这个过程中Java程序没有做任何改变,只是通过JVM在不同平台上运行罢了,可以说是“一次编译,多处运行”。
4、启动与消亡
JVM负责运行一个Java程序,当启动一个Java程序时,也产生一个虚拟机实例,当程序关闭时这个虚拟机实例也消亡。
JVM运行起点:Java虚拟机实例通过调用某个初始类的main方法来运行Java程序,这个main方法是共有的、静态的、返回值为void类型,并传入一个字符串数组作为参数。
5、两种线程
(1)守护线程:通常由虚拟机自己使用,比如执行垃圾收集任务的线程,此外Java程序也可以把创建的线程标记为守护线程。
(2)非守护线程:比如Java程序中main()的线程,只要有任何非守护线程在运行,则Java程序也在继续运行,当其中所有非守护线终止时,虚拟机实例终止。此外,若安全管理器允许也可以通过调用Runtime.exit()或System.exit()来退出程序。
二、JVM、JRE与JDK
1、JRE
Java Runtime Environment(Java运行环境),是Java平台,所有的Java程序在JRE下运行。
2、JDK
Java Development Kit(Java开发工具包),用于编译和调试Java程序,JDK工具也是Java程序,也需要在JRE下运行。因此为了保证JDK的独立与完整性,在安装JDK时也会安装JRE,即JDK的安装目录下会有JRE目录来存放JRE文件。
3、关系简图
Java--基础知识之JVM
三、JVM三个主要的子系统
1、类加载器子系统(ClassLoader Subsystem)
Java的dynamic class loading功能由ClassLoade子系统处理,类加载器子系统负责加载和链接。在运行时(不是编译时)、首次引用类时初始化类。
(1)加载
把class字节码文件从各个来源通过类加载器装载入内存中。
注:字节码来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译。
(2)校验
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全,包括文件格式、元数据、字节码、符号引用的验证。主要是为了保证加载进来的字节流符合虚拟机规范,不造成安全问题。
(3)准备
为类变量分配内存,并将其初始化为默认值。此时为默认值,在初始化的时候才会给变量赋值,即在方法区中分配这些变量所使用的内存空间。主要是为类变量分配内存、赋初值,不是实例变量。
注:初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值(比如基本类型的初值默认0,引用类型null,常量的初值为代码中设置的值等)。
(4)解析
把类型中的符号引用转换为直接引用(虚拟机把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用),解析包括类或接口、字段、类方法、接口方法的解析。
注:①符号引用:即一个字符串,这个字符串给出了一些能够唯一识别一个方法、一个变量、一个类的相关信息;②直接引用:一个内存地址或一个偏移量。
举例:调用方法test(),这个方法的地址是12345,test是符号引用,12345是直接引用。
(5)初始化
是对类变量初始化,是执行类构造器的过程。只对static修饰的变量或语句进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
以下情况会主动初始化:
①使用new关键字创建对象会对类初始化(这个类未被初始化过)
②初始化类的时候,如果其父类没有被初始化过,则先初始化父类
③进行反射调用的时候
④虚拟机启动时先初始化main方法所在类
⑤调用类的静态属性或静态方法,或给类的静态属性赋值时
以下情况不进行初始化:
①如果已经初始化就不再进行初始化(同一个类加载器下面只能初始化类一次)
②编译时能确定下来的静态变量,不会对类进行初始化,例如final修饰的静态变量
总结:
(1)每个类加载器都维护了一份自己的名称空间,同一个名称空间里不能出现两个同名的类
(2)双亲委派机制
如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载。
作用:避免重复加载,避免核心API被篡改,保证java平台的安全。
举例:可以自己写String类吗?不可以,类加载器有加载顺序,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个加载器已加载就视为已加载此类,保证此类所有ClassLoader加载一次。而加载的顺序是自顶向下,因此自己写的String是被Bootstrap ClassLoader加载了,所以Apps ClassLoader就不会再去加载自己写的String类了。
2、运行时数据区(Runtime Data Area)
(1)Method Area(方法区)
所有类级数据都将存储在这里,包括静态变量。每个JVM只有一个方法区域,它是一个共享资源。
(2)Heap Area(堆区)
所有对象及其对应的实例变量和数组都将存储在这里,每个JVM也只有一个堆区域。由于方法和堆区域为多个线程共享内存,因此存储的数据不是线程安全的。
(3)Stack Area(栈区)
对于每个线程,将创建一个单独的运行时堆栈;对于每个方法调用,都会在堆栈内存中生成一个条目,称为 Stack Frame所有本地变量都将在堆栈内存中创建,堆栈区域是线程安全的,因为它不是共享资源。
Jvm对该区域规范了两种异常:
①线程请求的栈深度大于虚拟机栈所允许的深度,将抛出*Error。
②若虚拟机栈可动态扩展,当无法申请到足够内存空间时将抛出OutOfMemoryError。
堆栈框架分为三个子实体:
Local Variable Array:方法的局部变量及相应的值存储在这里
Operand stack:如果需要执行任何中间操作,操作数堆栈充当运行时工作区来执行操作
Frame data:与方法对应的所有符号都存储在这里。在任何异常情况下,catch块信息都将保存在frame data中
(4)PC Registers(PC寄存器)
每个线程将有单独的PC寄存器,以保持当前执行指令的地址一旦指令执行,PC寄存器将更新与下一条指令。
(5)Native Method stacks(本地方法栈)
本地方法栈保存本地方法的信息。为每一个线程,将创建一个单独的本地方法栈。
3、执行引擎(Execution Engine)
分配给运行时数据区域的字节码将由执行引擎执行,执行引擎读取字节码并逐个执行。
(1)Interpreter(解释器)
解释器可以快速地解释字节码,但执行速度很慢。解释器的缺点是,当一个方法被多次调用时,每次都需要一个新的解释。
(2)JIT Compiler(编译器)
JIT编译器消除了解释器的缺点。执行引擎将在转换字节码时使用解释器的帮助,但是当它发现重复的代码时,它使用JIT编译器,JIT编译整个字节码并将其更改为本机代码。此本机代码将直接用于重复的方法调用,从而提高系统的性能。
JIT构成组件有:
Intermediate Code Generator:生成中间代码
Code Optimizer:负责优化生成的中间代码
Target Code Generator:负责生成机器代码/本机代码
Profiler – 特殊的组件,负责寻找 hotspots,即方法是否被多次调用
(3)Garbage Collector(垃圾回收)
收集和删除未引用的对象。可以通过调用System.gc()触发垃圾收集,但不能保证执行。JVM的垃圾收集收集创建的对象。
GC 分为两种:
①Minor GC:新生代(Young Gen)空间不足时触发收集,由于Java 中的大部分对象通常不需长久存活,新生代是GC收集频繁区域,所以采用复制算法。
注:复制算法
把内存空间划为两个相等的区域,每次只使用其中一个区域。GC时遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理。
不足之处:内存利用率问题;在对象存活率较高时,其效率会变低。
②Full GC:老年代(Old Gen )空间不足或元空间达到高水位线执行收集动作,由于存放大对象及长久存活下的对象,占用内存空间大,回收效率低,所以采用标记-清除算法。
注:标记-清除算法
分为两阶段:标记和清除。首先标记出哪些对象可被回收,在标记完成之后统一回收所有被标记的对象所占用的内存空间。
不足之处:无法处理循环引用的问题;效率不高;产生大量内存碎片,可能导致以后在分配大对象的时候而无法申请到足够的连续内存空间,导致提前触发新一轮GC。