1.首先稍微了解一下什么是jvm
1.1物理上
物理上,可认为jre(java运行时环境)的bin目录就是jvm,jvm运行时还要依赖lib类库。
1.2工作方式
这里对比c语言程序
- c语言程序编译之后生成exe文件,是可以点击直接运行的;java源文件编译成class文件之后是不能直接运行的,需要虚拟机来解析才能运行。
- c语言程序在windows编译成的exe放到linux或其他服务器时无法运行的,因为不同的操作系统对exe的解析是不一样的。class文件放到任何有安装虚拟机的服务器都可以正常运行,程序员只需要用相同的方式编写代码并编译成class文件,适配不同操作系统的事由虚拟机帮我们完成,增强了可移植性。
1.3JVM执行程序过程
- 加载class文件进内存
- 管理并分配空间
- 执行结束进行垃圾回收
1.4JVM生命周期
1.4.1生命周期
- JVM实例的生命周期:对应着一个java程序的运行,随它启动和消亡,有多少个java程序在运行就有多少个JVM实例----进程级
任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点 - JVM执行引擎实例的生命周期:对应着java程序运行时的线程,每个线程有自己的JVM执行引擎实例跟随着启动和消亡----线程级
- 当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。
非守护线程也称之为用户线程,是执行我们写的逻辑的线程。非守护线程是为守护线程服务的(如垃圾回收线程);当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
1.4.2执行引擎说明
物理机的执行引擎是由硬件实现的,和物理机的执行过程不同的是虚拟机的执行引擎由于自己实现的。
- 执行引擎以指令为单位读取Java字节码,一条一条地读取。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过以下两种方式转换成合适的语言。
- 转换方式:
解释器:一条一条地读取,解释并且执行字节码指令。一条一条操作效率比较低。字节码这种“语言”基本来说是解释执行的。
即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行;然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
2.运行时数据区
2.1模型
2.2说明
2.2.1方法区
- 永久代
存放类的元信息
- 类的完整有效名(全类名)
- 本类的直接父类的完整有效名(若类时interface或只继承Object算无父类)
- 类的修饰符(public、abstract、final不能被继承 的子集)
- 直接接口列表的完整有效名(全类名在java源码中是包.类名在类文件中是包/类名)
- 常量池(每个已加载的类都有)
① 静态常量(String、Integer等基本类型,static final修饰)
② 对类、域和方法的引用
(池中的数据项像数组项一样,用索引访问,在JAVA程序的动态链接中起核心作用) - 域信息(Field)
域名、域类型、域修饰符、声明顺序 - 方法信息
①方法名、返回类型、参数数量和类型(有序的)、修饰符
②除了abstract、native方法外,其他方法还保存方法的操作数栈、方法栈帧和局部变量区大小 - 异常表
- 类变量(静态变量)
JVM在使用一个类之前必须在方法区为其每个non-final类变量分配空间 - 常量(每个常量在常量池中有一份拷贝)
- 类加载器的引用
JVM必须知道类加载器是启动类加载器还是用户类加载器,是后者则将其引用保存到方法区中
JAVA在动态链接中需要用到,当解析一个类到另一个类的引用时,JVM要保证两个类的加载器相同,对区分名字空间的方向很重要 - Class的引用
JVM必须以某种方式将某个类的Class实例和方法区的类信息联系起来
(可通过Class实例中的许多方法获取类的信息,都是直接从方法区中直接获取的) - 方法表
JVM对每个加载的非虚拟类信息中添加一个方法表,方法表是一组对类实例方法的直接引用(包括父类中的),可快速激活实例的方法,提高效率
- 存放编译后代码
- 运行时生成的常量
- Class对象从这里拿类的信息
2.2.2堆
- 存储类实例(包括Class)
- 数组也是存储在堆中,不过引用存储在栈中
- 堆是线程共享的,所以在分配内存时要加锁,导致new的开销较大
- gc的主要工作区域
- Sun Hotspot JVM在空间充足的时候给每个线程分配一块独立的TLAB空间去存储类实例(线程独享,不上锁,高效),当空间不充足的时候还是线程共享堆,把类实例一起放进去
- 图解
在堆中分为几个代年轻代(Eden、Survior(缓冲,分为From Space和To Space))和老年代
新创建的类实例放在年轻代,经过若干次gc之后逐渐移动到老年代
2.2.3栈
- JAVA栈是方法执行的内存模型
- 在栈的底部会有一个栈帧
栈帧
局部变量表(非静态变量和形参):普通类型变量和引用型变量的引用,在编译时就已经确定了表所占的内存大小
操作数栈:完成运算等栈的经典应用
指向常量池的引用
方法的返回地址:执行完方法返回线程的地方
2.2.4程序计数器
- 无论如何切换,CPU在一个时刻只执行一个指令
- 为保证每个线程完整、正常的执行完,计数器是线程私有的
- native方法:undefined(无内存大小限制)
非native方法:保存指令地址 - 存储空间是固定的,不会报内存溢出异常
2.2.5本地方法栈
- 存储每个native方法的调用状态
- JAVA栈为JAVA方法服务,本地方法栈为native方法服务
- JAVA栈和本地方法栈的底层实现十分类似
- HotSpot虚拟机把两个栈合起来了
3.类生命周期
3.1类加载器
3.1.1分类
- 启动类加载器
加载<JAVA_HOME>/lib路径和-Xbootstrap参数指定的路径下的虚拟机类库的类
用户无法使用 - 扩展类加载器
加载<JAVA_HOME>/lib/ext路径下和被java.ext.dirs系统变量指定的路径的类库
用户可使用 - 应用程序类加载器
若用户未指定自定义类加载器,此类加载器为默认的类加载器
是ClassLoader的getSystemClassLoader方法的返回值
加载用户路径(classpath、项目路径)下的类库
用户可直接使用 - 自定义类加载器
自定义类加载器,设置其加载的路径,默认是应用程序类加载器的子类加载器
3.1.2补充命名空间
- 每个类加载器都有自己的命名空间,命名空间是由该加载器及其所有的父加载器加载的类组成
- 在同一个命名空间中,不会出现完整名字(包括类的包名)相同的两个类;在不同的命名空间中,可以出现类的完整名字(包括类的包名)相同的两个类
- 子加载器所加载的类能够访问到父加载器所加载的类;父加载器所加载的类无法访问到子类加载器所加载的类
- 如果两个两个加载器没有直接或者间接的父子关系,那么他们各自加载的类互相不可见
- 在一个类中加载另一个类,默认的类加载器是本类的类加载器
- 案例
- 类加载器的测试
案例中myClassloader为自定义类加载器,加载路径为项目路径
class A{
public A(){
System.out.println("A is loaded by" + this.getClass.getClassLoader());
}
}
class B{
public B(){
System.out.println("B is loaded by" + this.getClass.getClassLoader());
new A();
}
}
//打印出来的两个类加载器都是应用程序类加载器(AppClassloader),因为自定义加载器请求父加载器去加载
class Test1{
public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
Class clazz = myClassLoader.loadClass("B");
clazz.newInstance();
}
}
之后修改上面的案例,编译之后将B类的class文件剪切到D:/class/目录下,再运行程序
这里还是能够正常运行,A的类加载器是AppClassloader;B类的类加载器是MyClassLoader
说明:myClassLoader加载B时委托父加载器AppClassloader去加载,AppClassloader在项目路径下没找到B的class文件,所以加载不到,只能让myClassLoader自己去加载B;在加载A时myClassLoader再次委托AppClassloader去加载,加载成功了
//修改main方法
class Test2{
public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
myClassLoader.setPath("D:/class/")
Class clazz = myClassLoader.loadClass("B");
clazz.newInstance();
}
}
再次修改案例,重新编译项目之后,将A的class文件剪切到D:/class/目录下,运行时报了NoClassDefFoundError的错
说明:myClassLoader加载B的时候委托AppClassloader去加载,AppClassloader在项目路径下找到了B的class文件,加载成功。在调用构造方法时,AppClassloader会去项目路径下找A的class文件去加载,发现找不到,AppClassloader的父加载器就更不用说了,所以报错了。
- 命名空间的测试
编译下方案例,编译完将D的class文件剪切到D:/class/目录下,然后运行
运行结果报了NoClassDefFoundError的错
说明:如上方案例所示,C的类加载器是AppClassloader;D类的类加载器是MyClassLoader。AppClassloader是MyClassLoader的父加载器,在AppClassloader(父)的命名空间里看不到MyClassLoader(子)命名空间里的D的类信息
class C{
public C(){
System.out.println("C is loaded by" + this.getClass.getClassLoader());
System.out.println("D's class is : " + D.class);
}
}
class D{
public D(){
System.out.println("D is loaded by" + this.getClass.getClassLoader());
new C();
}
}
class Test3{
public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
myClassLoader.setPath("D:/class/")
Class clazz = myClassLoader.loadClass("D");
clazz.newInstance();
}
}
反之,若在D中去获取C的class是可以获取到的,子加载器是可以获取父加载器的命名空间里的信息的
若new两个MyClassLoader实例,让它们分别加载C和D(移走项目路径下的class,不让它们委托父加载器),C和D是互不可见的
4.类加载
4.1加载(装载)
- 通过全类名获取指定的字节码文件,并加载成二进制流
- 将字节流所代表的静态存储结构转化成方法区的运行时数据结构
4.2验证
4.2.1分类
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
4.2.2作用
- 验证加载进内存的数据是否是JVM需要的,能够处理的数据
- 验证这些数据是否对JVM的运行有害,可以及时处理
4.3准备
4.3.1作用
正式为类变量分配内存空间和设置初始值
4.3.2说明
- 在方法区中分配存储空间
- 只为static修饰的non-final变量分配空间,非static变量在实例的时候才分配
- 设置的初始值不是我们设置的值,是系统默认的初始值,像整型变量一般为0,自定义的值是在初始化阶段才进行赋值
4.4解析
将虚拟机常量池的符号引用替换成直接引用
符号引用是用符号去描述、定位引用的目标,目标允许还未加载进内存;直接引用则必须是已经加载进内存的目标
- 类、接口解析
- 字段解析
- 类方法解析
- 接口解析
4.5初始化
4.5.1初始化时机
- 新建对象实例
- 访问静态变量和静态方法时
- 使用java.lang.reflect进行反射调用时
- 初始化一个类时,其父类还没有进行初始化
- 虚拟机启动时,定义main()方法的那个类先初始化
4.5.2说明
- 若类具有父类,则父类也会初始化
- 初始化静态类变量的值,这次就是赋值我们自定义的值了
- 执行静态代码块
- 代码说明
public class Father {
public static int a = 666;
public static final String b = "bb....";
static{
System.out.println("父类初始化!");
}
}
public class Son extends Father{
static{
System.out.println("子类初始化!");
}
}
public class Test {
public static void main(String[] args){
//只执行了Father的静态代码块,子类未被初始化
System.out.println(Son.a);
//Father和Son的静态代码块都不执行
Father[] ff = new Father[10];
//成功打印了bb.....但静态代码块都未执行
System.out.println(Son.b);
}
}
4.6使用
- 用类信息实例对象去使用
- 用类的Class对象找到类信息
- 用类的Class执行相应的反射方法
4.7卸载
gc
5.案例解读类加载
/**
1.JVM通过Test的全类名找到class文件并加载
2.将类信息存入方法区,并实例一个Class对象放入堆内存中,再将其引用也和类信息存在一起,作为类数据的外部引用接口
3.找到main方法并激活,JVM始终保持一个指针指向Test的常量池
4.执行第一条指令,在常量池为a分配内存空间
5.保持在常量池的指针找到a发现它是一个引用类型,在方法区中查找A的类信息,若未找到则将其加载进内存
6.加载完成之后以直接指向方法区A的类信息的指针替代常量池中a的值
7.执行下一个指令new A()
8.通过A在方法区中的信息可知A的实例对象所需内存,为其分配一块空间,实例A对象
9.上面加载类的时候只在准备阶段为n赋了默认初始值0,在new的时候才执行初始化,为n赋值10;此时还给m赋了默认值
11.实例之后调用了构造方法,为m赋值5
10.把a变量入栈
11.执行接下来的指令,激活a的test方法,test方法中无操作
12.将a出栈,程序运行结束
*/
public class Test{
public static void main(String[] args){
A a = new A();
a.test();
}
}
class A{
private static int n = 10;
private int m = 5;
void test(){}
}