一、双亲委派
启动类加载器:启动类加载器是最顶层的类加载器,它负责加载Java的核心库,如java.*包下的类。它是用本地代码实现的,并且不是由Java编写的,因此没有明确的父类加载器。
平台类加载器:平台类加载器取代了之前的扩展类加载器。它主要用于加载JDK内部的API模块,例如java.xml、java.sql等。平台类加载器直接位于启动类加载器之下,它的任务是加载那些对整个平台至关重要的类和模块。
系统类加载器:负责加载应用程序classpath路径上的类文件。这是开发者自己编写的代码以及第三方库所在的区域。如果应用程序没有自定义类加载器,那么所有非核心库的类都将由这个加载器来加载。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
二、类加载的过程
(一)加载 (Loading)
读取字节码:从文件系统、网络或其他来源读取类的二进制数据。
创建 Class 对象:将这些二进制数据转换成一个 java.lang.Class 实例,该实例代表了类在 JVM 中的唯一标识。
存储到方法区:将类的数据存储到方法区(Metaspace 在 JDK 8+),这是 JVM 内存的一部分,用于存储已加载的类信息。
(二)链接 (Linking)
验证 (Verification):确保加载的类符合 JVM 规范,不会破坏现有的内存结构。这一步会检查类的格式是否正确、是否有非法的跨类操作等。
准备 (Preparation):为类变量分配内存并设置默认初始值(注意,此时不包括由显式赋值的静态变量)。对于 final 类型的静态变量,会直接赋上编译期确定下来的值。
解析 (Resolution):将类中的符号引用(例如类名、方法名和字段名)替换为直接引用(如内存地址)。这一步是可选的,不是所有情况下都会发生。
(三)初始化 (Initialization)
执行类构造器 <clinit>() 方法,它是编译器自动收集类中所有静态变量的赋值动作和静态代码块中的语句组成的。只有在这个阶段,静态变量才会被赋予程序员指定的初始值。
三、JVM内部结构
一旦类被加载,它们就进入了JVM的运行时数据区。JVM的主要组成部分包括:
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等。
堆:所有对象实例和数组都在这里分配空间。
栈:每个线程创建时都会创建一个新的栈,用来存储局部变量、部分结果以及执行上下文。
本地方法栈:与栈类似,但专为本地方法服务。虚拟栈。
程序计数器:记录当前线程所执行的字节码指令的位置。
四、垃圾回收
随着程序运行,JVM会不断创建新的对象,这会导致堆内存逐渐耗尽。为了防止这种情况发生,JVM提供了自动化的垃圾回收机制来管理内存。Java中的垃圾回收是根据 可达性分析算法 和 引用计数算法 来判断对象是否存活的。
引用计数器算法:
就是为每个对象都添加一个计数器,每多一个引用指向对象,计数器就加1,当计数器为0的对象,就是可回收的对象。
可达性分析算法:
简单来说这个算法的就是根据"GC Roots"对象为根,向下去搜索(去找叶节点),搜索走过的路径叫引用链(Reference Chain),当一个对象和"GC Roots"之间没有任何引用链时,这个对象就会判定为可回收的。
四种引用类型:强引用、软引用、弱引用、虚引用。
垃圾收集算法:
1.标记-清除:分为两个阶段:标记、清除。首先标记所有需要回收的对象,在标记完成后回收所有被标记的对象。
2.标记-整理:在完成标记阶段后,不是直接对可回收对象进行清理,而是让存活对象向着一端移动,然后清理掉边界以外的内。
3.复制:将内存划分为两个相等的块,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。年轻代主要用复制算法,幸存区复制,一般都是from复制到to,谁空谁是to,适用于对象存活度较低。
4.分代:新生代、老年代、永久代