文章目录
JVM自问自答
Drip
2025-06-20 22:55
累计阅读12
评论0

什么是JVM?

JVM指的是Java虚拟机,他本质上是一个运行在计算机上的程序。他的职责是运行Java字节码文件。

他有编写一次,导出运行的跨平台特性。他是将源代码编译为字节码文件,通过JVM在不同的操作系统上解释为对应操作系统的机器码然后运行。

同时他还有内存管理的机制,可以自动为对象分配和回收内存。

说说JVM的组成

嗯,在运行Java文件时,会将java文件编译成字节码文件,这时候会进入JVM的第一个组件:类加载器,将class字节码文件加载到内存中。

之后进入第二部分:在Java虚拟机里面所有用到的内存就称之为:运行时数据区,这主要分成了四大块,堆空间,栈空间,方法区(存放上面提到的class字节码信息)和程序计数器

第三个部分是执行引擎,它里面包含了:1.解释器(将字节码指令转化为机器码) 2.即时编译器 (将热点代码进行优化)3.垃圾回收器(对不在使用的对象进行回收)

最后就是本地接口:在Java虚拟机里面,有些方法是c和c++语言编写的,在Java虚拟机执行过程中会调用这部分方法。

说一下运行时数据区。

嗯,运行时数据区就是JVM所管理的内存区域。主要分为两大类。一个是线程共享的,多个线程会共用同一块内存,包含方法区和堆,一个是线程不共享的,每个线程有一块单独的内存空间,有本地方法栈,虚拟机栈,程序计数器

什么是程序计数器?

程序计数器也叫PC寄存器,每个线程都会通过程序计数器来记录当前要执行字节码指令的地址。

什么是Java虚拟机栈?

Java虚拟机栈采用栈的数据结构来管理方法调用和存储局部变量,先进后出,每一个方法调用一个栈帧来保存。 每个线程都会都会包含一个自己的虚拟机栈, 所以他是线程不共享的,他的生命周期和线程的相同。

Java虚拟机栈存放的是Java方法调用的栈帧,本地方法栈则是存储的native方法存放的栈帧。

什么是堆?

  • 在一般Java程序中,堆内存是空间最大的一块内存区域,创建出来的所有对象都存放在堆上

  • 栈帧上的局部变量表中,可以存放堆上对象的引用。

  • 另外,堆是垃圾回收最主要的部分,堆结构更详细的划分与垃圾回收器有关。

什么是方法区?

方法区主要存放的是基础信息。

  • 像每一个类加载的元数据。

  • 运行时常量池,保留了字节码文件的常量池内容,避免常量内容重复创建减少内存开销。

  • 字符串常量池,存储字符串的常量。

哪些区域会出现内存溢出,会有什么现象?

嗯,内存溢出指的是内存的某一块区域超出了规定允许使用内存的最大值,从而因为内存空间不足而失败,虚拟机一般会抛出指定的错误。

在Java虚拟机中,只有程序计数器不会出现内存溢出的情况,因为每个线程的程序计数器只保存一个固定长度的地址。

说一下类的生命周期(讲一下类加载过程?)

类的生命周期共分为四个阶段:加载,连接,初始化,卸载。

  1. 加载阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式加载到内存中,来获取字节码信息。

    • 类加载器加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中,来保存类的所有信息。

    • 最后会在堆中生成一份与方法区中类似的java.lang.Class对象,作用是在Java代码中去获取类的信息。

  2. 连接阶段主要就是检查编译的字节码文件是否符合《java虚拟机规范》,并且对一些static变量分配内存赋初值。

  3. 初始化阶段会执行静态代码块中的代码,并给静态变量赋初值。

  4. 判断一个类是否可以被卸载,需要满足三个条件:

    1. 此类所有对象都已经被回收,在堆中不存在该类的实例对象和子类对象

    2. 加载该类的的类加载器已经被回收

    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

什么是类加载器,有哪些常见的类加载器

类加载器负责在类的加载过程中,将字节码信息以流的方式加载到内存中。

  • 启动类加载器(Bootstrap Class Loader):这是最顶层的类加载器,负责加载Java的*核心库类,如java.lang包下的,它是用C++编写的,是JVM的一部分。启动类加载器无法被Java程序直接引用。

  • 扩展类加载器(Extension Class Loader):它是Java语言实现的,继承自ClassLoader类,负责加载Java扩展类。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器。

  • 应用程序类加载器(Application Class Loader):这也是Java语言实现的,负责加载用户类路径(ClassPath)上的指定类库,是我们平时编写Java程序时默认使用的类加载器。系统类加载器的父加载器是扩展类加载器。

  • 自定义类加载器(Custom Class Loader):开发者可以根据需求定制类的加载方式,可以重写findClass方法来实现。

这些类加载器之间的关系形成了双亲委派模型,其核心思想是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。

只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

什么是双亲委派模型,他的作用是什么?

双亲委派机制指的是,当一个类加载器接收到类加载的任务时,首先自身不会去尝试加载这个类,而是向上委派父类加载器,检测这个类是否被加载过,如果被加载过则直接返回。当父类加载器无法完成这个加载请求时,子加载器才会进行加载。总结来说就是:自下而上检测,自顶向下加载。

  1. 保证类加载的安全性:通过双亲委派机制可以避免恶意代码替换掉JDK类中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性

  2. 避免重复加载,双亲委派机制可以避免同一个类被多次加载

判断垃圾的方法有哪些?(如何判断堆上的对象有没有被引用)

在Java中,判断对象是否是垃圾,主要依据两种主流的垃圾回收机制来实现:引用计数法和可达性分析算法

引用计数法(Reference Counting)

  • 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。

  • 缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。

可达性分析算法(Reachability Analysis)(Hotspot虚拟机使用方案)

可达性分析算法将对象分为两类:GC-Root对象和普通对象,如果普通对象在GC-Root的引用链上,则该对象不能回收,因为这是可达的。同样,不可达的对象就可以被回收。

哪些对象可以称之为GC-Root对象呢?

  • 线程Thread对象

  • 虚拟机栈和本地方法栈正在引用的对象

  • 静态属性引用的对象

JVM中有哪些引用?

嗯,有四种引用。分别是:强引用,软引用,弱引用和虚引用

强引用:JVM里默认引用关系就是强引用,即普通对象被GC-Root对象关联的引用,只要有这层关系存在,普通对象就无法被回收。

软引用:软引用相较于强引用是比较弱的引用关系,如果一个对象只有软引用关联到他,当程序内存不足时,改对象会被回收。另外,软引用主要在缓存框架里使用。

弱引用:弱引用的整体机制和软引用基本一致,区别在于,弱引用包含的对象在垃圾回收时,不管内存够不够都会被直接回收,常用于ThreadLocal

虚引用:也被称作幻影引用,是最弱的引用关系。他唯一的用途就是当对象被垃圾回收器回收时,可以接收到对应的通知

如何触发垃圾回收?

  • 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。

  • 手动请求:虽然垃圾回收是自动的,开发者可以通过调用 System.gc() 建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。

  • JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等。

  • 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

垃圾回收算法有哪些?

  • 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。

  • 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。

  • 标记-整理算法:标记整理算法是对标记清除算法的的一个优化,他解决了容易产生内存碎片的问题。核心思想分为两个阶段。 标记阶段:使用可达性分析算法,将所有存活的对象进行标记,从GC-Root开始引用遍历出所有存活的对象。

    整理阶段:将所有存活对象引动到堆的一端。清理掉存活对象的内存空间。

  • 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数来设定)后,如果对象还存活,那么该对象会进入老年代。

minorGC、majorGC、fullGC的区别,什么场景触发full GC

在Java中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将GC分为三种类型:Minor GC(也称为Young GC)、Major GC(有时也称为Old GC)、以及Full GC。以下是这三种GC的区别和触发场景:

Minor GC (Young GC)

  • 作用范围:只针对年轻代进行回收,包括Eden区和两个Survivor区(S0和S1)。

  • 触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(Old Generation)。

  • 特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。

Major GC

  • 作用范围:主要针对老年代进行回收,但不一定只回收老年代。

  • 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。

  • 特点:相比Minor GC,Major GC发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。

Full GC

  • 作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。

  • 触发条件

    • 直接调用System.gc()Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。

    • Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。

    • 当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。

  • 特点:Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少Full GC的触发。

垃圾回收器 CMS 和 G1的区别?

区别一:使用的范围不一样:

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用

  • G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用

区别二:STW的时间:

  • CMS收集器以最小的停顿时间为目标的收集器。

  • G1收集器可预测垃圾回收 (opens new window)的停顿时间(建立可预测的停顿时间模型)

区别三: 垃圾碎片

  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片

  • G1收集器使用的是“标记-整理”算法,进行了空间整合,没有内存空间碎片。

区别四: 垃圾回收的过程不一样

img

注意这两个收集器第四阶段得不同

区别五: CMS会产生浮动垃圾

  • CMS产生浮动垃圾过多时会退化为serial old,效率低,因为在上图的第四阶段,CMS清除垃圾时是并发清除的,这个时候,垃圾回收线程和用户线程同时工作会产生浮动垃圾,也就意味着CMS垃圾回收器必须预留一部分内存空间用于存放浮动垃圾

  • 而G1没有浮动垃圾,G1的筛选回收是多个垃圾回收线程并行gc的,没有浮动垃圾的回收,在执行‘并发清理’步骤时,用户线程也会同时产生一部分可回收对象,但是这部分可回收对象只能在下次执行清理是才会被回收。如果在清理过程中预留给用户线程的内存不足就会出现‘Concurrent Mode Failure’,一旦出现此错误时便会切换到SerialOld收集方式。

什么情况下使用CMS,什么情况使用G1?

CMS适用场景:

  • 低延迟需求:适用于对停顿时间要求敏感的应用程序。

  • 老生代收集:主要针对老年代的垃圾回收。

  • 碎片化管理:容易出现内存碎片,可能需要定期进行Full GC来压缩内存空间。

G1适用场景:

  • 大堆内存:适用于需要管理大内存堆的场景,能够有效处理数GB以上的堆内存。

  • 对内存碎片敏感:G1通过紧凑整理来减少内存碎片,降低了碎片化对性能的影响。

  • 比较平衡的性能:G1在提供较低停顿时间的同时,也保持了相对较高的吞吐量。

G1回收器的特色是什么?

G1 的特点:

  • G1最大的特点是引入分区的思路,弱化了分代的概念。

  • 合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷

G1 相比较 CMS 的改进:

  • 算法: G1 基于标记--整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。

  • 停顿时间可控: G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。

  • 并行与并发:G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间。

说说内存泄漏和内存溢出

内存泄漏:在Java中如果不再使用一个对象,但是该对象依然在GC-Root的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。

内存溢出:内存溢出是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。

举个内存泄漏的例子并给出解决方案

1、静态属性导致内存泄露

在Java中,静态属性的生命周期通常伴随着应用整个生命周期。

public class StaticTest {
    public static List<Double> list = new ArrayList<>();
    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }
    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

我们可以使用VisualVM来监控堆内存的变化,如果发现堆内存有一个明显的增长趋势图,并且内存没有被垃圾回收器回收。

针对上述程序,如果吧list的变量前的static关键字去掉,再次执行程序,会发现内存发生了具体的变化。执行方法是,内存逐渐升高,但是执行完方法后,不再有数据指向对应的数据,垃圾回收器便进行了回收操作。

image-20240820112851893

那么如何优化呢?第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。

2、 未关闭的资源

无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。

忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。特别是当程序发生异常时,没有在finally中进行资源关闭的情况。这些未正常关闭的连接,如果不进行处理,轻则影响程序性能,重则导致OutOfMemoryError异常发生。

评论