内存区域划分
栈
- Java虚拟机栈(Java Virtual Machine Stacks):
- 作用:存储局部变量和部分方法信息,每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 特点:每个线程都有自己的栈,栈的大小可以动态调整。
- 本地方法栈(Native Method Stack):
- 作用:与虚拟机栈类似,用于支持Native方法的执行,其中存储的是Native方法的信息。
堆
Java堆(Java Heap):
- 作用:存储对象实例。所有线程共享的一块内存区域。
- 特点:Java堆的内存空间在程序启动时就被预先分配好,并且可以动态地扩展或缩小。
程序计数器
程序计数器(Program Counter Register):
- 作用:用于线程之间的指令协同,记录当前线程执行的字节码指令地址。
- 特点:每个线程都有独立的程序计数器,线程切换时切换到相应线程的计数器。
方法区
方法区(Method Area):
- 作用:存储类的结构信息,如类的字段、方法信息、构造器信息,也包括运行时常量池(Runtime Constant Pool)。
- 特点:方法区也是线程共享的。
类加载
Java 程序启动的时候,JVM 就会把 .class 文件读进内存并进行一系列后续工作
- 加载:找到 .class 文件->打开文件->读文件->创建空的类对象
- 链接
- 验证:检查 .class 文件格式是否符合规范要求
- 准备:给静态变量分配内存空间,空间内填充零值
- 解析:把字符串常量进行初始化,把符号引用替换成直接引用(编译过程中,编译器会使用一些特殊的符号来分别表示字符串常量,等到类加载的时候,才就会把这些字符串常量放到内存中,用对应的内存地址替换刚才的特殊符号)
- 初始化:会对类的静态成员进行初始化,执行静态代码块,如果这个类的父类还没加载,也要去加载父类
双亲委派模型
描述的是类加载中的加载阶段,也就是上述提到的第1阶段,去哪些目录找 .class 文件
JVM 存在一个特殊的模块——类加载器,负责完成类加载工作
JVM 自带三个类加载器:
- BootStrapClassLoader 负责加载标准库中的类
- ExtensionClassLoader 负责加载一些扩展的类
- ApplicationClassLoader 负责加载应用程序自己写的类
JVM 给这三个类加载器约定了父子关系:BootStrapClassLoader 是 ExtensionClassLoader 的父亲,ExtensionClassLoader 是 ApplicationClassLoader 的父亲。
双亲委派模型就是,一个类加载器在加载类时会先委托给它的父类加载器去尝试加载,只有在父类加载器无法加载时,才会由子类加载器自行加载。具体步骤如下
- 检查父类加载器是否已经加载过该类:当一个类加载器收到加载类的请求时,它首先会检查父类加载器是否已经加载过这个类。如果父类加载器已经加载过,就直接返回父类加载器加载的类,不再进行加载。
- 委派给父类加载器:如果父类加载器没有加载过该类,那么该类加载器将加载请求委派给其父类加载器。这一过程会一直持续,直到达到Bootstrap ClassLoader(引导类加载器)为止。
- 尝试自己加载:如果所有的父类加载器都无法加载该类,那么该类加载器将尝试自己加载这个类。如果加载成功,就返回给请求加载的类;如果加载失败,就抛出ClassNotFoundException异常。
这种层次化的类加载机制保证了类的唯一性,避免了类的重复加载
垃圾回收机制
简介
GC 是 “Garbage Collection”(垃圾回收)的缩写,它是一种自动内存管理的机制,用于自动检测和回收程序中不再使用的内存,防止内存泄漏和提高内存利用率。垃圾回收由 JVM 负责。
在程序执行过程中,对象被创建在堆内存中,而垃圾回收的目标是识别和清理不再被程序引用的对象,释放它们占用的内存空间。这样,程序就能够动态地管理内存,无需开发人员手动释放内存。
如何判断一个对象是不是垃圾
-
引用计数
使用额外的计数器,记录某个对象有多少个引用指向它,如果引用为 0,就说明这个对象是垃圾了,C++ 中的
shared_ptr
用的就是这个方法缺点:
- 多线程中,需要修改同一个引用计数,需要考虑线程安全问题。
- 如果对象小,数量多,引用计数会造成不小空间开销
- 可能会带来循环引用问题,比如 C++ 中的
shared_ptr
就有这个问题,引入weak_ptr
才得以解决
-
可达性分析算法,这是 Java 采用的方案
在 Java 虚拟机中,可达性分析算法通常采用根搜索算法(Root Search Algorithm),从一组称为"根"的起始对象出发,通过引用关系逐步遍历对象图,标记所有可以被直接或间接引用到的对象,在标记完活动对象之后,清理掉没有被标记的对象,将它们回收释放内存。
什么样的对象被称为根?通常是程序中能直接访问的对象,如:
- 栈里的引用所指向的对象
- 常量池中对应的对象
- 方法区中,静态引用类型的成员
其实就是个暴力算法,把所有能访问的都访问一遍而已。
如何回收垃圾
-
标记-清除
直接清除垃圾,但是会造成大量的内存碎片
-
复制算法
将内存分为两半,用一半留一半,回收垃圾前,把要保留的对象复制到另一半,然后把之前的一半空间全部释放。
虽然解决了内存碎片问题,但是可用空间减少了一半,空间利用率大大降低
-
标记-整理
和顺序表删除元素一样,把要保留的对象都往前搬。
虽然提高了内存利用率,也降低了内存碎片的情况,但是这种方法比较耗时
-
分代回收
综合上述方案。在不同的场景下,使用不同的方案。
给对象引入一个“年龄”概念,年龄即该对象被 GC 扫描过的轮次
把年龄小的对象(新生代)和年龄大的对象(老年代)放到不同的内存区域中,并且使用不同的回收算法
新生代的对象生命周期短(经验规律),采用复制算法能够迅速回收不再使用的对象;而老年代的对象生命周期较长,采用标记-整理的算法来处理。
新生代的内存布局通常包括一个伊甸区、两个幸存区
具体步骤:
-
新的对象放到伊甸区
-
伊甸区中的对象绝大部分活不过一轮 GC,活下来的放到幸存区中。
从伊甸区放到幸存区,使用复制算法,因为存活的对象少,复制的开销小,而且幸存区的大小也比较小,对空间利用率的影响也不大。
-
幸存区中的对象经过一轮 GC,使用复制算法把活下来的放到另一个幸存区中
-
当一个对象在幸存区中,经历了多轮 GC 后仍然存活,那么这个对象就会晋升为老年代
-
老年代对象,被 GC 扫描的频率会降低。如果老年代对象变成垃圾,那么就使用标记整理算法来处理,因为老年代对象被回收的频率不高,使用标记整理算法的开销就在可接受范围内了。
例外:对于特别大的对象,不必经历上述分代过程,直接进入老年代。因为大对象不适合频繁复制。
-