文章目录
- 1.JVM中的内存区域划分
- 2.JVM的类加载机制
- 2.1 加载
- 2.2 验证
- 2.3 准备
- 2.4 解析
- 2.5 初始化
- 2.6 类加载的时机
- 3 类加载器
- 4.双亲委派模型
- 5.JVM中的垃圾回收策略
- 5.1 找谁是垃圾
- 5.1.1 引用计数法
- 5.1.2 可达性分析法
- 5.2 释放垃圾
- 5.2.1 标记清除算法
- 5.2.2 复制算法
- 5.2.3 标记整理算法
- 5.2.4 分代算法
1.JVM中的内存区域划分
JVM就是一个Java进程,Java进程会从操作系统这里申请一大块内存空间,给Java代码来使用。
而这一大块内存空间又可以进一步细分为以下几个最核心的内存区域:
- 堆:存放的是new出来的对象,即成员变量。
- 栈:存放的是方法与方法之间的调用关系,即局部变量。
- 方法区(旧)/元数据区(新):存放的是类加载之后的类对象(.class,Test.class),即静态变量。
这块的主要考点就是给一段代码,判断某个变量处于内存中的哪个位置,需要注意是的,我们是通过变量的形态(成员变量,局部变量,静态变量)来判断,而不是通过变量的类型来判断。
例如下面的例子:
void func() {Test t = new Test();}
接下来我们就系统的介绍一下JVM运行时数据区,也叫JVM的内存布局。按照 Java 的虚拟机规范,可以细分为程序计数器、虚拟机栈、本地方法栈、堆、方法区等,如下图:
其中堆和方法区在一个JVM进程中只有一份,而栈(虚拟机栈和本地方法栈)和程序计数器则是有多份的,每一个线程有一份。
- 程序计数器 : 也叫PC寄存器,用途是记录当前程序执行到哪个指令了,它是一个简单的long类型变量,里面存了一个内存地址,这个内存地址就是下一个要执行的字节码的内存地址。
- Java虚拟机栈:通常就是指”栈“,它的生命周期和线程一样,当线程执行一个方法时,会创建一个对应的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,然后栈帧会被压入栈中。当方法执行完毕后,栈帧会从栈中移除。
- 本地方法栈:是给JVM内部的本地方法使用的(JVM内部通过C++实现的方法)。
- 堆:堆(heap)是 JVM 中最大的一块内存区域,被所有线程共享,在 JVM 启动时创建,主要用来存储对象的。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是所有线程共享的。
2.JVM的类加载机制
.class 文件需要加载到虚拟机中之后才能运行和使用,类加载的过程就是将.class文件加载到内存中得到类对象的过程。其中类加载的过程分为五步,即加载→验证→准备→解析→初始化,如下图所示:
2.1 加载
加载阶段就是查找并加载类的二进制数据(.class文件),在加载阶段,JVM做三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
2.2 验证
这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证包括了:文件格式验证(二进制文件)、字节码验证,符号引用验证
2.3 准备
在准备阶段,JVM会给类对象分配内存空间并设置初始值,初始值为数据类型的默认初始值,如0,0L,false,null等。
2.4 解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
其中符号引用就是字符串常量,它在.class文件中已经存在了,但它们之间只知道彼此的相对位置(偏移量),并不知道自己在内存中的实际位置,而当真正加载到内存中的时候,就会把字符串常量填充到特定的位置上,字符串常量之间的相对位置还是一样的,但此时它们有了实际的内存地址,此时的字符串常量就是直接引用了(Java中的普通引用)。
2.5 初始化
初始化阶段就是针对类对象进行初始化(初始化静态成员,执行静态代码块,如果该类还有父类还需加载它的父类)。
2.6 类加载的时机
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
那么什么是需要的时候呢?
- 创建了这个类的实例
- 使用了这个类的静态方法/静态属性
- 使用子类,也会触发父类的加载
3 类加载器
在介绍双亲委派模型之前,我们需要先知道JVM加载类时需要用到一组特殊的模块,即类加载器。
类加载器(ClassLoader)用于动态加载 Java 类到 Java 虚拟机中。主要有四种类加载器:
-
启动类加载器(Bootstrap ClassLoader)负责加载Java标准库中的类。
-
扩展类加载器(Extension ClassLoader):负责加载一些非标准的但是Sum/Oracle扩展的类
-
应用程序类加载器(Application ClassLoader):负责加载项目中自己写的类以及第三方库中的类。
-
用户自定义类加载器 (User-Defined ClassLoader),我们可以通过继承java.lang.ClassLoader类来创建自己的类加载器。
4.双亲委派模型
在上面我们介绍了JVM类加载的过程,而这就不得不提到一个重要的考点了——双亲委派模型。
它发生在第一步加载中找.class文件的时候,是 Java 类加载机制中的一个重要概念。这种模型指的是一个类加载器在尝试加载某个类时,首先会将加载任务委托给其父类加载器去完成。只有当父类加载器无法完成这个加载请求(即它找不到指定的类)时,子类加载器才会尝试自己去加载这个类。 如下图所示:
- 当一个类加载器需要加载某个类时,它首先会请求其父类加载器加载这个类。
- 这个过程会一直向上递归,也就是说,从子加载器到父加载器,再到更上层的加载器,一直到最顶层的启动类加载器(Bootstrap ClassLoader)。
- 启动类加载器会尝试加载这个类。如果它能够加载这个类,就直接返回;如果它不能加载这个类(因为这个类不在它的搜索范围内),就会将加载任务返回给委托它的子加载器。
- 子加载器接着尝试加载这个类。如果子加载器也无法加载这个类,它就会继续向下传递这个加载任务,依此类推。
- 这个过程会继续,直到某个加载器能够加载这个类,或者所有加载器都无法加载这个类,最终抛出 ClassNotFoundException。
5.JVM中的垃圾回收策略
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存爆掉。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
而垃圾回收之前我们需要先清楚谁是垃圾,谁不是垃圾,即GC分为了两步,先是找谁是垃圾,再对垃圾进行释放。
5.1 找谁是垃圾
常用的垃圾判断算法有引用计数算法,可达性分析算法。
5.1.1 引用计数法
引用计数法:给对象增加一个引用计数器,每当有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1,任何时候计数器为 0 的对象就是不可能再被使用的。
优点:实现简单,效率高
缺点:无法解决循环引用的问题
5.1.2 可达性分析法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,即从 GC Roots 到该对象节点不可达,则证明该对象是需要垃圾收集的。
可以作为GC Roots的对象:
- 栈上的局部变量(每个栈的每个局部变量都是起点)
- 常量池中引用的对象
- 方法区中,静态变量引用的对象
5.2 释放垃圾
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是进行垃圾回收,如何高效地进行垃圾回收呢?
5.2.1 标记清除算法
标记清除算法,分为 2 部分,先把内存区域中的这些对象进行标记,哪些属于可回收的标记出来,然后把这些垃圾拎出来清理掉。
优点:实现简单。
缺点:产生大量不连续的内存碎片。当我们想申请一块连续的较大的内存时,可能就申请不到。
5.2.2 复制算法
在标记清除算法上演化而来的,用于解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
优点:执行效率高,没有内存碎片的问题。
缺点:空间利用率低,因为复制算法每次只能使用一半的内存。
5.2.3 标记整理算法
标记整理算法,标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
优点:解决了内存碎片问题,比复制算法空间利用率高。
缺点:因为有局部对象移动,所以效率不是很高。
5.2.4 分代算法
当前虚拟机的垃圾收集都采用分代收集算法,是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
由于新生代存放的大部分数据是朝生夕死的,所以新生代使用的是效率最高的复制算法;而老生代使用的是标记-清除或标记-整理算法,如果标记-清除可以满足需要那么就使用效率更好的标记-清除算法,如果标记-清除算法不能满足需要就使用标记-整理算法。
今天的分享到这里就结束了,感谢支持!