jvm类加载

1.首先稍微了解一下什么是jvm

1.1物理上

在这里插入图片描述
物理上,可认为jre(java运行时环境)的bin目录就是jvm,jvm运行时还要依赖lib类库。

1.2工作方式

这里对比c语言程序

  1. c语言程序编译之后生成exe文件,是可以点击直接运行的;java源文件编译成class文件之后是不能直接运行的,需要虚拟机来解析才能运行。
  2. c语言程序在windows编译成的exe放到linux或其他服务器时无法运行的,因为不同的操作系统对exe的解析是不一样的。class文件放到任何有安装虚拟机的服务器都可以正常运行,程序员只需要用相同的方式编写代码并编译成class文件,适配不同操作系统的事由虚拟机帮我们完成,增强了可移植性。
    在这里插入图片描述

1.3JVM执行程序过程

  1. 加载class文件进内存
  2. 管理并分配空间
  3. 执行结束进行垃圾回收

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方法区

  • 永久代
    存放类的元信息
  1. 类的完整有效名(全类名)
  2. 本类的直接父类的完整有效名(若类时interface或只继承Object算无父类)
  3. 类的修饰符(public、abstract、final不能被继承 的子集)
  4. 直接接口列表的完整有效名(全类名在java源码中是包.类名在类文件中是包/类名)
  5. 常量池(每个已加载的类都有)
    ① 静态常量(String、Integer等基本类型,static final修饰)
    ② 对类、域和方法的引用
    (池中的数据项像数组项一样,用索引访问,在JAVA程序的动态链接中起核心作用)
  6. 域信息(Field)
    域名、域类型、域修饰符、声明顺序
  7. 方法信息
    ①方法名、返回类型、参数数量和类型(有序的)、修饰符
    ②除了abstract、native方法外,其他方法还保存方法的操作数栈、方法栈帧和局部变量区大小
  8. 异常表
  9. 类变量(静态变量)
    JVM在使用一个类之前必须在方法区为其每个non-final类变量分配空间
  10. 常量(每个常量在常量池中有一份拷贝)
  11. 类加载器的引用
    JVM必须知道类加载器是启动类加载器还是用户类加载器,是后者则将其引用保存到方法区中
    JAVA在动态链接中需要用到,当解析一个类到另一个类的引用时,JVM要保证两个类的加载器相同,对区分名字空间的方向很重要
  12. Class的引用
    JVM必须以某种方式将某个类的Class实例和方法区的类信息联系起来
    (可通过Class实例中的许多方法获取类的信息,都是直接从方法区中直接获取的)
  13. 方法表
    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补充命名空间

  • 每个类加载器都有自己的命名空间,命名空间是由该加载器及其所有的父加载器加载的类组成
  • 在同一个命名空间中,不会出现完整名字(包括类的包名)相同的两个类;在不同的命名空间中,可以出现类的完整名字(包括类的包名)相同的两个类
  • 子加载器所加载的类能够访问到父加载器所加载的类;父加载器所加载的类无法访问到子类加载器所加载的类
  • 如果两个两个加载器没有直接或者间接的父子关系,那么他们各自加载的类互相不可见
  • 在一个类中加载另一个类,默认的类加载器是本类的类加载器
  • 案例
  1. 类加载器的测试

案例中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的父加载器就更不用说了,所以报错了。

  1. 命名空间的测试

编译下方案例,编译完将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加载(装载)

  1. 通过全类名获取指定的字节码文件,并加载成二进制流
  2. 将字节流所代表的静态存储结构转化成方法区的运行时数据结构

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初始化时机

  1. 新建对象实例
  2. 访问静态变量和静态方法时
  3. 使用java.lang.reflect进行反射调用时
  4. 初始化一个类时,其父类还没有进行初始化
  5. 虚拟机启动时,定义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(){}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值