目录
- 内存区域划分
-
- 堆(Heap)
- [方法区(Method Area)](#方法区(Method Area))
- [程序计数器(Program Counter Register)](#程序计数器(Program Counter Register))
- [虚拟机栈(VM Stack)](#虚拟机栈(VM Stack))
- [本地方法栈(Native Method Stack)](#本地方法栈(Native Method Stack))
- 类加载的过程
- 垃圾回收机制
-
- [1. 找出垃圾](#1. 找出垃圾)
- [2. 释放垃圾的内存空间](#2. 释放垃圾的内存空间)
JVM(Java虚拟机)是Java Virtual Machine的缩写,是一种可以执行Java字节码的虚拟机。它为Java应用程序提供了一个与平台无关的执行环境,使得Java程序只需编写一次,就可以在安装了JVM的任何系统上运行
本文主要介绍三个部分:JVM内存区域划分、类加载的过程、垃圾回收机制
内存区域划分
运行时数据区划分如下
堆(Heap)
堆是JVM中最大的一块内存区域,主要用于存储对象实例,new出来的对象变量都是存储在栈上。堆是所有线程共享的内存区域(只有一份),并且是垃圾回收器的主要工作区域。堆内存大概分为以下几个部分:
- 新生代(Young Generation):大多数对象的生命周期都很短,因此年轻代被划分为三个部分:Eden区、Survivor0区(S0)、Survivor1区(S1)。对象通常在Eden区被创建,经过一次垃圾回收后,存活的对象会被移动到S0或S1区,每经过一次垃圾回收,存活的对象就会在S0和S1之间移动,直到晋升到老年代。
- 老年代(Old Generation) :在年轻代中经历了多次垃圾回收仍然存活的对象,以及一些大对象(如大数组),会被移动到老年代。老年代的垃圾回收频率较低,因为这里存放的都是生命周期较长的对象。
方法区(Method Area)
方法区是所有线程共享的内存区域(只有一份),用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register)
程序计数器是一块小的内存空间,它为每个线程私有。程序计数器保存了当前执行的字节码指令的地址(类似于咱们写代码时的行号)。在JVM中,字节码解释器通过改变程序计数器的值来控制执行流程。
程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依靠这个计数器完成。
虚拟机栈(VM Stack)
虚拟机栈也是线程私有的内存区域,它的生命周期与线程相同。虚拟机栈用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量和操作数栈,当方法执行结束时,栈帧会被销毁。
-
局部变量表(Local Variables):存储方法中的局部变量。
-
操作数栈(Operand Stack):用于存储计算过程中的中间数据。
-
动态链接(Dynamic Linking):将符号引用转换为直接引用。
-
方法出口(Method Exit):用于方法正常退出或异常退出时的清理工作。
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈类似,Java虚拟机栈是给JVM使用的,而本地方法栈是给本地方法使用的。
提问:下面变量(a、b、c、d、e、f)属于哪个内存区域?
java
class Test {
private int a;
private Test b = new Test();
private static int c;
private static Test d = new Test();
public static void main(String[] args) {
int e = 1;
Test f = new Test();
}
}
答:a、b属于堆,c、d属于方法区,e、f属于栈
变量属于哪个区和变量是否为内置类型无关
局部变量==>栈
成员变量==>堆
静态成员变量==>方法区
类加载的过程
JVM类加载是Java程序运行的基础,它涉及将类的.class文件从磁盘加载到内存中,并进行校验、链接和初始化等一系列步骤。
类加载过程
-
加载(Loading):
-
类加载过程的第一步,JVM通过类的全限定名获取其定义这个类的二进制字节流(例如,从.class文件、JAR文件或者网络中获取)。
-
将这个字节流所代表的静态存储结构转化为JVM内部运行时的数据结构。
-
在内存中创建一个代表该类的
java.lang.Class
对象,这个对象将作为程序访问这个类的接口。简单来说就是:根据类名找到对应的.class文件,打开文件并读取内容。
-
-
验证(Verification) :
确保读取到的.class文件符合JVM规范,没有安全问题。包括文件格式验证、元数据验证、字节码验证和符号引用验证,确保类的信息是合法的,不会危害虚拟机的安全。
-
准备(Preparation) :
为类对象分配内存空间,根据读取到的.class文件内容,确定类对象所需要的空间大小,申请出对应大小的空间,并设置默认初始值(通常是0)。
-
解析(Resolution) :
将类、接口、字段和方法的符号引用转换为直接引用。符号引用就是类、字段、方法的全限定名,直接引用保存的是内存中的地址。
-
初始化(Initialization) :
执行类的静态初始化和静态变量的赋值操作,执行类中的静态代码块。
类加载器
JVM内置了几种主要的类加载器:
-
启动类加载器(Bootstrap ClassLoader):
负责加载标准库中的类。
-
扩展类加载器(Extension ClassLoader):
负责加载扩展类。
-
应用程序类加载器(Application ClassLoader):
负责加载第三方库中的类、自己写的代码中的类。
双亲委派模型
双亲委派模型(Parent Delegation Model)是Java虚拟机(JVM)中的一种类加载机制。它的核心思想是,当一个类加载器收到类加载请求时,会先委托其父类加载器去完成这个请求,只有在父类加载器无法完成时,自己才会尝试去加载这个类。
双亲委派模型是类加载5个步骤中第一个步骤中的环节,
工作流程:通过得到的全限定名,
- 从Application ClassLoader开始进行加载,此时不会立刻扫描目录,而是把任务委派给Extension ClassLoader
- Extension ClassLoader拿到任务后,也不会立刻扫描目录,而是把任务委派给Bootstrap ClassLoader
- Bootstrap ClassLoader在目录中进行扫描加载对应的类,如果Bootstrap ClassLoader没找到对应的类,就把任务返回给Extension ClassLoader,如果找到了直接进行后续的类加载过程。类似的,如果Extension ClassLoader没找到,会继续吧任务返回给Application ClassLoader,如果Application ClassLoader找到了直接返回,如果Application ClassLoader没找到,就会抛出异常
类加载请求首先由父类加载器尝试完成,只有在父类加载器无法完成类加载时,才由子类加载器尝试自己去加载。
双亲委派模型机制就是为了防止用户自己定义的类和标准库的类名一样,把标准库的类覆盖掉。
垃圾回收机制
JVM的垃圾回收机制(Garbage Collection, GC)是Java内存管理的重要组成部分,主要目标是自动回收不再使用的对象,释放内存资源,避免内存泄漏和溢出。垃圾回收机制主要工作区域是堆。
GC是如何回收的?
1. 找出垃圾
方案1:给每个对象分配一个计数器,计数器记录的是当前有多少个变量引用了这个对象,每增加一个引用计数器就+1,每减少一个引用就-1。如果计数器为0,表示当前对象没有人引用它,说明这个对象就是垃圾。这个方案存在问题循环引用问题,并且会消耗额外的内存空间,JVM没有采用。
循环引用问题:
java
class Test {
Test t = null;
}
//..........
class Main {
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;
}
}
//这两个对象引用计数器为0不能被释放,但是这两个对象又无法使用
方案2:可达性分析(JVM采用的方案)
JVM会从一组根对象(GCroot)开始,遍历所有可达对象(类似于二叉树、多叉树遍历)。如果某个对象不可通过根对象引用到达,JVM便会将其视为垃圾并回收。常见的GCroot:栈上的局部变量、方法区中的静态成员、常量池中引用指向的对象。
2. 释放垃圾的内存空间
有以下垃圾回收算法
标记-清除算法 :
先标记出所有存活的对象,然后清除未被标记的对象。这种方法简单,但会产生大量内存碎片(不是连续的空间),后续申请内存的时候可能申请不了(申请的空间一定是连续的)。
复制算法 :
将可用内存分为大小相等的两块,每次只使用一块,当一块用完,将存活的对象复制到另一块,然后清理已使用过的内存。这种方法解决了内存碎片问题,但代价是内存使用效率降低,复制成本比较大。
标记-整理算法 :
类似于顺序表删除中间元素的操作。先标记出存活的对象,然后让所有存活的对象向一端移动,之后清理端边界以外的内存区域。这种方法解决了内存碎片问题。这种方法的代价是移动存活对象的开销比较大
分代收集算法(JVM采取的方案) :
根据对象存活周期的不同,将堆分为新生代和老年代。
- 新生代(Young Generation):大多数对象的生命周期都很短,因此年轻代被划分为三个部分:Eden区、Survivor0区(S0)、Survivor1区(S1)。对象通常在Eden区被创建,经过一次垃圾回收后,存活的对象会被移动到S0或S1区,每经过一次垃圾回收,存活的对象就会在S0和S1之间移动,直到晋升到老年代。
- 老年代(Old Generation):在年轻代中经历了多次垃圾回收仍然存活的对象,以及一些大对象(如大数组),会被移动到老年代。老年代的垃圾回收频率较低,因为这里存放的都是生命周期较长的对象。
新生代中的对象生命周期短,适合使用复制算法;老年代中的对象生命周期长,适合使用标记-整理算法。
垃圾回收器
现在常见的垃圾回收器有CMS、G1
CMS(Concurrent Mark-Sweep)垃圾回收器:
减少STW(Stop The World),标记清除算法,并行处理大部分垃圾回收过程。可能产生内存碎片,老年代需要更大内存。
G1(Garbage First)垃圾回收器:
面向服务端的低延迟垃圾回收器,将堆划分为多个区域,优先回收垃圾最多的区域。并行和并发执行,适合大内存应用。