深入解析JVM:从类加载到垃圾回收的全面指南

发布于:2025-09-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

jvm概述

为什么学习jvm

jvm作用

jvm整体组成部分

jvm类加载系统

类加载过程

1、加载:

2、链接:

3、初始化

类在哪些情况下会被加载

类不会被加载 

类加载器分类

站在jvm角度上

 再细分类加载器

双亲委派机制

如何打破双亲委派机制

jvm运行时数据区

概述

程序计数器

java虚拟机栈​编辑

栈帧中存储什么内容

本地方法栈

堆空间区域划分

为什么分区(代)

对象创建内存分配过程​编辑​编辑​编辑

jvm调优

方法区

方法区的垃圾回收

本地方法接口

什么是本地方法

Java中为什么要调用本地方法

执行引擎

Java程序执行过程中涉及两次编译

​编辑将字节码转换为机器码有两种方式:

为什么Java是半编译半解释执行

垃圾回收

什么是垃圾对象

垃圾回收发展

哪些区域会出现垃圾回收

内存溢出与内存泄漏

内存溢出:

内存泄漏:

Stop the World

垃圾回收阶段算法

1、垃圾标记阶段

引用计数算法

可达性分析算法(根搜索算法)

哪些对象可以视为GCRoots(根对象)

对象的finalization机制

2、垃圾回收阶段

1、标记-复制算法(Copying)

2、标记-清除算法(Mark-Sweep)

3、标记-压缩算法(Mark-Compact  )

标记-清除和标记-压缩对比:

总结

垃圾回收器

什么是垃圾回收器

有哪些垃圾回收器

从线程数量上分类

从工作模式上进行划分

从分区角度上分:

垃圾收集器性能指标

常见垃圾收集器

设置垃圾回收器


jvm概述

为什么学习jvm

高级程序员了解需要。

jvm作用

jvm负责把编译后的字节码转换为机器码。

jvm整体组成部分

类加载部分:把硬盘上的字节码加载到内存中(运行时数据区)。

运行时数据区:存储运行时产生的各种数据,类信息,对象信息...

执行引擎:负责将字节码转为机器码。

本地发放接口:调用本地方法 。(启动线程 start0())

垃圾回收器:

jvm类加载系统

负责将硬盘上的字节码文件加载到jvm中,生成类的class对象,存储在方法区;

类就是一个模板;

类加载过程

1、加载:

以二进制文件流进行读取,在内存中生成类的class对象

2、链接:

验证:验证字节码的结构是否正确

准备:为类的静态属性进行初始化的赋值

解析:把字节码的符号引用 替换成 内存中的直接引用地址 

           字节码查看:javap -verbose Demo.class

3、初始化

类初始化阶段主要是为类中的静态成员进行赋值;

因为类加载执行完初始化阶段才说明类加载完成了。

类在哪些情况下会被加载

  • 调用类中静态成员(变量,方法)
  • new类的对象
  • 在类中执行main()
  • 反射加载类 class.dorName("地址")
  • 子类被加载会带动父类被加载

类不会被加载 

1、类作为数组类型

 

2、只是访问类中静态的常量 

 

 

类加载器分类

类加载器是负责读取类的功能

站在jvm角度上

引导类加载器(不是用java写的,是用c/c++),负责读取java中底层数据库

java写的类加载器(用来读取我们写的应用程序)

 再细分类加载器

1、启动类加载器

      c/c++语言实现,负责加载Java核心类库(系统库 Java.lang)

2、扩展类加载器

      用Java语言来实现的,继承自classloader,加载jre下面扩展类的jre/lib/ext目录

3、应用程序类加载器

      用Java语言实现的,继承classloader类,用来加载我们自己开发的应用程序类

双亲委派机制

当加载一个类的时候,它总是先向上加载它的父级类加载器,确保把系统中的类优先的加载;

直到父级的类找不到时,再逐级向下,让子级类加载器加载;

如果子级也找不到,最终抛类找不到异常。

防止我们自己写的类替换了系统中类。

如何打破双亲委派机制

自定义加载器 

jvm运行时数据区

概述

存储运行时产生的各种数据

程序计数器

程序计数器用来记录每一个线程执行的指令位置;

速度是最快的,而且是线程私有的(每一个线程都会有一个计数器);

此区域不会出现内存溢出(不够用),也不会出现垃圾回收。

java虚拟机栈

栈是运行的,解决程序方法执行,在虚拟机栈中是用来运行Java自己写的方法的;

调用方法,方法入栈,运行结束出栈(先进后出,栈顶的方法,成为当前栈帧);

虚拟机栈是线程私有的,线程之间互相隔离;

一个方法就是一个栈帧,在栈帧中存储局部变量,运行结果......

栈区域不存在垃圾回收,但是会存在内存溢出问题。

栈帧中存储什么内容

1、局部变量表

      int a = 10;

      int b = 10;

2、操作数栈

      int c = a + b; 

3、方法返回地址

本地方法栈

本地方法栈是用来执行调用的本地方法的。

是线程私有的,不会存在垃圾回收;

会出现内存溢出的问题。

堆的作用是用来存储Java语言产生的对象的;

是运行时数据区中最大的一块内存空间,空间大小可以设置;

堆空间是所有线程共享的;

堆空间是垃圾回收的重点区域,堆中没有被使用到的垃圾堆下,会被垃圾回收器回收掉。

堆空间区域划分

新生区(新生代 年轻代):

伊甸园区:

幸存者0区:

幸存者1区:

老年区(老年代):

为什么分区(代)

可以将不同生命周期的对象,存储再不同的区域,针对不同的区域采取不同的垃圾回收算法,使得垃圾回收策略更加优化。

对象创建内存分配过程

新创建的对象都存储在伊甸园区,当垃圾回收时,将还被使用的对象转移至某一个幸存者区,将伊甸园区垃圾对象进行清除,当下一次垃圾回收时,将伊甸园区存活的对象与当前正在使用的幸存者区存活的对象,转移到另一个幸存者区(每一次会空闲幸存者区)。

当一个对象经历过15次垃圾回收后,仍然存活,那么就把该对象转移到老年代。;

老年代比较少进行垃圾回收,在老年代空间不足时,会进行垃圾回收;

当回收后内存仍然不足时,会触发FULLGC(整堆收集  应尽量避免);

当整堆收集后,它仍然不够使用,那么就会出现内存溢出错误  ---OOM(OutOFMemoryError)

jvm调优

可以根据程序具体的使用场景,对运行时数据区的各种空间大小进行调整    例如堆、方法区。

                                                   对垃圾回收器进行选择

方法区

方法区主要用来存储加载的类信息

方法区的大小是可以设置的

方法区也会进行垃圾回收,方法区也可能出现内存溢出问题

方法区的垃圾回收

方法区的垃圾回收,是对类信息进行回收的

类信息不再被使用,类信息也可以被卸载

卸载条件

该类产生的对象不存在了

该类的class对象,也不再被使用了

加载给类的类加载器也被回收了

本地方法接口

是虚拟机中专门用来调用本地方法的接口。

什么是本地方法

在Java中被  native关键字  修饰的方法,没有方法体,不是用Java语言实现的方法,用c/c++在操作系统底层实现的方法。

没有方法体:void a();  abstract方法也没方法体。

eg:

     Object hashCode() 获取对象内存地址 涉及到读取内存;

     IO中读文件(输入文件 操作硬盘) read0();

     启动线程  native void start0(); 启动线程,就是把这个线程注册到操作系统。

Java中为什么要调用本地方法

因为Java属于应用层(上升)语言,有时候,需要对硬件系统资源进行调用;

此时就不方便,在一个系统资源不允许应用程序直接调用;

直接调用可能会造成改写底层,影响较大。

那就需要通过本地方法 调用操作硬件资源。

执行引擎

执行引擎是虚拟机核心部件之一;

主要作用是将加载到虚拟机中的字节码 再次 转换为机器码(字节码并不是系统能够直接执行的机器码)。

执行引擎可以通过解释/编译两种方式 实现字节码转为机器码。

Java程序执行过程中涉及两次编译

第一次  hello.java(源代码 通过jdk javac调用编译器)--->hello.class文件  称为前端编译;

第二次  通过执行引擎 将字节码 编译为 机器码  称为后端编译。


将字节码转换为机器码有两种方式:

解释器(解释执行):对字节码逐行进行解释翻译,重复性代码,也是每次都要解释执行,效率低。

编译器(编译执行):对某段字节码进行整体编译,然后存储起来,以后使用时不需要编译了,效率高。

                                编译器会针对执行过程中的热点代码进行编译,并缓存起来。

为什么Java是半编译半解释执行

程序开始运行时,解释器可以立即发挥作用,投入使用;

而编译器虽然执行效率高,但是前期需要对热点代码进行跟踪和编译,需要消耗时间。

垃圾回收

什么是垃圾对象

指在运行程序中没有任何指向的引用对象(没有任何引用指向的对象)。

垃圾对象如果不清理,新的对象可能没有足够的空间,可能会导致内存溢出问题

垃圾回收发展

早期c/c++这类语言,内存管理是手动的,使用时申请,使用完后手动释放。

优点:对内存管理更加精确,效率高;

缺点:增加程序员负担,控制不好,容易出事(忘了释放,误操作内存空间)。

后来就发展为自动回收:

java,c#...都采用自动垃圾回收

优点:解放程序员;

缺点:会占用一些内存空间(垃圾不是出现后立即回收的),降低了程序员管理内存的能力。

哪些区域会出现垃圾回收

堆  产生的对象  频繁回收年轻代,较少回收老年代

方法区  类信息卸载  整堆收集时,会进行回收 FULL GC

内存溢出与内存泄漏

内存溢出:

内存不够用了

内存泄漏:

系统中哪些用不到的,又不能回收的对象

eg:

      单例对象;

      数据库连接对象,IO流,socket这些提高close()的类   用完之后,没有关闭,垃圾回收器,是        不能主动回收这些对象的。

内存泄漏,虽然不会直接除法内存溢出,但是长期有对象不能被回收,也是导致内存溢出的原因之一。

Stop the World

垃圾回收时会经历两个阶段:一是标记阶段,二是回收阶段,在标记和回收时,需要我们的用户线程暂停,不暂停标记和回收时肯会造成错标和漏标。

垃圾回收阶段算法

1、垃圾标记阶段

将虚拟机中不再被任何引用指向的对象标记出来,在垃圾回收阶段,就会将标记对象进行回收

垃圾标记阶段相关算法:

引用计数算法

存在缺陷,没有被虚拟机所使用。

设计思想:在我们的对象中维护一个整数计数器变量,当有引用指向对象时,我们的计数器加一,相反就减一(引用断开)。

优点:设计实现简单,容易分辨我们的对象是否是垃圾对象。

缺点:需要维护一个变量存储引用数量,需要频繁的修改引用计数器变量,占空间,还耗时;

           最重要的是无法解决循环引用问题。

可达性分析算法(根搜索算法)

设计思想:从一些可以被称为GCRoots的对象开始向下查找,只要某一个对象与GCRoots对象有联系的,可以判断对象时被调用的,与GCRoots对象引用链没有任何关系的对象,可以视为垃圾对象。

哪些对象可以视为GCRoots(根对象)

1、虚拟机栈中(被调用的方法)所使用的对象(活跃对象);

2、类中的静态属性;

3、虚拟机中使用的系统类对象。

对象的finalization机制

Object类中有一个finalize()方法,这个方法是在对象被回收之前,由虚拟机自动调用,在对象被回收前需要执行的一些操作就可以在此方法中编写。

finalize()可以在子类中重写。

finalize()方法只会被调用一次(第一次被判定为垃圾,要对其回收,调用finalize(),对象有可能又被引用了,对象就不能被回收,当下一次被判定为垃圾对象时,就不会再调用finalize())。

复活后然后呢?

由于finalize()方法的存在,被标记为垃圾的对象也不是非死不可的。

可以将对象分为三种状态:

可触及:被GCRoots引用的,不是垃圾对象;

可复活的:被判定为垃圾的,但是finalize()方法还没有被调用的;

不可触及的:被判定为垃圾的,finalize()已经被调用过了。

2、垃圾回收阶段

1、标记-复制算法(Copying)

将内存可以分为较小的块,当发生垃圾回收时,将一个区域中存活的对象复制到另一个区域,在另一个区域从头开始排列,清除当前垃圾回收的区域。

优点:清理之后,内存没有碎片;

不足:回收时,需要移动对象,所以适合小内存块,而且存活对象少的情况。

适合新生代。

2、标记-清除算法(Mark-Sweep)

将被标记为垃圾的对象的地址进行记录,后面如果分配新对象,判断垃圾对象的空间是否能够存储下新的对象,如果可以存储下,用新对象直接覆盖垃圾对象即可;

存活对象是不发生移动的。

优点:对象不移动;

不足:回收后内存中会出现碎片。

3、标记-压缩算法(Mark-Compact  )

将存活的对象会移动到内存区域的一端,按顺序排列(压缩),清除边界以外的空间;

在标记清除的基础上进行一次内存够管理。

优点:回收后没有内存碎片。

标记-清除和标记-压缩对比:

标记-清除:不移动存活对象;

标记-压缩:会移动存活对象;

两者都适合老年代对象回收。 

先使用标记-清除,当老年代空间不足,或者不能再存储一个较大的对象,再使用标记-压缩算法。

总结

垃圾回收时,根据不同的分区采用不同的回收算法。

新生代:标记-复制;

老年代:标记-清除、标记-压缩。

垃圾回收器

什么是垃圾回收器

垃圾回收器是堆垃圾回收过程的一个实践者(落地)

不同的虚拟机中垃圾回收器的种类也是很多的

有哪些垃圾回收器

从线程数量上分类

单线程:垃圾回收器只要一个;

多线程:垃圾回收器有多个。

从工作模式上进行划分

独占式:垃圾回收线程执行时,其他用户线程需要暂停(stop the world);

并发式:垃圾回收线程和用户线程可以做到并发执行。

从分区角度上分:

新生代:

老年代:

垃圾收集器性能指标

吞吐量:

用户线程暂停时间(重点):

回收时内存开销:

常见垃圾收集器

Serial:单线程,新生代

Serial Old:单线程,老年代

ParNew:多线程新生代收集器

Parallel Scavenge:多线程新生代收集器

Parallel Old:多线程老年代收集器

CMS:

多线程,老年代收集器,开创了垃圾收集线程与用户线程并发执行的先例

并发标记清除收集器

初始标记--独占执行

并发标记--并发执行

重新标记--独占执行

并发清除-并发执行     

G1:

G1垃圾回收器,继承了CMS中,垃圾收集线程和用户线程并行执行的特点,减少了用户线程暂停时间。

同时将新生代和老年代的各个区域又划分成多个更小的区域,对每一个区域进行跟踪,优先回收价值高的区域(垃圾多的区域,例如可以把伊甸园区分成好几个小的区域)。

提升回收效率,提高了吞吐量,不再区分年轻代和老年代,可以做到对整个堆进行回收。

非常适合服务器端程序,大型项目。

设置垃圾回收器

打印默认垃圾回收器

-XX:+PrintCommandLineFlags -version

打印垃圾回收详细信息

-XX:+PrintGCDetails -version

设置默认垃圾回收器

-XX:+UseG1GC