2021-04-18
Posted Java_小墨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2021-04-18相关的知识,希望对你有一定的参考价值。
JVM内存结构和类加载机制
一、JVM内存结构:
- java虚拟机栈:存放的是对象的引用(指针)和局部变量
- 程序计数器:每个线程都有一个程序计数器,跟踪代码运行到哪个位置了
- 堆:对象、数组
- 方法区:字节流(字节码文件)所代表的静态存储结构转换为方法区的运行时数据结构
- 常量池:String(字符串常量)
二、JVM的类加载机制:双亲委派模型
- 启动类加载器(Bootstrap ClassLoader):主要负责加载JDK\\jre\\lib目录中并且能被虚拟机识别的类库到JVM内存中。
- 扩展类加载器(Extension ClassLoader):主要负责加载JDK\\jre\\lib\\ext目录中的类库到JVM内存中。
- 应用程序类加载器(Application ClassLoader):负责加载类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
1.protected synchronized Class loadClass(String name, boolean resolve)
2. throws ClassNotFoundException
3. // 首先检查该name指定的class是否有被加载
4. Class c = findLoadedClass(name);
5. if (c == null)
6. try
7. if (parent != null)
8. // 如果parent不为null,则调用parent的loadClass进行加载
9. c = parent.loadClass(name, false);
10. else
11. // parent为null,则调用BootstrapClassLoader进行加载
12. c = findBootstrapClass0(name);
13.
14. catch (ClassNotFoundException e)
15. // 如果仍然无法加载成功,则调用自身的findClass进行加载
16. c = findClass(name);
17.
18.
19. if (resolve)
20. resolveClass(c);
21.
22. return c;
23.
整个过程其实非常简单:先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。
-
自定义类加载器:若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。
-
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
能不能自己写个类叫java.lang.System?
为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
三、类的加载过程
当类的字节码文件被ClassLoader加载到内存中后,就开始了它的生命周期。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。[加载,链接,初始化]
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性(例如接口只在调用的时候才知道具体实现的是哪个子类)。
1. 加载:
1)通过“类全名”来获取定义此类的二进制字节流
2)将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
相对于类加载过程的其他阶段,加载阶段是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。
2. 验证:
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
3.准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的静态变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。
其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:
public static int value = 12;
那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为12的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。
上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建议上面类变量value定义为:
public static final int value = 12;
4. 解析:
-
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
-
这些符号(如int a = 5中的a)就是符号引用,而解析过程就是把它转换成指向堆中的对象地址的逻辑地址。
5. 初始化:
类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。在以下四种情况下初始化过程会被触发执行:
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化
4)jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
以上是关于2021-04-18的主要内容,如果未能解决你的问题,请参考以下文章