一,类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行加载。(要注意的是,对于main函数所在的类,在程序刚开始运行时就会被加载。)
1,加载
在加载之前,JVM要先从磁盘上寻找字节码文件(文件名.class):JVM根据系统环境变量的CLASSPATH里面找字节码文件的搜索路径。
(.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\lib\dt.jar)
路径中 . 代表当前工程目录,例如一般自己写的程序编译的字节码文件都在当前工作目录下,但是系统自带的类则在其他路径中,如:System Out 类等。
加载指的是JVM的类加载器从对象的字节码文件(student.class)中将类的Class对象(类加载器为所有被载入内存中的类生成一个java.lang.Class实例对象)读入到内存(这个对象里面放的是该对象的属性,方法和访问限定等)。也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。类的加载由类加载器完成,类加载器通常由JVM提供,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
2,链接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。
1> 验证: 验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,能够在JVM上运行,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2>准备:类准备阶段负责为类的静态成员分配内存,并设置默认初始值(此处的设置初始值并不是真正的赋值)。若该类有基类,则继续加载基类,并为其基类的静态成员分配空间。(这就是为什么打印时先打印基类的static,在打印派生类的,因为是链接中的准备是从派生类一直往基类上进行的,链接准备完后直接停留在基类,所以初始化执行是从基类开始,又一直往下)
3>解析:将类的二进制数据中的符号引用替换成直接引用。??????????
3,初始化
初始化是为类的静态变量赋予正确的初始值,和调用类的静态初始化块。
如果类中有语句:private static int a = 10,它的执行过程是这样的,(首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,)因为变量a是static的,则给a分配内存,此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,又把a的真正的值10赋给a,此时a=10。
二,类的加载时机
1.创建类的实例,也就是new一个对象(首次)
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(Class.forName("com.lyj.load"))
5.初始化一个类的子类(会首先初始化子类的父类)
6.JVM启动时标明的启动类,即文件名和类名相同的那个类
除此之外,下面几种情形需要特别指出:
对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
三,类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名
????类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
1)根类加载器(Bootstrap class loader):它用来加载 Java 的核心类,是用原生代码(c/c++)来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中/java/jdk/jre/lib/rt.jar里所有的class,是JVM运行时必须依赖的库,也可理解为JVM是有这里面的class文件生成的,由C++实现,不是ClassLoader子类)。由于该类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。当开发者要访问时,返回的则是null。
比如获取String类的类加载器时,因为String类是由Bootstrap class loader加载的,所以在获取类加载器时,返回的就是null.
2)拓展类加载器(Extensions class loader):它负责加载JRE的扩展目录,/java/jdk/jre/lib/ext或者由/java/ext/dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。(在本质上它是有父类加载器,为Bootstrap class loader,但是因为Bootstrap class loader底层的jar包是用C和C++实现的,而在java中是无法识别c和c++语言的,所以当Extensions class loader在获取其父类加载器时,返回的是null。)
3)系统类加载器(System class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
四、类加载机制:
1.JVM的类加载机制主要有如下3种。
全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:所谓的双亲委派,就是当要求子类去加载某类时,则先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加
载器在接到加载类 的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二
进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
双亲委派机制的优势:采用双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
五,加载类的三种方式
1,Class c = Student .class;
该方法仅能将Student类的Class对象加载到内存,不会进行初始化。
注意:要用类名.class,因为此时没有生成对象
所以不会有打印结果。
2,Class c2=Class.forName(" 包名.要加载的类的类名");
将类加载进来,并进行内存分配和初始化赋值
此时借助的使Class对象的方法,并将包名.要加载的类的类名赋给该方法
所以打印结果会有静态初始化块中的语句。
3.Student student =new Student("张三“,20,‘男’,”西安工业大学“);
Class c3=student.getClass();
将类加载进来,并进行内存分配和初始化赋值,此时也会生成类的实例.
注意:此时是通过生成对象的getClass()来进行类的加载。
运行结果
******************************************************************
1 package L18; 2 class Student{ 3 private String name; 4 private int age; 5 private char sex; 6 private static String school; 7 static{ 8 System.out.println("类的静态初始化块"); 9 school="西安工业大学1"; 10 } 11 { 12 System.out.println("类的实例初始化块"); 13 } 14 public Student(String name,int age,char sex,String school){ 15 System.out.println("类的构造函数"); 16 this.name=name; 17 this.age=age; 18 this.sex=sex; 19 this.school=school; //静态成员变量也可以在构造函数中对其赋初值 20 } 21 22 @Override 23 public String toString() { 24 return "Student{" + 25 "name='" + name + '\'' + 26 ", age=" + age + 27 ", sex=" + sex + 28 ", school=" + school + 29 '}'; 30 } 31 } 32 public class ClassLoadCase { 33 public static void main(String[] args) throws Exception { 34 //Class c=Student.class; //仅将类的Class对象加载进来 35 // Class c2=Class.forName("L18.Student"); 36 Student student=new Student("张三",20,'男',"西安工业大学"); 37 Class c3=student.getClass(); //将类加载进来,并进行内存分配和初始化赋值,此时也会生成类的实例 38 } 39 }
六,JVM的内存划分
栈内存(stack): 函数及函数里面的局部变量(简单/引用)的内存
(函数定义处开辟栈空间,当出了函数右大括号,则栈内存回收,函数里面定义的变量的内存同时也会被回收)
堆内存(heap):new的对象,constant pool 常量池
(new的时候开辟空间,当没有被引用的时候,就会被GC在下一个回收周期所回收(不是立即!!此外还要判断对象有没有重写finalize方法))
方法区(method area):Class对象, static的成员变量
(类加载的时候就在方法区开辟空间,一般持续到应用程序结束)
引用链接:https://blog.csdn.net/m0_38075425/article/details/8162734