JVM 类加载概述

共 6745字,需浏览 14分钟

 ·

2020-08-30 09:16

来源:SegmentFault 思否社区

作者:又坏又迷人




JVM简介


JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。


Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。





内存结构概述



类加载子系统(Class Loader)


类加载器分为:自定义类加载器 < 系统类加载器 < 扩展类加载器 < 引导类加载器


类加载过程分为:加载、链接、验证、初始化。



程序计数器(Program Counter Register)


是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。



虚拟机栈 (Stack Area)


栈是线程私有,栈帧是栈的元素。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。



本地方法栈 (Native Method Area)


JVM 中的栈包括 Java 虚拟机栈和本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。



堆 (Heap Area)


堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间。


Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。



方法区(Method Area)


方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。


JDK 8 使用元空间 MetaSpace 代替方法区,元空间并不在JVM中,而是在本地内存中



类加载过程概述


类加载器子系统负责从文件系统或者网络中在家Class文件,class文件在文件开头又特定的文件标识。


ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。


加载类的信息存放于一块被称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)




类加载器ClassLoader角色


  1. class文件存在本地硬盘上,在执行时加载到JVM中,根据这个文件可以实例化出n个一模一样的实例。
  2. class文件加载到JVM中,被称为DNA元数据模板,放在方法区中。
  3. 在.class文件 -> JVM -> 最终成为元数据模板的过程中,ClassLoader就扮演一个快递员的角色。





类加载过程概述


类的加载过程大致分为三个阶段:加载,链接,初始化。




类的加载过程一:加载(Loading)


  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口



类的加载过程二:链接(Linking)


验证(Verify)


  1. 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害到虚拟机的安全。


准备(Prepare)


准备阶段是进行内存分配。为类变量也就是类中由static修饰的变量分配内存,并且设置初始值,这里要注意,初始值是默认初始值0、null、0.0、false等,而不是代码中设置的具体值,代码中设置的值是在初始化阶段完成的。另外这里也不包含用final修饰的静态变量,因为final在编译的时候就会分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例对象会随着对象一起分配到Java堆中。


public class HelloApp {
private static int a = 1; // 准备阶段为0,而不是1
public static void main(String[] args) { System.out.println(a); }}


解析(Resolve)


解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量等。





类的加载过程三:初始化(initialization)


  1. 初始化阶段就是执行类构造器方法()的过程
  2. 此方法不需要定义,是javac编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来
  3. 构造器方法中指令按语句在源文件中出现的顺序执行。
  4. ()不同于类的构造器(构造器是虚拟机视角下的())
  5. 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
  6. 虚拟机必须保证一个类的()方法在多线程下被同步加锁


需要注意,如果没有定义静态变量或静态代码块的话则没有()


案例如下:


public class HelloApp {

static { code = 20; } private static int code = 10;
//第一步:在准备阶段初始化了code默认值为0。 //第二步:根据类执行顺序先执行静态代码块,赋值为20. //第三步:最后赋值为10,输出结果为10.
public static void main(String[] args) { System.out.println(code); // 10 }}


通过字节码文件可以很清楚的看到结果:


 0 bipush 20 2 putstatic #3  5 bipush 10 7 putstatic #3 10 return


先被赋值为20,然后改为10。





类加载器概述


JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader) 和 自定义类加载器(User-Defined ClassLoader)


从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器


无论怎么划分,在程序中最常见的类加载器始终只有三个:


系统类加载器(System Class Loader) < 扩展类加载器(Extension Class Loader) < 引导类加载器(Bootstrap Class Loader)


它们之间的关系不是继承关系,而是level关系。



系统类加载器和扩展类加载器间接或直接继承ClassLoader。划线分为两大类。



public class HelloApp {

public static void main(String[] args) {
//获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@60e53b93
//获取其上层:获取不到引导类加载器 ClassLoader bootStrapLoader = extClassLoader.getParent(); System.out.println(bootStrapLoader);//null

//我们自己定义的类是由什么类加载器加载的:使用系统类加载器 ClassLoader classLoader = HelloApp.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

//看看String是由什么类加载器加载的:使用引导类加载器 ClassLoader classLoaderString = String.class.getClassLoader(); System.out.println(classLoaderString);//null }
}


引导类加载器(Bootstrap ClassLoader)


  1. 这个类加载使用c/c++语言实现,嵌套在JVM内部。
  2. 他用来加载Java的核心库,(JAVA_HOME/jre/lib/rt.jar、resources.jar、或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  3. 并不继承自java.lang.ClassLoader ,没有父加载器。
  4. 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  5. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。


扩展类加载器(Extension ClassLoader)


  1. Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  2. 派生于ClassLoader类。
  3. 上一层类加载器为启动类加载器。
  4. 从java.ext.dirs系统属性所指定的目标中加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的Jar放在此目录下,也会自动由扩展类加载器加载。


系统类加载器(System ClassLoader)


  1. Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
  2. 派生于ClassLoader类。
  3. 上一层类加载器为扩展类加载器。
  4. 它负责加载环境变量classpath或系统属性 java.class.path指定下的类库。
  5. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都有由它来完成加载。
  6. 通过ClassLoader.getSystemClassLoader()方法可以获取该类加载器。




为什么需要用户自定义类加载器?


  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄露


用户自定义类加载器实现步骤


  1. 通过集成抽象类java.lang.ClassLoader类的方式,实现自己的类加载器。
  2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载器,但是在JDK1.2之后不再建议用户去覆盖loadClass()方法,而是建议把自定义类加载逻辑写在findClass()方法中。
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己编写findClass()方法其获取字节码流的方式,使得自定义加载器编写更为简洁。


关于ClassLoader


ClassLoader是一个抽象类,系统类加载器和扩展类加载器间接或直接继承ClassLoader。


常用方法如下:






双亲委派机制


Java虚拟机对class文件采用的是按需加载的方式,也就是说需要使用该类的时候才会将它的class文件加载到内存生成class对象。


当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。


工作原理



  1. 如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求向上委托给上一级类加载器去执行。
  2. 如果上一级类加载器还存在上一级,则进一步向上委托,依次递归,请求最终会达到引导类加载器。
  3. 如果引导类加载器可以完成类加载任务,就成功返回。如果无法完成类加载任务,会依次往下再去执行加载任务。这就是双亲委派机制。


比如我们现在在自己项目中创建一个包名java.lang下创建一个String类。

package java.lang;
public class String {
static { System.out.println("我是自己创建的String"); }}


public class HelloApp {

public static void main(String[] args) {
String s = new String();
}
}


执行之后并不会输出我们的语句,因为我们的String类加载器一开始由系统类加载器一级一级往上委托,最终交给引导类加载器,引导类加载器一看是java.lang包下的,ok,我来执行,最终执行的并不是我们自己创建String类,保证了核心api无法被纂改,避免类的重复加载。


package java.lang;
public class String {
static { System.out.println("我是自己创建的String"); }
public static void main(String[] args) { System.out.println("Hello World !!!"); }}


如果我们想运行如上代码,我们会得到如下错误:


错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:   public static void main(String[] args)否则 JavaFX 应用程序类必须扩展javafx.application.Application


因为我们知道在核心api String类中是没有main方法的,所以我们可以确定加载的并不是我们自己创建的String类。


在JVM中表示两个Class对象是否为同一个类存在的必要条件:


  • 类的完整类名必须一致,包括包名。
  • 加载这个类的ClassLoader也必须相同。


顺便说一句,我们包名如果为java.lang则会报错。




点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流。

- END -

浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报