JVM底层原理、四大垃圾回收算法

1,类装载器ClassLoader介绍

1.1,类装载器的种类,包含启动类加载器(Bootstrap),扩展类加载器(Extension),应用程序类加载器(AppClassLoader)

注意:

Classloader有多种,可以说三个,也可以说是四个(第四个为自己定义的类加载器,继承ClassLoader),系统自带的三个分别为:
1,启动类加载器(Bootstrap) ,C++所写
2,扩展类加载器(Extension) ,Java所写
3,应用程序类加载器(AppClassLoader)

我们new自己对象的时候创建的是应用程序类加载器(AppClassLoader)。

1.2,实例验证类装载器的种类

public class Demo {

    public static void main(String[] args) {
        Object object = new Object();
        Demo demo = new Demo();

        System.out.println(object.getClass().getClassLoader());
        System.out.println(demo.getClass().getClassLoader());
        System.out.println(demo.getClass().getClassLoader().getParent());
    }
}

输出:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@76ed5528

Process finished with exit code 0

1.3,类加载顺序的双亲委派机制

双亲委派机制:“我爸是李刚,有事找我爹”。
例如:需要用一个A.java这个类,首先去顶部Bootstrap根加载器去找,找得到你就用。
找不到再下降一层,去Extension加载器去找,找得到就用。
找不到再降一层,去AppClassLoader加载器去找,找得到就用。这时还没找到就会报"CLASS NOT FOUND EXCEPTION"。

1.4,实例验证双亲委派机制

package java.lang;

public class String {

    public static void main(java.lang.String[] args) {
        System.out.println("双亲委派机制验证代码");
    }
}

输出:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

Process finished with exit code 1
上面代码证明了类加载器的顺序:首先加载的是Bootstrap加载器,由于JVM中有java.lang.String这个类,
所以会首先加载这个类,而这个类中并无main方法,所以会报“在这个类 java.lang.String 中找不到 main 方法”错误。

这个问题就涉及到,如果有两个相同的类,那么java到底会用哪一个?如果使用用户自己定义java.lang.String,
那么用这个类的程序会出错,所以,为了保证用户写的源代码不污染java出厂自带的源代码,
而提供了一种“双亲委派”机制,保证“沙箱安全”。即先找到先使用。

2,方法区的介绍Method Area

注意:

方法区:绝对不是存放方法的地方,他是存放类的描述信息(模板)的地方,存储的每一个类的结构信息(比如static)
永久代和元空间的解释:
方法区是一种规范,类似于接口定义的规范:List list = new ArrayList();
把这种比喻用到方法区则有:
1,java 7中:方法区 f = new 永久代();
2,java 8中:方法去 f = new 元空间();,

Classloader只是负责class文件的加载,相当于快递员,这个“快递员”并不是只有一家,Classloader有多种
加载之前是“小class”,加载之后就变成了“大Class”,这是按照java.lang.Class模板生成了一个实例。“大Class”就装载在方法区,模板实例化之后就得到n个相同的对象
JVM并不是通过检查文件后缀是不是.class来判断是否需要加载的,而是通过文件开头的特定文件标志-cafe babe

 

3,Execution Engine执行引擎的介绍

4,本地方法接口(Native Interface)与本地方法栈(Native Method Stack)的介绍

注意:平时我们所说的栈指的是Java栈,本地方法栈(Native Method Stack)里面装的都是native方法。

见线程类Thread中start0()方法

 

5,PC寄存器的介绍

注意:本地native方法不归java管,所以计数器是空的

 

上图中亮色部分有2个特点
1,所有线程共享(灰色是线程私有)
2,亮色地方存在垃圾回收

6,Java栈的介绍

6.1,代码模拟栈内存溢出

public class Demo {

    private static void outStack() {
        outStack();
    }

    public static void main(java.lang.String[] args) {
        outStack();
    }
}

输出:
Exception in thread "main" java.lang.StackOverflowError
	at jvm.Demo.outStack(Demo.java:6)
        ...
Process finished with exit code 1

注意:
栈管运行,堆管存储
栈是线程私有的,不存在垃圾回收
栈帧的概念:java中的方法被扔进虚拟机的栈空间之后就成为“栈帧”,比如main方法,是程序的入口,被压栈之后就成为栈帧。

7,堆+栈+方法区的交互关系的介绍

 

8,堆的介绍

注意:
Java 7之前和图上一模一样,Java 8把永久区换成了元空间
堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成
当执行new Person();时,其实是new在新生区的伊甸园区,然后往下走,走到养老区,但是并未到元空间。

注意:
GC发生在伊甸园区,当对象快占满新生代时,就会发生YGC(Young GC,轻量级GC)操作,伊甸园区基本全部清空
幸存者0区(S0),别名“from区”。伊甸园区没有被YGC清空的对象将移至幸存者0区,幸存者1区别名“to 区”
每次进行YGC操作,幸存的对象就会从伊甸园区移到幸存者0区,如果幸存者0区满了,就会继续往下移,如果经历数次(默认15次)YGC操作对象还没有消亡,最终会来到养老区
如果到最后,养老区也满了,那么就对养老区进行FGC(Full GC,重GC),对养老区进行清洗
如果进行了多次FGC之后,还是无法腾出养老区的空间,就会报OOM(out of Memory)异常
from区和to区位置和名分不是固定的,每次GC过后都会交换,GC交换后,谁空谁是to区

注意:
整个堆分为新生区和养老区,新生区占整个堆的1/3,养老区占2/3。新生区又分为3份:伊甸园区,幸存者0区(from区),幸存者1区(to区) = 8:1:1
每次从伊甸园区经过GC幸存的对象,年龄(代数)会+1

8.1,堆的永久代/元空间

注意:
临时对象说明,其在伊甸园区生,也在伊甸园区死。
堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成,元空间也叫方法区
永久代(方法区)几乎没有垃圾回收,里面存放的都是加载的rt.jar等,让你随时可用

注意:
上面的图展示的是物理上的堆,分为两块,新生区和养老区。
堆的参数主要有两个:-Xms,Xmx:
1,-Xms堆的初始化的大小
2,Xmx堆的最大化大小
Young Gen(新生代)有一个参数-Xmn,这个参数可以调新生区和养老区的比例。但是,这个参数一般不调。
永久代也有两个参数:-XX:PermSize,-XX:MaxPermSize,可以分别调永久带的初始值和最大值。
Java8 后没有这两个参数啦,因为Java8后元空间不在虚拟机内啦,而是在本机物理内存中

8.2,代码验证JVM堆默认初始化内存大小&可使用最大内存大小

public static void main(java.lang.String[] args) {
        long totMemory = Runtime.getRuntime().totalMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println("堆总共内存(M)-" + totMemory/(1024*1024));
        System.out.println("堆最大内存(M)-" + maxMemory/(1024*1024));
}

输出:
堆总共内存(M)-119
堆最大内存(M)-1744

Process finished with exit code 0

8.3,代码验证JVM堆内存可调节

IDEA—Run—Edit Configurations

设置堆内存初始化,最大化内存大小,打印GC日志:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails

public static void main(java.lang.String[] args) {
        long totMemory = Runtime.getRuntime().totalMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println("堆总共内存(M)-" + totMemory/(1024*1024));
        System.out.println("堆最大内存(M)-" + maxMemory/(1024*1024));
}

输出:
堆总共内存(M)-981
堆最大内存(M)-981
Heap
 PSYoungGen      total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3015K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 330K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

8.4,模拟堆内存溢出OOM异常

1,首先把上述堆内存调成10M后,再new一个10M的对象,导致Full GC也无法处理,直至撑爆堆内存,查看堆溢出错误(OOM),程序及结果如下:
public static void main(java.lang.String[] args) {
        byte[] bytes = new byte[10*1024*1024];
}

输出:
[GC (Allocation Failure) [PSYoungGen: 1532K->496K(2560K)] 1532K->531K(9728K), 0.0030234 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 496K->480K(2560K)] 531K->515K(9728K), 0.0027392 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 35K->453K(7168K)] 515K->453K(9728K), [Metaspace: 3007K->3007K(1056768K)], 0.0088867 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 453K->453K(9728K), 0.0082019 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 453K->435K(7168K)] 453K->435K(9728K), [Metaspace: 3007K->3007K(1056768K)], 0.0151079 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at jvm.Demo.main(Demo.java:10)
Heap
 PSYoungGen      total 2560K, used 57K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0e6a0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 435K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 6% used [0x00000000ff600000,0x00000000ff66cf78,0x00000000ffd00000)
 Metaspace       used 3039K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 333K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 1

8.5,GC各个参数讲解

[GC (Allocation Failure) [PSYoungGen: 1532K->496K(2560K)] 1532K->531K(9728K), 0.0014412 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[Full GC (Allocation Failure) [PSYoungGen: 464K->0K(2560K)] [ParOldGen: 35K->453K(7168K)] 499K->453K(9728K), [Metaspace: 3007K->3007K(1056768K)], 0.0101167 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]

 

提问下:

下面程序中,有几个线程在运行

答:有两个线程,一个是main线程,另一个是后台GC线程

面试题:

1,GC是什么?

要答分代收集算法:
次数上频繁收集Young区
次数上较少收集Old区
基本不动元空间

 

知识点:
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC or Young GC),一种是全局GC(major GC or Full GC)
Minor GC和Full GC的区别
普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上 (因为养老区比较大,占堆的2/3)

2,GC有哪四大算法

1,引用计数法
2,复制算法(Copying)
3,标记清除(Mark-Sweep)
4,标记压缩(Mark-Compact)
        

8.5,GC四大算法的介绍

8.5.1,引用计数法(现在一般不用)

引用计数法一般不用的另一个缺点也很明显,就是当涉及到相互引用的场景时,代码说话

public class RefCountGC {
    private Object instance;

    public RefCountGC() {
        instance = null;
    }

    public static void main(String[] args) {
        RefCountGC objA = new RefCountGC();
        RefCountGC objB = new RefCountGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        
        System.gc();
    }
}
结论:
虽然objA和objB都置空,但是他们之前曾发生过相互引用,所以调用system.gc(手动版唤醒GC,后台也开着自动档)并不能进行垃圾回收。
并且,system.gc执行完之后也不是立刻执行垃圾回收

8.5.2,复制算法(Coping)

年轻代中使用的是Minor GC(YGC),这种GC算法采用的是复制算法(Copying)。

Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时 ( 默认是15岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。

8.5.2.1,新生区为什么考虑用复制算法,简单说用它有什么好处

因为新生代中的对象基本都是朝生夕死的(90%以上),简言之就是因为Eden区对象一般存活率较低,需要复制的对象少。
使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。
复制算法的优点是不会产生内存碎片。

上面动画中,Area空闲代表to,Area激活代表from,绿色代表不被回收的,红色代表被回收的。

8.5.2.2,有优点必然有缺点,复制算法的缺点是什么呢

如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,
并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,
而且最重要的是,我们必须要克服50%内存的浪费。

8.5.3,标记清除(Mark-Sweep)

老年代一般是由标记清除或标记清除+标记整理的混合实现。

8.5.3.1,标记清除优缺点

动画演示:

8.5.4,标记压缩(Mark-Compact)

标记压缩(Mark-Compact)又叫标记清除压缩(Mark-Sweep-Compact),或者标记清除整理算法。
老年代一般是由标记清除或标记清除+标记整理的混合实现

动画演示:

 

面试题:请说出各个GC回收算法优缺点

内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程

整理不易,请作者喝杯咖啡呗~

<think>嗯,用户想了解JVM的基本概念工作原理。首先,我需要明确JVM的定义,它是Java虚拟机的缩写,是Java程序运行的核心。接下来,要分步骤解释它的工作原理。记得用户提到需要结构清晰,所以应该按照流程来组织内容,比如编译、加载、执行、内存管理、垃圾回收这些环节。 首先,JVM的作用是实现跨平台,这点很重要,用户可能已经知道Java的“一次编写,到处运行”,但需要解释JVM如何做到这点。然后,从Java源代码编译成字节码开始,类加载器的作用,这部分需要引用已有的资料,比如引用[3]提到类加载器加载字节码到内存,可以引用这个。 接下来是执行引擎,解释器逐行解释字节码,但为了优化性能,JIT编译器会将热点代码编译成机器码,这里需要提到引用[4]中的JIT优化手段。内存管理部分要分堆栈,堆用于对象实例,栈用于方法调用,引用[2]提到内存模型垃圾回收机制,可以引用这里的相关内容。 垃圾回收机制JVM的重要部分,需要说明自动内存管理的原理,以及不同的GC算法。可能用户对GC如何工作感兴趣,但不需要深入细节,保持基础解释即可。 用户的问题还包括如何工作,所以整个流程需要连贯:从编写代码到编译成.class文件,类加载器加载到内存,执行引擎处理,内存分配回收。每个步骤都需要简明扼要,避免过于技术化,但确保准确性。 还需要注意使用中文回答,所有数学公式用$...$,独立公式用$$...$$,但当前问题可能不涉及太多数学内容,不过如果有提到算法或性能分析可能需要应用。不过JVM原理更多是概念性的,可能用不上数学公式,但需要检查是否有必要的地方。 另外,用户要求生成相关问题,所以最后要准备几个相关问题,比如JVM内存结构、类加载过程、垃圾回收算法、调优方法等,参考已有的引用内容,比如引用[4]提到了JIT优化,引用[2]提到调优技术,可以围绕这些生成问题。 最后,确保引用标识正确添加到对应的内容后面,例如在解释类加载器时引用[3],内存管理引用[2],执行引擎引用[4]等。检查所有引用是否准确,没有遗漏。 总结下来,回答的结构应该是:定义JVM,跨平台特性,工作流程分步骤解释,每个步骤引用相关文献,最后列出相关问题。保持语言简洁,符合用户要求的清晰结构。</think>### JVM 基本概念与工作原理解析 #### **JVM 是什么?** Java虚拟机(Java Virtual Machine, JVM)是Java技术的核心组件,负责执行编译后的Java字节码(`*.class`文件)。它的核心目标是实现**跨平台运行**,即“一次编写,到处运行”。通过将字节码转换为特定平台的机器指令,JVM屏蔽了底层操作系统的差异[^1][^3]。 --- #### **JVM 如何工作?** JVM的工作流程可分为以下关键步骤: 1. **编译阶段** Java源代码(`.java`文件)通过编译器(如`javac`)转换为**字节码**(`.class`文件),字节码是独立于平台的中间代码。 $$ \text{Java源码} \xrightarrow{\text{编译}} \text{字节码} $$ 2. **类加载** 类加载器(ClassLoader)将字节码加载到JVM内存中,包括以下过程: - **加载**:读取字节码文件。 - **验证**:确保代码符合JVM规范。 - **准备**:为静态变量分配内存并初始化默认值。 - **解析**:将符号引用转换为直接引用。 - **初始化**:执行静态代码块赋值操作[^4]。 3. **执行引擎** - **解释器**:逐行解释字节码为机器指令执行。 - **即时编译器(JIT)**:将频繁执行的代码(热点代码)编译为本地机器码以提升性能。 ```java // 示例:JIT优化的热点代码 for (int i = 0; i < 1000000; i++) { calculate(i); // 多次调用后触发JIT编译 } ``` 4. **内存管理** JVM通过运行时数据区管理内存: - **堆(Heap)**:存储对象实例,是垃圾回收的主要区域。 - **栈(Stack)**:存储方法调用时的局部变量操作数。 - **方法区(Method Area)**:存储类元数据、常量池等[^2]。 5. **垃圾回收(GC)** JVM自动回收不再使用的对象内存,常见的算法包括: - 标记-清除(Mark-Sweep) - 复制算法(Copying) - 分代收集(Generational)[^2]。 --- #### **核心原理总结** JVM通过**类加载、执行引擎、内存管理、垃圾回收**四大机制,为Java程序提供高效、稳定的运行环境。其跨平台能力依赖于字节码平台相关的JVM实现,而性能优化则通过JIT编译与分代GC实现[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值