一、JVM内存结构
1、方法区:存储编译后的类、常量等(.class字节码文件)
2、堆内存:存储对象
3、程序计数器:存储当前执行的指令地址(计算机处理器(CPU)正在执行的下一条指令在内存中的地址)
4、虚拟机栈:java栈,存储局部变量、方法参数、返回值以及异常处理信息
5、本地方法栈:存储本地方法的执行状态信息以上是JVM内存结构的主要部分,其中除了方法区外其他部分都是java程序员直接操作和调优的重要部分
线程私有:程序计数器、虚拟机栈、本地方法栈。
线程共享:方法区、堆。
以下是一个更完整的示例代码,演示了JVM内存结构的各个部分:
public class MemoryStructureExample {
// 静态变量,存储在方法区
private static String staticVar = "Static Variable";
public static void main(String[] args) {
// 局部变量,存储在虚拟机栈
int localVar = 10;
// 创建一个对象实例,存储在堆内存
MemoryStructureExample obj = new MemoryStructureExample();
// 调用方法,会在虚拟机栈中创建方法调用的栈帧
obj.method();
}
// 实例方法
public void method() {
// 方法中的局部变量,存储在虚拟机栈
String localVar2 = "Local Variable";
// 创建一个对象实例,存储在堆内存
Object obj = new Object();
// 调用本地方法,本地方法栈存储本地方法的执行状态信息
System.out.println(System.currentTimeMillis());
}
}
在这个示例中,我们展示了JVM内存结构的各个部分的应用:
- 静态变量
staticVar
存储在方法区; main
方法中的局部变量localVar
存储在虚拟机栈;MemoryStructureExample
对象实例存储在堆内存;method
方法中的局部变量localVar2
也存储在虚拟机栈;- 方法中创建的
Object
对象实例也存储在堆内存; - 调用本地方法
System.currentTimeMillis()
时,本地方法栈存储本地方法的执行状态信息。
二、JVM垃圾回收
GC 的目的在于实现堆内存中无用对象内存自动释放,减少内存碎片、加快分配速度 。线程私有的不存在垃圾回收,线程共享才存在垃圾回收。以下我们围绕如何发现垃圾和如何进行垃圾回收进行详细描述:
(一)如何发现垃圾?
1、引用计数算法
引用计数算法核心思想是,堆中的对象每被引用一次,则计数器加 1,每减少一个引用就减 1,当对象的引用计数器为 0 时可以被当作垃圾收集。
优点:效率高,比较快
缺点:无法检测出循环引用,如两个对象互相引用时,他们的引用计数永远不可能为 0
2、可达性分析(根搜索)算法
根搜索算法是把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的
引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕
之后,剩余的节点则被认为是没有被引用到的节点,即可以当作垃圾。
3、三色标记法(黑灰白)
三色标记法是用三种颜色记录对象的标记状态。这种算法通过标记对象的颜色来表示它们的状态,以确定哪些对象是活动的,哪些是垃圾对象。黑色-已标记,灰色-标记中,白色-未标记。原理是通过将引用链上的对象全部标记,最终剩余的不在引用链上的对象全部是白色的(未标记的),然后对未标记的无用的对象进行回收。这种算法通过标记对象的颜色来表示它们的状态,以确定哪些对象是活动的,哪些是垃圾对象。
3.1起始的三个对象还未处理完成,用灰色表示
3.2该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
3.3依次类推
3.4沿着引用链都标记了一遍
3.5最后为标记的白色对象,即为垃圾
(二)如何清除垃圾?
1、标记清除算法(空间碎片,CMS)
标记清除算法是通过GC Root引用链往下查找,对于引用链上有引用的对象进行标记,然后对之外的无用的对象进行清除。缺点是存在内存碎片的问题。
2、标记整理算法(性能较差,G1)
标记整理算法是在标记清除算法上多了一步整理的操作,去除了空间碎片的问题。缺点是性能较差
3、标记复制算法(占用成倍的空间 )
3.1将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象。
3.2标记阶段与前面的算法类似。
3.3在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
3.4复制完成后,交换 from 和 to 的位置即可。
三、四种引用
总的来说,强引用是最常见的引用类型,只有在不再被引用时才会被回收;软引用在内存不足时会被回收;弱引用在下一次垃圾回收时会被回收;虚引用在对象被回收时会被放入引用队列中,需要手动清除。根据不同的需求和场景,可以选择合适的引用类型来管理对象的生命周期。
(一)强引用:
普通变量赋值即为强引用,如 A a = new A();通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收。
(二)软引用:
例如:SoftReference a = new SoftReference(new A());如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象;软引用自身需要配合引用队列来释放,典型例子是反射数据。
(三)弱引用:
例如:WeakReference a = new WeakReference(new A());如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象,弱引用自身需要配合引用队列来释放,典型例子是 ThreadLocalMap 中的 Entry 对象。
(四)虚引用:
-
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
-
必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
-
典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存。