目录
什么是JVM?
JVM 执行流程
JVM 运行时数据区
堆(线程共享)
Java虚拟机栈(线程私有)
什么是线程私有?
程序计数器(线程私有)
方法区(线程共享)
JDK 1.8 元空间的变化
运行时常量池
内存布局中的异常问题
1. Java堆溢出
2. 虚拟机栈和本地方法栈溢出
JVM 类加载
1. 类加载过程
加载
验证
准备
解析
初始化
双亲委派模型
垃圾回收机制
死亡对象的判断算法
引用计数算法
可达性分析算法
垃圾回收算法
标记-清除算法(Mark and Sweep):
复制算法(Copying Garbage Collection):
标记-整理算法(Mark and Compact):
分代算法(Generational Garbage Collection):
什么是JVM?
JVM(Java虚拟机)是Java编程语言的关键组成部分,它是一种虚拟计算机环境,用于执行Java程序。JVM的主要作用是将Java源代码编译成与特定计算机硬件无关的字节码,并在运行时将这些字节码转换为机器码,以便在不同平台上运行Java应用程序。
在JVM的运行环境中,Java程序能够实现跨平台的特性,因为它们不需要直接与底层操作系统进行交互,而是依赖JVM来处理与硬件的交互。这使得Java成为一种高度可移植和可扩展的编程语言。
JVM的关键功能包括:
- 类加载:JVM负责加载Java类的字节码文件,通过类加载器实现这一任务。
- 内存管理:JVM自动管理内存分配和垃圾回收,以确保应用程序不会出现内存泄漏和溢出。
- 字节码执行:JVM解释或编译Java字节码,将其转换为本地机器码以执行应用程序。
- 多线程支持:JVM提供多线程支持,允许并发执行Java应用程序的部分或全部代码。
- 垃圾回收:JVM使用垃圾回收机制来自动释放不再被引用的内存,以提高内存利用率。
JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?
JVM 执行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码 文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调 用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:
1. 类加载器(ClassLoader)
2. 运行时数据区(Runtime Data Area)
3. 执行引擎(Execution Engine)
4. 本地库接口(Native Interface)
JVM 运行时数据区
JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称JMM)完全不同,属于完全不同的两个概念,它由以下5大部分组成:
堆(线程共享)
堆的作用:程序中创建的所有对象都在保存在堆中。
我们常见的 JVM 参数设置 -Xms10m 最小启动内存是针对堆的,-Xmx10m 最大运行内存也是针对堆的。
ms 是 memory start 简称,mx 是 memory max 的简称。
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。
垃圾回收的时候会将 Eden 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。
Java虚拟机栈(线程私有)
Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的 内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
Java 虚拟机栈中包含了以下 4 部分:
1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量 表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局 部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数 和局部变量。
2. 操作栈:每个方法会生成一个先进后出的操作栈。
3. 动态链接:指向运行时常量池的方法引用。
4. 方法返回地址:即在方法执行完成后将控制返回到调用方法的指令位置。方法出口通常用于支持方法调用的返回操作。
什么是线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器 (多核处理器则指的是一个内核) 都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。
程序计数器(线程私有)
程序计数器的作用:用来记录当前线程执行的行号的。
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器值为空。 程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!
方法区(线程共享)
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。
汽车(Java虚拟机规范):在这个比喻中,汽车代表了Java虚拟机规范,它定义了Java虚拟机的基本结构和功能,其中包括了方法区的概念。
动能提供装置(方法区):这相当于汽车的一个重要部分,就像Java虚拟机中的方法区一样。方法区是用于存储类的结构信息、静态变量、常量池、方法的字节码等的内存区域。它提供了Java应用程序所需的关键信息。
发动机和电机(永久代和元空间):在不同类型的汽车中,动能提供装置可以有不同的实现。对于燃油车,它使用发动机作为动能提供装置,而电动汽车使用电机。同样地,Java虚拟机可以使用永久代或元空间来实现方法区。
永久代(PermGen):就像燃油车使用汽油发动机一样,一些早期版本的Java虚拟机使用永久代作为方法区的实现。永久代有一些限制,如固定大小,可能会导致内存溢出问题。
元空间(Metaspace):与之不同,元空间是Java虚拟机规范的一种新实现方式。它更灵活,不再受到永久代的限制,可以动态调整大小,避免了一些与永久代相关的问题。
总之,永久代和元空间都是Java虚拟机规范中对方法区的不同实现方式,就像汽油发动机和电动机都是动能提供装置的不同实现一样。选择使用哪种实现方式取决于Java虚拟机的版本和配置,以及应用程序的需求。这个比喻很好地概括了它们之间的关系。
JDK 1.8 元空间的变化
1. 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内存的参数影响了,而是与本地内存的大小有关。
2. JDK 8 中将字符串常量池移动到了堆中。
运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
内存布局中的异常问题
1. Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来 GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
我们可以设置JVM参数-Xms:设置堆的最小值、-Xmx:设置堆最大值。下面我们来看一个 Java堆OOM的测试,测试以下代码之前先设置 Idea 的启动参数,如下图所示:
JVM 参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
示例:
public class Main {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list =
new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
}
以上程序的执行结果如下:
Java堆内存的OOM异常是实际应用中最常见的内存溢出情况。当出现Java堆内存溢出时,异常堆栈信 息"java.lang.OutOfMemoryError"会进一步提示"Java heap space"。当出现"Java heap space"则很明 确的告知我们,OOM发生在堆上。
此时要对Dump出来的文件进行分析,以MAT为例。分析问题的产生到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
内存泄漏 : 内存泄漏是指在程序中分配了一些内存(通常是用于存储对象或数据),但在后续的程序执行中,无法释放或回收这些内存,导致内存消耗不断增加,最终可能导致程序性能下降或崩溃。
内存溢出 : 内存溢出(Memory Overflow)是指在程序运行时,尝试分配的内存超出了可用的物理内存或虚拟内存的范围,导致程序无法继续正常执行的情况。内存溢出通常是由于程序内存管理不当或程序本身存在缺陷引起的。内存溢出会导致程序崩溃或异常终止。
2. 虚拟机栈和本地方法栈溢出
由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置。
关于虚拟机栈会产生的两种异常:
1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
2. 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常
示例:
观察StackOverFlow异常(单线程环境下)
/**
* JVM参数为:-Xss128k
*/
public class Test {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
Test test = new Test();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("Stack Length: " + test.stackLength);
throw e;
}
}
}
会出现如下运行结果:
出现StackOverflowError异常时有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机默认参数,栈深度在多多数情况下达到1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用。
如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的 方式来换取更多线程。
/**
* JVM参数为:-Xss2M
*/
public class Test {
private void dontStop() {
while(true) {
}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test test = new Test();
test.stackLeakByThread();
}
}
以上代码运行电脑可能会崩,记得保存所有工作~~🤗
JVM 类加载
1. 类加载过程
对于一个类来说,它的生命周期是这样的:
其中前 5 步是固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来说总共分为以下几个步骤:
1. 加载
2. 连接
1. 验证
2. 准备
3. 解析
3. 初始化
下面我们来看看每个步骤的具体执行内容~~😁
加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了。
在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节 流中包含的信息符合《Java虚拟机 规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证选项: 文件格式验证,字节码验证,符号引用验证...
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
比如此时有这样一行代码:
public static int value = 123;
它是初始化 value 的 int 值为 0,而非 123。
解析
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
初始化
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化 阶段就是执行类构造器方法的过程。
双亲委派模型
Java虚拟机(JVM)的双亲委派模型是一种类加载机制,用于确保Java类的加载和安全性。这个模型基于"委派"原则,它指定了JVM在加载类时首先尝试委派给父类加载器,只有在父类加载器无法加载类时才会由子类加载器尝试加载。这个模型有助于确保类的一致性、避免重复加载、并提高安全性。
下面是双亲委派模型的关键概念:
-
父类加载器(Parent Class Loader):在JVM中,有一个层次结构的类加载器链,根类加载器(Bootstrap Class Loader)位于最顶层,然后是扩展类加载器(Extension Class Loader),最后是应用程序类加载器(Application Class Loader)。父类加载器负责加载核心Java类库(如java.lang包中的类),并向下委派加载请求。
-
子类加载器(Child Class Loader):自定义的类加载器通常是子类加载器,它们负责加载应用程序特定的类。当一个类加载器收到加载请求时,它首先将请求委派给其父类加载器,如果父类加载器无法找到类,子类加载器才会尝试加载。
-
双亲委派规则:当类加载器收到加载类的请求时,它首先检查是否已经加载过该类。如果没有,它将委派给其父类加载器进行加载。这一过程递归继续,直到达到根类加载器。如果根类加载器仍然找不到类,那么会由底层的类加载器尝试加载。这种层层委派的方式确保了类的唯一性和一致性。
双亲委派模型的优点包括:
-
避免了类的重复加载:如果一个类已经被一个类加载器加载,那么它不会被另一个类加载器重复加载,这有助于节省内存和资源。
-
安全性:通过限制用户自定义类加载器的能力,双亲委派模型可以确保核心Java类库的安全性,防止恶意代码替代标准类库。
-
保持类的一致性:双亲委派模型可以确保不同类加载器加载的类是相同的,避免了类的混乱和不一致性。
总之,双亲委派模型是Java类加载的关键机制之一,它有助于保持类加载的安全性和一致性,同时提高了性能和资源利用率。在自定义类加载器时,通常建议遵循双亲委派模型,以确保类加载行为的一致性~~😁
启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。
扩展类加载 器。加载 lib/ext 目录下的类。
应用程序类加载器:加载我们写的应用程序。
自定义类加载器:根据自己的需求定制类加载器。
垃圾回收机制
垃圾回收机制(Garbage Collection,简称GC)是Java虚拟机(JVM)的一个关键特性,用于管理和回收不再被程序使用的内存,以确保内存的有效使用和防止内存泄漏。垃圾回收机制的主要目标是自动释放不再引用的对象,以便释放内存并减少内存泄漏的风险。
在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。
死亡对象的判断算法
引用计数算法
每个对象都带有一个引用计数器,每当有一个引用指向对象时,计数器加1,每当引用不再指向对象时,计数器减1。当对象的引用计数器减到零时,表示对象不再被引用,即可认为该对象是垃圾,可以被回收。
引用计数法的一些关键概念和示例:
引用计数器:每个对象都包含一个整数计数器,用于记录对象被引用的次数。
引用操作:当新的引用指向对象时,引用计数器加1;当引用不再指向对象时,引用计数器减1。
垃圾对象:当一个对象的引用计数器降到零时,表示对象不再被引用,可以被认为是垃圾对象,即可被回收。
内存回收:引用计数法的内存回收是实时的,即当引用计数器降到零时,立即回收对象所占用的内存。
但是JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题,下面是一个循环引用示例
class Node {
Node next;
Node() {
next = null;
}
void setNext(Node node) {
next = node;
}
}
public class ReferenceCycleExample {
public static void main(String[] args) {
Node node1 = new Node();
Node node2 = new Node();
// 创建循环引用
node1.setNext(node2);
node2.setNext(node1);
// 现在node1和node2互相引用,但引用计数法无法检测到这一点
// 尝试释放引用
node1 = null;
node2 = null;
// 即使没有引用指向这两个对象,它们仍然相互引用,无法被回收,造成内存泄漏
}
}
在上述示例中,Node
类表示一个简单的链表节点,其中的next
字段指向下一个节点。在main
方法中,我们创建了两个节点node1
和node2
,并通过setNext
方法相互引用,形成了循环引用。尽管在后续代码中将node1
和node2
设置为null
,但它们仍然相互引用,因此无法被引用计数法正确回收,导致内存泄漏。
这个示例突出了引用计数法的一个主要问题:它无法处理循环引用的情况。因为引用计数法只关注引用计数,而不考虑对象之间的引用关系,因此循环引用可能导致对象的引用计数永远不会降为零,即使对象已经不再被程序使用,也无法被正确回收。这就是为什么现代垃圾回收器通常使用基于可达性分析的算法,如标记-清除,以解决这个问题。
可达性分析算法
可达性分析算法是JVM用于标记和回收不再被程序引用的对象的核心机制。它通过以下步骤来确定哪些对象仍然是可达的,哪些是不可达的:
根集合(Root Set):JVM从一组根对象开始,这些根对象包括程序的主方法中的局部变量、静态变量(类变量)中引用的对象以及已经在执行过程中被分配的对象。
标记阶段:在标记阶段,JVM从根集合出发,通过引用链遍历对象,将它们标记为活动对象(可达)。引用链包括字段、数组元素、方法参数等。如果对象可以通过引用链访问到,它被认为是可达的。
清除阶段:在标记阶段之后,未被标记为活动对象的对象被认为是不可达的垃圾,将被回收。这些对象占用的内存将被释放,以供将来分配新对象。
内存区域
对于可达性分析算法,主要关注堆内存的分配和回收。
堆内存(Heap):堆是用于存储对象的主要内存区域。在可达性分析算法中,堆被划分为不同的代,通常包括新生代(Young Generation)、幸存区(Survivor Space)、老年代(Old Generation)。
新生代:新分配的对象通常位于新生代。在新生代中,有三个区域:一个Eden区和两个幸存区(通常称为S0和S1)。对象首先分配到Eden区,然后在垃圾回收发生时,存活的对象会被移动到幸存区。幸存区之间也会发生对象的复制。
老年代:在对象经历多次幸存区的复制后,它们最终会被晋升到老年代。老年代主要用于存储生命周期较长的对象。
栈内存(Stack):栈内存用于存储方法调用的局部变量、操作数栈和方法调用的返回地址。它的生命周期与方法的调用和返回密切相关,不用于存储对象。
可达性分析与内存区域的关系
可达性分析算法主要影响堆内存的使用和回收。当可达性分析算法标记不再被引用的对象为垃圾时,这些对象所占用的堆内存将被释放。可达性分析还帮助JVM确定哪些对象需要在不同代之间进行移动,以优化内存使用。
新生代和老年代之间的对象移动与可达性分析密切相关,因为幸存区中的对象在不同代之间移动。幸存区的复制和清理操作是为了确保年轻代内存的高效利用。老年代的管理也依赖于可达性分析,以及在必要时执行Full GC(Full Garbage Collection)来回收老年代的垃圾。
垃圾回收算法
垃圾回收算法是Java虚拟机(JVM)用于管理内存中对象的方法,以便回收不再被引用的对象,从而释放内存资源。不同的垃圾回收算法适用于不同的情况和内存分配模式。以下是四种常见的垃圾回收算法的详细讲解:
标记-清除算法(Mark and Sweep):
-
标记阶段:在此阶段,垃圾回收器从一组根对象(通常包括程序的主方法中的局部变量、静态变量等)出发,通过引用链遍历对象图,并将可达对象标记为活动对象。这些对象不会被回收。
-
清除阶段:在标记阶段之后,所有未被标记为活动对象的对象被认为是垃圾,将被回收。此时,垃圾回收器会释放垃圾对象占用的内存。
-
优点:简单明了,适用于任何内存分配模式。
-
缺点:可能会产生内存碎片,回收效率较低,会引发应用程序停顿。
像这样子我们把上面的黑色垃圾都打上标记,垃圾回收器就会释放这些内存。
复制算法(Copying Garbage Collection):
-
新生代划分:堆内存被划分为两个相等的区域,通常称为Eden区和幸存区(S0和S1)。
-
标记和复制阶段:新对象首先分配到Eden区。当Eden区满时,执行标记和复制阶段。在这个阶段,垃圾回收器标记所有活动对象,然后将它们复制到另一个幸存区。幸存区之间也会进行对象的复制。
-
清理阶段:清理阶段将不再被引用的对象所占用的区域标记为空闲,并将Eden区和一个幸存区的对象互换。
-
优点:有效避免了内存碎片,回收效率高,适用于新生代的短生命周期对象。
-
缺点:需要额外的内存来进行复制操作,老年代对象无法直接受益。
如上示例,假如a和c对象被认为是垃圾,它会把剩下的对象(我们这个示例只剩下了b对象)先复制一份到s0上,然后把Eden一整个区域内存全部释放,这样有效避免了内存碎片,回收效率高,适用于新生代的短生命周期对象。
标记-整理算法(Mark and Compact):
-
标记阶段:与标记-清除算法相同,标记所有可达对象。
-
整理阶段:在清理阶段,不会简单地清除垃圾对象,而是将存活的对象向一端移动,然后清除掉未移动的垃圾对象。这样可以减少内存碎片。
-
优点:避免了内存碎片,回收效率高,适用于堆中包含较多长生命周期对象的情况。
-
缺点:需要对象移动操作,可能引发一定的停顿。
分代算法(Generational Garbage Collection):
-
堆分代:堆内存被分为不同的代,通常包括新生代和老年代。新分配的对象通常位于新生代。
-
新生代回收:在新生代中,通常采用复制算法,因为新生代的对象生命周期短。垃圾回收频繁发生,但效率高。
-
老年代回收:在老年代中,通常采用标记-清除或标记-整理算法,因为老年代的对象生命周期较长。垃圾回收不太频繁,但可能导致较长的停顿。
-
优点:根据对象的生命周期进行不同的回收,提高了效率和响应时间。
-
缺点:需要管理不同代的对象,引入了复杂性。
当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
新生代(Young Generation):
特点:新生代是堆内存的一部分,通常用于存储具有较短生命周期的对象。由于大多数对象在创建后很快就变得不可达,因此新生代采用了一种高效的垃圾回收策略。
回收策略:新生代通常采用复制算法(Copying Garbage Collection)作为主要的垃圾回收策略。这意味着新生代被划分为三个区域:Eden区和两个幸存区(S0和S1)。新对象首先分配到Eden区。当Eden区满时,发生垃圾回收,将存活的对象复制到其中一个幸存区,同时清理掉Eden区。随着垃圾回收的进行,对象会在幸存区之间进行复制,最终存活的对象会晋升到老年代。
优点:新生代的复制算法有效避免了内存碎片,回收效率高。
缺点:复制算法需要额外的内存来存储复制后的对象,老年代对象无法直接受益,可能导致内存使用效率不高。
老年代(Old Generation):
特点:老年代用于存储具有较长生命周期的对象,这些对象在多次垃圾回收后仍然存活。
回收策略:老年代通常采用标记-清除(Mark and Sweep)或标记-整理(Mark and Compact)算法作为主要的垃圾回收策略。这些算法涉及标记不再被引用的对象,然后清除或整理内存。
标记-清除算法(Mark and Sweep):标记不再被引用的对象,然后清除垃圾对象,但可能会导致内存碎片。
标记-整理算法(Mark and Compact):标记不再被引用的对象,然后将存活的对象向一端移动,清除未移动的垃圾对象,以减少内存碎片。
优点:老年代的标记-清除或标记-整理算法能够处理具有较长生命周期的对象,并在垃圾回收时避免内存碎片。
缺点:可能引发较长的垃圾回收停顿,影响应用程序的响应时间。
写在最后
通过本博客,我们深入探讨了Java虚拟机(JVM)的工作原理和内部机制。我们从JVM的基本概念开始,了解了其执行流程和运行时数据区域的重要性。我们探讨了JVM内存管理的关键方面,包括堆内存、栈内存、方法区、运行时常量池等,以及Java堆溢出和虚拟机栈溢出等内存问题的解决方法。
我们还深入研究了JVM的类加载过程,了解了加载、验证、准备、解析和初始化等步骤,以及双亲委派模型的重要性。此外,我们介绍了JDK 1.8中元空间的变化和垃圾回收机制的工作原理。
在垃圾回收方面,我们讨论了死亡对象的判断算法,引用计数算法和可达性分析算法等关键概念。我们还介绍了不同的垃圾回收算法,包括标记-清除、复制、标记-整理和分代算法,以及它们在不同内存区域的应用。
最后,我们希望这篇博客能够帮助您更深入地理解Java虚拟机的内部工作原理,从而更好地编写高性能和可靠的Java应用程序。JVM作为Java生态系统的核心组件,其深入理解对于Java开发人员来说是至关重要的。感谢您的阅读!😁