JVM 内存模型与垃圾回收机制详解
jvm内存模型
- 元空间(Metaspace):JDK1.8之后取代PermGen(永久代),存储类元数据,包括类的结构、方法、常量池、字段信息、静态变量等,不再使用JVM 堆内存,而是直接使用本地内存(也就是操作系统的内存)。因此Metaspace可以扩展到更大的内存空间,而不受堆内存限制。
- 堆(Heap):堆分为年轻代(Young Generation)和 老年代(Old Generation)。
- 栈(Stack):每个线程都有一个私有的栈,栈中存储的是栈帧。栈帧包含:局部变量表(存储方法的参数、局部变量、返回值等),操作数栈(执行方法的计算和操作), 动态链接(方法调用时的符号引用),方法返回地址(用于标识方法执行完毕后返回的地址)。
- 本地方法栈(Native Stack):与栈类似,但是它存储的是 Native 方法(即由 Java 调用的本地方法)的相关信息。
- 程序计数器(PC Register):每个线程都有一个独立的程序计数器,用于记录当前线程执行的 字节码指令的地址。在多线程环境下,JVM 使用该寄存器来追踪线程执行的位置。
类加载器
启动类加载器(Bootstrap ClassLoader)<-扩展类加载器(Extension ClassLoader)<-系统类加载器(System ClassLoader)< -自定义类加载器(Custom ClassLoader)
启动类加载器(Bootstrap ClassLoader)
作用:负责加载JRE(Java Runtime Environment)中的核心类库,通常是JDK中的rt.jar(包含了所有的核心Java类,如java.lang.*包中的类)。
实现:启动类加载器是由本地代码实现的,因此它是JVM中的最顶层加载器,并且通常是一个由C++编写的本地类加载器。它并不是java.lang.ClassLoader类的子类。
职责:加载JDK内部的核心类,像java.lang.Object、java.lang.String、java.util.* 等。
扩展类加载器(Extension ClassLoader)
作用:负责加载JRE中的扩展库,即ext目录中的类库。扩展库通常位于$JAVA_HOME/lib/ext目录下,或者是由java.ext.dirs系统属性指定的路径。
实现:扩展类加载器是由 ClassLoader 类的子类 URLClassLoader 实现的,并且它会依赖于启动类加载器。
职责:加载扩展 JDK 类库(比如 javax.、org.xml. 等)。
系统类加载器(System ClassLoader)
作用:也叫做应用类加载器,负责加载应用程序的类路径(classpath)中指定的类。
实现:系统类加载器是ClassLoader类的一个实例,它通过读取classpath中指定的路径加载类文件。通常这些类是我们在项目中开发的类。
职责:加载应用程序中的类,通常由-classpath或-cp参数指定,或者是CLASSPATH环境变量指定的路径。
自定义类加载器(Custom ClassLoader)
作用:开发者可以自定义类加载器以满足特定的需求。通常情况下,自定义类加载器可以扩展ClassLoader类,并重写findClass和loadClass方法来实现自定义的类加载行为。
实现:自定义类加载器通常用于动态加载某些特定路径下的类,或者在特殊环境下加载类,例如,JSP的动态加载、插件机制、Web容器的类加载等。
职责:通过重写类加载的逻辑来满足特定应用的需求,如加载特定路径的类、支持热部署等。
双亲委派模型
工作原理如下:
类加载请求:当一个类加载器接收到加载某个类的请求时,它不会立即去加载该类,而是会将这个请求委托给它的父类加载器先去处理。
父类加载器加载:父类加载器会尝试加载这个类。如果父类加载器可以成功加载该类,它就直接返回该类。如果父类加载器不能加载该类,它就会将加载请求委托给它的父类加载器,直到最终委托到 启动类加载器(Bootstrap ClassLoader) 为止。
加载成功:一旦某个加载器成功加载了这个类,它会将类加载到JVM内存中,并返回该类。
当前加载器加载:如果父类加载器无法加载该类(即父类加载器没有找到该类),那么当前类加载器会尝试自己加载这个类。
优点:
避免重复加载类:确保类在整个JVM中只加载一次,避免了不同类加载器加载同一个类的冲突。
保证核心类的安全性:通过将核心类(如 java.lang.* 包中的类)委派给顶层的启动类加载器加载,防止了用户自定义类加载器覆盖核心类。
增强了系统的可扩展性:通过这种层次化加载的机制,Java可以方便地通过不同的类加载器来加载不同来源的类,实现模块化和插件化的设计。
引用类型有哪些?
分为强软弱虚四种
- 强引用:最常见的引用类型,例如:new Object(),强引用关联的对象,一定不会被GC
- 软引用:SoftReference,在内存紧张时可能被回收
- 弱引用:WeakReference,垃圾回收时一定会被回收
- 虚引用(幻影引用):PhantomReference,必须与 引用队列 (ReferenceQueue) 一起使用,用来在对象被垃圾回收前做一些清理工作,垃圾回收时一定会被回收
垃圾回收算法
标记-清除算法(Mark-and-Sweep)
分为两个阶段
- 标记所有活跃的对象,即从根对象(GC Roots)开始遍历,标记所有可达的对象。
- 遍历堆内存,回收所有没有被标记的对象(即不可达对象)。
优点:
- 实现简单,容易理解。
- 适用于所有类型的垃圾回收。
缺点:
- 效率低:标记阶段和清除阶段的遍历需要分别一次,整个过程效率较低。
- 内存碎片:清除后的内存空间可能不连续,导致内存碎片。
复制算法(Copying)
通过将堆内存划分为两部分:一个活跃区(使用区)和一个空闲区。每次进行垃圾回收时,只有一个区域使用,另一个区域空闲。垃圾回收时,会将存活的对象从当前区域复制到另一个空闲区域。
优点:
- 没有内存碎片,因为只会将存活对象复制到空闲区域。
- 清理速度较快,因为只需遍历一部分内存。
缺点:
- 需要额外的内存空间(必须有一块空闲的区域)。
- 对大对象不友好,复制过程可能造成额外的性能开销。
应用:
年轻代的垃圾回收:在JVM中,年轻代通常采用复制算法,分为From Space和To Space。年轻代垃圾回收(Minor GC)时,将存活的对象从 From Space复制到To Space。
标记-整理算法(Mark-Compact)
标记-清除算法的改进版。它在标记阶段与标记-清除算法一样,标记所有存活的对象。然后在整理阶段,将存活对象压缩到堆的一端,清理掉堆的另一端的内存空间。
优点:
- 不会产生内存碎片,因为所有存活对象会被移动到堆的一端。
- 回收过程比标记-清除算法更加高效。
缺点:
- 对存活对象的移动会带来额外的性能开销。
- 需要重新调整对象的引用。
应用:
老年代的垃圾回收:在老年代的垃圾回收中,JVM可以采用标记-整理算法。标记-整理算法通过压缩对象,避免内存碎片的产生。
分代收集算法(Generational Collection)
分代收集算法是现代垃圾回收算法的核心思想,基于大多数对象很快就变得不可达这一事实,JVM将堆内存划分为多个区域(一般分为 年轻代 和 老年代)进行垃圾回收。分代收集算法结合了复制算法和标记-清除算法,根据不同的内存区域选择最合适的回收策略。
优点:
- 根据对象的生命周期和存活率,合理分配资源。年轻代和老年代使用不同的垃圾回收策略。
- 提高了垃圾回收的效率,减少了暂停时间。
缺点:
- 对大对象不友好,可能导致内存碎片。
- 较复杂的内存管理,需要管理多个区域。
应用:
- 年轻代:通常采用 复制算法,因为大多数对象在创建后很快就会变得不可达。
- 老年代:通常采用 标记-清除 或 标记-整理算法,因为大多数对象在老年代存活时间较长。
垃圾回收器
G1 的回收阶段:
- 初始标记:标记出所有GC Root可达的对象,会STW
- 并发标记:G1会并发地标记所有从GC Root可达的对象,包括可达对象的引用链,不会STW
- 最终标记:G1 会修正并发标记阶段可能出现的变动(例如对象的引用关系变化),会STW
- 筛选回收:标记-复制,其中转移阶段需要分配新内存和复制对象的成员变量。转移阶段会STW