JVM(Java Virtual Machine)
JVM概述:
虚拟机
就是一台虚拟计算机,它是一款软件,用来执行虚拟的计算机指令。虚拟机可以分为两类:系统虚拟机和程序虚拟机。
VMware就属于系统虚拟机。物理的模拟了一个可以操作的操作系统平台。
Java虚拟机就是一个程序虚拟机。它是为执行某个单个程序的而设计的。Java虚拟机中执行的指令我们称之为Java字节码指令。Java虚拟机是执行Java字节码文件的,可以独立运行。Java虚拟机是Java技术的核心,因为所有的Java程序都是在Java虚拟机中运行。
JVM的作用
- 负责将字节码加载到内存中,也就是运行时数据区。
- 负责存储数据
- 负责将字节码翻译为机器码并执行指令
- 负责垃圾回收
JVM的特点
- 一次开发多处使用
- 自动内存管理
- 自动垃圾回收
JVM的结构(组成部分)
- 类加载器:负责从文件系统或者网络加载class文件。
- 运行时数据区:包括程序计数器、本地方法栈、Java虚拟机栈、堆、方法区。
- 执行引擎:把字节码翻译为机器码
- 本地方法接口:与本地方法库进行链接
- 垃圾回收:
程序在执行前需要将Java代码转换为字节码,JVM首先需要把字节码通过类加载器把文件加载到内存中的运行时数据区,字节码文件是JVM的一台指令集规范,并不能直接用底层操作系统去执行,因此需要特定的命令解析器 执行引擎,将字节码翻译为操作系统能读懂的机器码,然后交由CPU执行,执行时需要调用其他语言的接口 本地方法接口来实现整个程序的功能。
JVM结构——类加载器
主要负责加载class文件。好比一个快递员
加载过程分为三步:
- 加载
- 链接
- 初始化
加载
- 通过IO获取类的二进制字节流
- 将这个字节流所表示的静态存储结构转换为方法区的运行时结构
- 在内存中生成一个代表这个类的Class对象,作为这个类的各种数据的访问入口
链接
链接分为三步:验证、准备、解析
验证:
- 对字节码文件格式进行验证。class文件在开头都有特定的文件表示(如以CA FE BA BE表示开头)。
- 对字节码内容进行分析,确保符合Java语言规范。
准备:对类的静态属性分配内存,并设置默认值;不包含被final修饰的static常量,在编译时已经初始化
如:
public static int value = 12;//在准备阶段完成后value的值为0,不是12
解析:
将二进制字节码替换为地址引用。
初始化
类初始化,为类的静态变量赋值。如:上面的value在初始化后的值就是12。
类什么时候初始化(被加载)
- 通过new关键字创建对象。
- 访问类的静态变量或方法。
- 对类进行反射操作。
- 加载子类时导致父类被加载。
- 执行该类中的main方法。
注意:一下两种情况是不会加载类的:
-
引用该类的静态常量,不会导致类被加载,因为静态常量在编译期间就已经被初始化了,这里的常量是指字面上已经制定值得常量,而不是需要经过计算才能得到结果的常量。
public final static int a = 5 ;//不会导致类被加载, public final static int b = 5 ;//会导致类加载
-
构造某个类的数组时,该类不会被初始化。
User[] users = new User[10];
类加载的顺序
被static修饰的变量或语句进行赋值,如果同时存在多个静态常量和代码块时自上而下执行,如果初始化一个子类而父类还没有被加载时优先加载父类。
public class User {
static int num = 10;
static {
num = 5;
}
public static void main(String[] args) {
//num 从准备到初始化变化为num=0--> num=10 --> num=5
System.out.println(num);
}
}
public class User {
static {
num = 5;
}
static int num = 10;
public static void main(String[] args) {
//num 从准备到初始化变化为num=0--> num=5 --> num=10
System.out.println(num);
}
}
类加载器的分类
从JVM角度出发可以将类加载器分为两类:
- 引导类加载器。
- 其他所有类加载器,这类加载器有java语言编写,全部继承自抽象类java.lang.ClassLoader。
站在java开发人员的角度来看,类加载器应当分为四类:启动类加载器、扩展类加载器、应用类加载器、自定义类加载器。
启动类加载器(引导类加载器 BootStrap ClassLoader)
这个类加载器使用的是C/C++语言实现的,嵌套在JVM内部。不继承ava.lang.ClassLoader。处于顶层,没有父加载器。用来加载java核心类库。
扩展类加载器(Extension ClassLoader)
由Java语言编写,由sun.misc.Launcher$ExtClassLoader 实现。派生于ClassLoader类。
从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。
应用程序类加载器(系统类加载器 Application ClassLoader)
java语言编写,语言编写的,由 sun.misc.Launcher$AppClassLoader 实现。派生于ClassLoader类。
加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类。
双亲委派机制
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
工作原理:
- 如果一个类加载器接收到了类加载请求,它首先会把这个请求委托给它的父类加载器去执行。
- 如果父类还存在父类加载器,就会继续向上委托,直到顶层启动类加载器。
- 父级可以完成加载就返回,父级如果最终没有找到,就委派给子级加载器,如果都无法完成加载就会抛出ClassNotFoundException异常。
为什么要打破双亲委派机制
双亲委派机制是自底向上的委托,自顶向下的加载的过程。
这样做一个明显的目的就是为了保护java类库的安全性,不会被开发者覆盖。
如何的打破双亲委派机制
可以通过继承ClassLoader类,重写loadClass或者findClass方法,在loadClass方法中是实现双亲委派逻辑的地方,修改它会破会双亲委派机制不建议使用,建议使用重写findClass方法实现自定义的类加载器,典型的tomcat中,加载器部署在tomcat中的项目时,就使用的是自己的类加载器。
JVM运行时数据区
运行时数据区组成
- 程序计数器
- 本地方法栈
- java虚拟机栈
- 堆
- 方法区
1.程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,几乎可以忽略。
用来存储每个线程下一条指令的地址,也就是即将要执行的命令代码。
是线程私有的,每个线程都拥有一个线程计数器,生命周期与线程一致。
是运行时数据区中,唯一一个不会出现内存溢出的空间,并且运行速度最快。
2.本地方法栈
用来运行本地方法的区域
是线程私有的
空间大小可以调整
可能会出现栈溢出
3.java虚拟机栈
栈的特征
栈是运行时的单位,管理方法的调用运行。
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存着一个个栈帧对应着一次方法的调用。
是用来运行java方法的区域。
可能会出现栈溢出。
栈只有两个操作压栈和出栈
栈的运行原理
先进后出的结构。
每个时间点只有一个活动栈,就是只有正在执行的方法的栈帧是有效的,也就是顶部的栈帧最顶部的称为当前栈帧。
栈帧结构:
一个栈帧包含:
局部变量表(存储在方法中声明的变量)
操作数栈(实际计算运行)
动态链接
void A(){
B();//B方法的地址
}
方法返回地址
小总结:
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈可能会出现内存溢出
java栈,本地栈大小是可以调整的
4.堆
基本作用特征
堆是JVM管理的内存空间最大的一块区域,但是大小可调(调优时)。
堆区域在JVM启动时就被创建,其空间大小就被确定了。
堆区域是存储空间,用来存储对象。
堆区域是存在垃圾回收的,并且是线程共享的区域。
堆空间的分区
- 年轻代(新生代/新生区)
伊甸园区(Eden 对象刚刚创建存储在此区域)
幸存者1区(Survivor 0/From)
幸存者2区(Survivor 1/To)
- 老年代(老年区)
为什么要分区
可以根据对象的存活的时间放在不同的区域,可以区别对待。
频繁回收年轻代,较少回收老年代。
创建对象,在堆内存中分布过程
1.新创建的对象,都存储在伊甸园区
2.当垃圾回收时,将伊甸园中垃圾对象直接销毁,将存活的对象,移动到幸存者1区,
3.之后创建的新对象还是存储在伊甸园区,再次垃圾回收到来时,将伊甸园中的存活对象移动到幸存者2区,
同样将幸存者1区的存活对象移动到幸存者2区,每次保证一个幸存者区为空的,相互转换.
4.每次垃圾回收时,都会记录此对象经历的垃圾回收次数,当一个对象经历过15次回收,仍然存活,就会被移动到老年代
垃圾回收次数,在对象头中有一个4bit的空间记录 最大值只能是15(1111),
5.老年区回收次数较少,当内存空间不够用时,才会去回收老年代.
堆空间默认比例
堆空间默认新生代占1,老年代占2,新生代占堆的1/3,可以通过**-XX:NewRatio**=3调整,表示老年代占3,新生代占堆的1/4;
一般项目中生命周期长的对象较多时,可以通过调整老年代大小进行调优。
新生代中的伊甸园区(Eden)和两个幸存者区(Survivor)空间占比为8:1:1;开发者可以通过**-XX:SurvivorRatio进行调整。新生区的对象默认生命周期超过15(回收年龄),就会去养老区,可以通过-XX:MaxTenuringThreshold**自定义回收年龄。
分代收集思想(Minor GC、Major GC、Full GC)
JVM在进行回收的时候并不是新生区和老年去一起回收,多数是新生区更加频繁。
Minor GC:称为新生区收集,在新生区内进行操作,操作频繁。
Major GC:称为老年区收集,在老年区进行操作,操作较少。
Full GC:整个java堆和方法区的垃圾收集。一般出现的情况有:System.gc();(一般不使用)、老年区空间不足、方法区空间不足。整堆收集时,会造成线程停止,开发期间应当避免使用整堆收集。
堆空间的参数设置
官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
字符串常量池
JDK7之后的版本中常量池放在了堆空间中,因为方法区的回收效率很低。方法区的回收是发生在Full GC发生时,而Full GC是在老年代的空间不足、方法区空间不足时触发的。开发过程中会有大量的字符串被创建,字符串常量池存放在方法区会导致回收效率不高,永久代内存不足,放在堆里,能及时回收内存。
5.方法区
方法区又称为non-heap(非堆),方法区可以看做一块独立于堆的内存空间
基本特征
在jvm启动时创建,大小可调。
其大小决定了系统可以保存多少个类,如果系统定义的类太多可能会出现溢出。
是线程共享的区域。
作用
主要用来存储已经加载的类的信息,以及即时编译器编译后的信息,以及常运行时量池
方法区、堆、栈交互关系
方法区存储类信息(元信息)
堆中存储创建的对象
栈中存储对象引用
如:
Person ren = new Person();
等号左边的Person表示类型信息,属于类信息,存储在方法区;ren为变量,存放于局部变量表中,也就是存于栈中;等号右边的new Person()属于创建的对象,存放于堆中。
方法区大小设置
-XX:MetaspaceSize 设置方法区的大小
windows jdk默认的大小是21MB
也可以设置为-XX:MaxMetaspaceSize 的值是-1,表示没有限制. 没有限制 就可以使用计算机内存
可以将初始值设置较大一点,减少了FULL GC发生
方法区的内部结构
方法区存储信息主要:类型信息,域(Field)信息,方法(Method)信息,常量,静态变量,即时编译器编译后的代码缓存,运行时常量池。
方法区的垃圾回收
在Full GC时方法区发生垃圾回收。
主要是回收类信息,类信息回收条件比较苛刻,满足以下3点:
- 在堆中,该类及其子类的对象都不存在了
- 该类的类加载器不存在了
- 该类的Class对象不存在了
也可以认为类一旦被加载就不会被卸载了。
特点总结
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈,堆,方法区可能会出现内存溢出
java栈,本地栈,堆,方法区大小是可以调整的
堆,方法区是线程共享的,是会出现垃圾回收的
本地方法接口
什么是本地方法接口
用native关键字修饰的方法称为一个本地方法, 没有方法体.
如:hashCode();线程中调用的start()方法底层的start0();
为什么要使用本地方法
java语言需要与外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可.
java的jvm本身开发也是在底层使用到了C语言
执行引擎
作用:将加载到内存的字节码解释/编译为不同平台的机器码。
注意:
.java—编译—>.class 是在开发期间有jdk提供的编译器(javac)进行的源码编译(也称为 前端编译)
.class(字节码)—解释/编译—>机器码 由执行引擎完成(称为后端编译)
什么是解释器?什么是编译器(JIT)?
解释器:将字节码逐行解释执行,效率抵
编译器(JIT 即时编译器):将字节码编译缓存起来,存储在方法区的JIT代码缓存中,执行更加高效,不会立即使用编译器。
将一些频繁操作的热点代码进行编译,并缓存到方法区中,可以提高执行效率。
为什么Java是半编译半解释型语言?
JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的 JIT 代码缓存中(执行效率更高了)
程序启动后,先使用解释器立即执行,省去了编译时间,程序运行一段时间后,对热点编译缓存,提高后续执行效率,采用的解释器和编译器结合的方案。