垃圾回收
垃圾回收算法
- 引用计数算法(循环引用会导致内存不会被释放,即内存溢出)
- 可达性分析算法:通过一系列的 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 处,其余参数的顺序继续排列。
- 栈帧的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
操作数栈
- 栈顶缓存技术:由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理 CPU 的寄存器中,以此降低内存的读写次数,提高执行引擎的执行效率。
- 每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好。
- 并非采用访问索引方式进行数据方法,而是只能通过标准的入栈、出栈操作完成一次数据访问。
方法的调用
- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期间保持不变,这种情况下将调用方的符号引用转为直接引用的过程称为静态链接。
- 动态链接:如果被调用的方法在编辑期无法确定,只能在运行期将调用的方法的符号引用转为直接引用的过程称为动态链接。
- 方法绑定
- 早期绑定:被调用的目标方法在编译期可知,且在运行时保持不变
- 晚期绑定:被调用的方法在编译器无法被确定,只能在程序运行期根据实际的类型进行绑定相关的方法。
- 非虚方法:如果方法在编译期就确定了具体的调用版本,则这个版本在运行时时不可变的,这样的方法称为非虚方法
- 静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法。
- 虚方法表:面向对象的编程中,会很频繁的使用动态分配(确定对象调用哪个方法的过程,
多态的核心), 如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率,因此为了提高性能,JVM 采用在类的方法区建立一个虚方法表,使用索引表来代替查找。- 每个类都会有一个虚方法表,表中存放各个方法的实际入口
- 虚方法表会在类加载的链接阶段被创建( 准确的是在链接阶段中的解析步骤之后进行的,因为要确保所有方法引用都已抓换为直接引用 ), 并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法也初始化完毕。
- 方法重写的本质
- 找到操作数栈顶的第一个元素所执行对象的实际类型,记作 C。
- 如果在类型C中找到与常量中的描述符号简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
Java 虚拟机栈的特点
- 运行速度快,仅次于 PC 寄存器
- 局部变量表随着栈帧的创建而创建,它的大小在编译期确定
- Java 虚拟机栈会出现的两种异常
- StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。
- OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法在动态扩展,抛出 OutOfMemoryError 异常。
- Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。(程序计数器也是这样)
- 出现 StackOverFlowError 时,内存空间可能还有很多。
本地方法栈
本地方法栈是 JVM 运行 native 方法准备的空间。它与 Java 虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。
堆
堆是用来存放对象的内存空间,几乎 所有的对象都存储在堆中。
堆的特点
- 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。
- 在虚拟机启动时创建。
- 是垃圾回收的主要场所
- 堆分为新生代和老年代。新生代分为 Eden 和 survivor 。 Survivor 分为 from 和 to 。
新生代与老年代
- 老年代比新生代生命周期长
- 新生代与老年代空间默认比例为
1:2, JVM 参数XX:NewRatio=2 - HotSpor 中 Eden。Survivor-From、Survivor-To 的比例为
8:1:1 - 几乎所有的 Java 对象都是在 Eden 区被 new 出来的,Eden 放不下的大对象,就直接进入老年代。
对象分配的过程
- new 的对象先放在 Eden 区,大小有限制
- 如果创建新对象时,Eden 空间满了,就会触发 Minor GC
- Minor GC ,会 Eden 的垃圾对象清理,并将新创建的对象放到 eden 区
- 将 eden 区的对象移到 survivor 的 from 区,
逃逸分析
- 标量替换
- 标量:不可被分解的量,(Java 的基本数据类型)
- 聚合量:可以被分解的量(Java 对象)。
- 对象和数组并非都是在堆上分配内存的。
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer s = new StringBuffer();
s.append(s1);
s.append(s2);
return s;
}
TLAB
线程本地分配缓存区。属于 Eden
四种引用方式
- 强引用:创建一个对象,并把整个对象赋给另一个引用变量。
Object obj = new Object(); - 软引用:有用,但是非必须的对象。
SoftReference<Object> softRef = new SoftReference<>(new Object()); - 弱引用:比软引用更弱的一种引用类型。无论内存是否够,只要发生垃圾收集,只具有弱引用的对象就会被回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object()); - 虚引用:最弱的引用关系,无法通过虚引用来取得一个对象实例。
方法区
- 已被虚拟机加载的类信息
- 常量
- 静态变量
- 即使编辑器编译后的代码
方法去的特点
- 线程共享
- 永久代
- 内存回收效率低
运行时常量池
常量是存放在运行时常量池里面的。
