垃圾回收
垃圾回收算法
- 引用计数算法(循环引用会导致内存不会被释放,即内存溢出)
- 可达性分析算法:通过一系列的 GC roots 为起点,遍历所有能够从这些根对象到达的对象。如果一个对象从任何 GC roots 都不可达,那么认为这个对象时垃圾对象,可以被 GC 回收,
- GC roots
- 虚拟机栈中引用的对象(主要是局部变量表)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- GC roots
如何回收
- 标记-清除算法 (内存碎片过多)
- 复制算法
- 标记整理算法
- 分代收集算法
- Java 堆(Java Heap)分为新生代和老年代
- 新生代(Young Generation) --> 大批对象死去,少量存活(复制算法)
- Eden
- Survivor
- From
- To
- 老 年代(Old Generation):对象存活率高,没有额外空间(标记-清除算法、标记整理算法)
- 新生代(Young Generation) --> 大批对象死去,少量存活(复制算法)
- Java 堆(Java Heap)分为新生代和老年代
minor gc
- 清空 Eden 区
- 将未被回收的对象转移到 Survivor 的 From 区,如果 From 区空间不足,则直接放到 Old 区。
- 将 Survivor 的 From 区未被回收对象转移到 To 区。如果 To 区空间不足,则直接放到 Old 区。
Survivor 存在的意义
为什么新生代发生 minor gc 后,不把未被回收的对象直接转移到老年代呢?
因为一个对象在很大概率下不会存活很久,躲过第一次 minor gc,并不代表能躲过第二次,第三次。所以可以先把这个对象放到 survivor 的 from 区暂存下。 只有经历 16 次 minor gc 还能存活对象才会被转移到老年代。
survivor 是如何解决内存碎片呢?
首先 survivor 分为 from 区 和 to 区。
minor gc 时,会把 eden 区 和 survivor 的 from 区存活的对象转移到 survivor 的 to 区。 此时。form 区是空的,而 to 区也没有内存碎片。
将 from 区和 to 区的职责对换,即 to 区变成了 from 区,from 区变成了 to 区。
内存担保机制
这通常是指 JVM 在执行垃圾收集时用来确保有足够的内存空间为新生成的对象分配空间的一种策略。 当 JVM 无法确定是否有足够的空间为新对象分配内存时,就会触发垃圾收集来释放内存。 内存担保机制确保了系统的稳定性,防止了因内存耗尽导致的程序崩溃
老年代
占堆内存的 2/3
只有在 major gc 的时候才会进行清理,每次 GC 会触发 stop-the-world。内存越大,stw 的时间越长。采用的是 标记整理算法。
下面的对象会直接转移到 Old 区。
- 大对象:需要大量连续空间的对象。(避免
survivor出现大量的内存复制)- 当创建对象的大小超过某个阈值时会直接放到 Old 区,通过
-XX:PretenureSizeThreshold指定
- 当创建对象的大小超过某个阈值时会直接放到 Old 区,通过
- 创建存活的对象(经历 16 次
minor gc的对象) - 动态年龄对象
survivor相同年龄所有对象大小的总和大于survivor空间的一半时,年龄大于该年龄的对象可以直接进去老年区。
新生代和老年代的总结
新生代时所有对象产生的地方,当年轻代内存用完时,就会触发 Minor GC。
新生代的主要特点
- 大多数新建的对象都位于 Eden 区
- 当 Eden 区被对象填满时,就会执行
Minor GC。把Eden区和survivor的form存活下来的对象转移到survivor的to区。 - 交换
survivor的from和to区的职责。
老年代在内存被占满是会进行 Major GC。
Minor GC、Major GC、Full GC 的区别
Full GC
Full GC 会对整个堆内存进行回收,包括新生代和老年代。
新生代空间不足会触发 Minor GC,只清理新生代内存。 老年代空间不足会触发 Full GC,对整个堆内存进行回 收。
Full GC 的频率受下面的因素的影响
- 堆内存大小
- JVM 配置参数
- 对象分配速度
Full GC 的触发时机
- Minor GC 后老年代空间不足
- 显示调用
System.gc()。调用System.gc()方法不能保证立即进行Full GC - 永久代空间不足
- CMS 初始化标记阶段出现 Promotion Failed
Full GC 的触发是由 JVM 自动管理的。Full G C的触发可能导致较长的停顿时间,因为它需要扫描整个堆内存并进行标记、整理操作。
Minor GC
Minor GC 会对新生代的垃圾对象的内存进行回收。
Minor GC 的触发时机
- Eden 区没有足够的空间
Major GC
Major GC 会对老年代的垃圾对象的内存进行回收。
Major GC 的触发时机
- 老年代空间不足
- 永久代垃圾回收
- JVM 显示调用
System.gc()、Runtime.getRuntime().gc()
Major GC 会导致程序暂停、直到垃圾回收操作完成。
内存结构
- 程序计数器(PC 寄存器):当前线程正在执行的那条字节码指令的地址,如果当前执行的是本地方法,那么此时程序 计数器为
Undefined。- 是一块较小的内存空间
- 线程私有:每个线程都有自己的程序计数器。
- 生命周期:随着线程的创建而创建,随着线程的结束而销毁。
- 是唯一一个不会出现
OutOfMemoryError的内存区域。
- Java 虚拟机栈(Java 栈):描述Java 方法运行过程的内存模型。
- Java 虚拟机栈会为每一个即将运行的 Java 方法创建一个叫做“栈帧”的区域,用于存放方法运行过程中的一些信息。
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口信息
- ...
- Java 虚拟机栈会为每一个即将运行的 Java 方法创建一个叫做“栈帧”的区域,用于存放方法运行过程中的一些信息。
- 本地方法栈
- 堆
- 方法区
Java 虚拟机栈
压栈出栈的过程
当方法运行过程中,如果要创建局部变量,就需要将局部变量的值存入栈帧的局部变量表中。
Java 虚拟机栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的方法,PC 寄存器也会执行这个地址。 只有这个活动的栈帧的本地变量可以被操作数栈使用,当前这个栈帧中调用另一个方法,与之对应的栈帧又会被创建, 新创建的栈帧压入栈顶,变为当前的活动栈帧。
方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。 如果没有返回值,那么新的活动栈中的操作数就没有变化。
局部变量表
定义为一个数字数组,主要用于存储方法参数、定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及 return address 类型。
return address 类型:一个特殊的类型,用于指向字节码指令地址,表示的是方法指向后的返回点。
局部变量表的大小是在编译期确定下来的,在方法运行期间大小不会改变。
基本单元是 slot。
只要被局部变量表中直接或间接引用的对象都不会被回收。
slot
- JVM 会为局部变量表的每一个 slot 分配一个访问索引,通过这个访问索引就可以访问到局部变量表中指定的局部变量的值。
- 如果当前栈帧是由构造方法或者实例方法创建的,那么该对象的引用 this,会存放在 index 为 0 的 slot 处,其余参数的顺序继续排列。
- 栈帧的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的。