就是一台虚拟计算机,它是一款软件,用来执行虚拟的计算机指令。虚拟机可以分为两类:系统虚拟机和程序虚拟机。
VMware就属于系统虚拟机。物理的模拟了一个可以操作的操作系统平台。
Java虚拟机就是一个程序虚拟机。它是为执行某个单个程序的而设计的。Java虚拟机中执行的指令我们称之为Java字节码指令。Java虚拟机是执行Java字节码文件的,可以独立运行。Java虚拟机是Java技术的核心,因为所有的Java程序都是在Java虚拟机中运行。
程序在执行前需要将Java代码转换为字节码,JVM首先需要把字节码通过类加载器把文件加载到内存中的运行时数据区,字节码文件是JVM的一台指令集规范,并不能直接用底层操作系统去执行,因此需要特定的命令解析器 执行引擎,将字节码翻译为操作系统能读懂的机器码,然后交由CPU执行,执行时需要调用其他语言的接口 本地方法接口来实现整个程序的功能。
主要负责加载class文件。好比一个快递员
加载过程分为三步:
链接分为三步:验证、准备、解析
验证:
准备:对类的静态属性分配内存,并设置默认值;不包含被final修饰的static常量,在编译时已经初始化
如:
public static int value = 12;//在准备阶段完成后value的值为0,不是12
解析:
将二进制字节码替换为地址引用。
类初始化,为类的静态变量赋值。如:上面的value在初始化后的值就是12。
注意:一下两种情况是不会加载类的:
引用该类的静态常量,不会导致类被加载,因为静态常量在编译期间就已经被初始化了,这里的常量是指字面上已经制定值得常量,而不是需要经过计算才能得到结果的常量。
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开发人员的角度来看,类加载器应当分为四类:启动类加载器、扩展类加载器、应用类加载器、自定义类加载器。
这个类加载器使用的是C/C++语言实现的,嵌套在JVM内部。不继承ava.lang.ClassLoader。处于顶层,没有父加载器。用来加载java核心类库。
由Java语言编写,由sun.misc.Launcher$ExtClassLoader 实现。派生于ClassLoader类。
从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。
java语言编写,语言编写的,由 sun.misc.Launcher$AppClassLoader 实现。派生于ClassLoader类。
加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类。
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
工作原理:
双亲委派机制是自底向上的委托,自顶向下的加载的过程。
这样做一个明显的目的就是为了保护java类库的安全性,不会被开发者覆盖。
可以通过继承ClassLoader类,重写loadClass或者findClass方法,在loadClass方法中是实现双亲委派逻辑的地方,修改它会破会双亲委派机制不建议使用,建议使用重写findClass方法实现自定义的类加载器,典型的tomcat中,加载器部署在tomcat中的项目时,就使用的是自己的类加载器。
程序计数器是一块较小的内存空间,几乎可以忽略。
用来存储每个线程下一条指令的地址,也就是即将要执行的命令代码。
是线程私有的,每个线程都拥有一个线程计数器,生命周期与线程一致。
是运行时数据区中,唯一一个不会出现内存溢出的空间,并且运行速度最快。
用来运行本地方法的区域
是线程私有的
空间大小可以调整
可能会出现栈溢出
栈的特征
栈是运行时的单位,管理方法的调用运行。
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存着一个个栈帧对应着一次方法的调用。
是用来运行java方法的区域。
可能会出现栈溢出。
栈只有两个操作压栈和出栈
栈的运行原理
先进后出的结构。
每个时间点只有一个活动栈,就是只有正在执行的方法的栈帧是有效的,也就是顶部的栈帧最顶部的称为当前栈帧。
栈帧结构:
一个栈帧包含:
局部变量表(存储在方法中声明的变量)
操作数栈(实际计算运行)
动态链接
void A(){
B();//B方法的地址
}
方法返回地址
小总结:
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈可能会出现内存溢出
java栈,本地栈大小是可以调整的
堆是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**自定义回收年龄。
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是在老年代的空间不足、方法区空间不足时触发的。开发过程中会有大量的字符串被创建,字符串常量池存放在方法区会导致回收效率不高,永久代内存不足,放在堆里,能及时回收内存。
方法区又称为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点:
也可以认为类一旦被加载就不会被卸载了。
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈,堆,方法区可能会出现内存溢出
java栈,本地栈,堆,方法区大小是可以调整的
堆,方法区是线程共享的,是会出现垃圾回收的
用native关键字修饰的方法称为一个本地方法, 没有方法体.
如:hashCode();线程中调用的start()方法底层的start0();
java语言需要与外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可.
java的jvm本身开发也是在底层使用到了C语言
作用:将加载到内存的字节码解释/编译为不同平台的机器码。
注意:
.java—编译—>.class 是在开发期间有jdk提供的编译器(javac)进行的源码编译(也称为 前端编译)
.class(字节码)—解释/编译—>机器码 由执行引擎完成(称为后端编译)
解释器:将字节码逐行解释执行,效率抵
编译器(JIT 即时编译器):将字节码编译缓存起来,存储在方法区的JIT代码缓存中,执行更加高效,不会立即使用编译器。
将一些频繁操作的热点代码进行编译,并缓存到方法区中,可以提高执行效率。
JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的 JIT 代码缓存中(执行效率更高了)
程序启动后,先使用解释器立即执行,省去了编译时间,程序运行一段时间后,对热点编译缓存,提高后续执行效率,采用的解释器和编译器结合的方案。