自学内容网 自学内容网

小林Coding—Java「五、Java虚拟机面试篇」

五、Java虚拟机面试篇(难⭐️⭐️⭐️)

内存模型

JVM的内存模型介绍一下

JVM运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈 五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
在这里插入图片描述
JVM的内存结构主要分为以下几个部分:

  • 元空间: 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间不在虚拟机中,而是使用本地内存。(方法区是在元空间中的)
  • Java虚拟机栈: 每个线程有一个私有的栈(即Java虚拟机栈,每个线程都有一个),随着线程的创建而创建。栈里面存着的是一种叫 “栈帧” 的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。
  • 本地方法栈: 与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法栈执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法和数据结构没有强制规定,因此虚拟机可以自由实现它。
  • 程序计数器: 程序计数器可以看成是 当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一个线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。
  • 堆内存: 堆内存是JVM所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过GC(垃圾回收)。当申请不到空间时会抛出OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。堆的唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在堆中。
  • 直接内存: 直接内存主要通过 NIO(New Input/Output)类实现,引入了一种基于通道(Channel)和缓冲区Buffer的IO方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免了这Java堆和Native堆中来回复制数据。

JVM内存模型里的堆和栈有什么区别?

  • 用途: 栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据。 每当一个方法被调用,一个栈帧(stack frame)就会在栈中创建,用于存储该方法的信息。当方法执行完毕,栈帧也会被移除。堆用于存储对象的实例(包括类的实例和数组)。当你使用new关键字创建一个对象时,对象的实例就会在堆上分配空间。
  • 生命周期: 栈中的数据具有明确的生命周期:当一个方法调用结束时其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,对象会在垃圾回收机制(Garbage Collection, GC)检测到对象不再被引用才被回收。
  • 存取速度: 栈的速度比堆快,因为栈遵循LIFO的原则,操作简单。堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多时间,且垃圾回收机制的运行也会影响性能。
  • 存储空间: 栈的空间大小较小且固定,由操作系统管理。当栈溢出通常是因为递归过审或者局部变量过大。堆的空间较大且动态扩展,由JVM管理。堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象。
  • 可见性: 每个线程都有自己的栈,是私有的。堆是堆所有线程共享的,所有线程都可以访问堆上的对象。

栈中存的到底是指针还是对象?

栈(Stack)主要用于管理线程的局部变量和方法调用的上下文,而堆Heap则是用于存储所有类的实例和数组。

栈中存储的是:基本类型的数据(int,double)和对象的引用,而不是对象本身。

栈中存储的不是对象,而是对象的引用。也就是说,当栈方法中声明一个对象时,如MyObject obj = new MyObject(); 这里的obj实际上是一个存储在栈上的引用,指向堆中世纪的对象实例。这个引用是一个固定大小的数据,它指向堆中分配给对象的内存区域。

堆分为哪几部分呢?

Java堆主要用于存放对象实例和数组。堆的划分通常可以分为以下几个部分:
在这里插入图片描述

  • 新生代(Young Generation): 新生代分为Eden Space和Survivor Space。在Eden Space中,大多数新创建的对象首先存放在这里。 Eden区相对较小,当Eden区满时,会触发一次Minor GC(新生代垃圾回收)。在Survivor Spaces(黄色)中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会从Eden区被移动到其中一个Survivor空间,以继续他们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。
  • 老年代(Old Generation/ Tenured Generation): 存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也被称为 Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。
  • 元空间(Metaspace): 从Java 8开始,元空间取代永久代,用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。
  • 大对象区(Large Object Space/ Humongous Objects): 在某些JVM实现中(如G1垃圾收集器),为大对象分配了专门的区域,称为大对象区。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。

程序计数器的作用,为什么是私有的?

Java程序是支持多线程一起运行的,多个线程一起运行的时候cpu会有一个调动器组件给他们分配时间片,比如说会给线程1分给1个时间片,它在时间片内如果代码没有执行完,就会把线程1的状态 执行一个咱村,切换到线程2去,执行线程2的代码。等线程2的代码执行到一定程度后,线程2的时间片用完了再切换回来,再继续执行线程1剩余部分的代码。

我们考虑一下,如果在线程切换的过程中,下一条执行执行到哪里了,是不是还会用到我们的程序计数器。每个线程都有自己的程序计数器,因为他们各自执行的代码的指令是不一样的啊,所以每个线程都要有自己的计数器。

方法区中的方法的执行过程?
(方法区是在元空间中的,这个部分在Java虚拟机(JVM)中用于存储类元数据、常量以及静态变量等)

  1. 解析方法调用: JVM会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)
  2. 栈帧创建: 在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息
  3. 执行方法: 执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈、跳转控制、对象创建、方法调用等。
  4. 返回处理: 方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。

方法区中包含哪些东西?(不懂⭐️⭐️⭐️)

《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等

  • 类信息:包含类的结构信息、类的访问修饰符、父类与接口等信息。
  • 常量池:存储类和接口中的常量,包括字面值常量、符号引用、运行时常量池。
  • 静态变量:存储类的静态变量,这些变量在类初始化的时候被赋值。
  • 方法字节码:存储类的方法字节码,即编译后的代码。
  • 符号引用:存储类和方法的符号引用,是一种不同于直接引用的引用类型。
  • 运行时常量池:存储着在类文件中的常量池数据,在类加载后在方法区生成该运行时常量池。
  • 常量池缓存:用于提升类加载的效率。

String保存在哪里呢?

String保存在字符串常量池中,不同于其他对象,String的值是不可变的,而且可以被多个引用共享。

String s = new String(“abc”) 执行过程中分别对应哪些内存区域?

首先,我们看到这个代码中有一个new关键字,我们知道new指令是创建一个类的实例对象并完成加载初始化的,因为这个字符串对象是在运行期才能确定的,创建的字符串对象是在堆内存上的。

其次,在String的构造方法中传递了一个字符串abc,由于这里的abc是被final修饰的属性,所以它是一个字符串变量。在首次构建这个对象时,JVM拿字面量“abc”去字符串常量池视图获取对应String对象的引用。于是在中创建一个abc的String对象,并将其引用保存到字符串常量池中,然后返回;

所以,如果abc这个字符串常量不存在,则创建两个对象,分别是abc这个字符串常量,以及new String这个实例对象。如果abc这个字符串常量存在,则只会创建一个对象。

引用类型有哪些? 有什么区别?

引用类型主要分为强软弱虚四种。

  • 强引用指的是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
  • 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统中发生内存溢出前才会对这类引用的对象进行回收。
  • 弱引用用WeakReference来描述,若因就用的强度比软引用更低一点。弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
  • 虚引用也被称为幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用。同样,当发生GC时,虚引用也会被回收。可以用虚引用来管理堆外内存。

弱引用了解吗?举例说明在哪里可以用?

Java的弱引用是一种引用类型,它不会组织一个对象被垃圾回收。

在Java中弱引用 由 WeakReference类实现。弱引用的一个主要用途是创建非强制性的对象引用,这些引用在内存压力大时会被垃圾回收器清理,从而避免内存泄漏。

弱引用的使用场景:

  • 缓存系统: 使用弱引用来维护缓存,可以让JVM中需要更多内存时自动清理这些缓存对象。
  • 对象池: 中对象池汇总,弱引用可以用于管理那些暂时不使用的对象。当对象不再被强引用时,他们可以被垃圾回收,释放内存。
  • 避免内存泄露: 当一个对象不应该被长期引用时,使用弱引用可以防止该对象被意外保留,从而避免潜在的内存泄露。

示例代码:
假设我们有一个缓存系统,使用弱引用来维护缓存中的对象:

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

public class CacheExample {

    private Map<String, WeakReference<MyHeavyObject>> cache = new HashMap<>();

    public MyHeavyObject get(String key) {
        WeakReference<MyHeavyObject> ref = cache.get(key);
        if (ref != null) {
            return ref.get();
        } else {
            MyHeavyObject obj = new MyHeavyObject();
            cache.put(key, new WeakReference<>(obj));
            return obj;
        }
    }

    // 假设MyHeavyObject是一个占用大量内存的对象
    private static class MyHeavyObject {
        private byte[] largeData = new byte[1024 * 1024 * 10]; // 10MB data
    }
}

在这个例子中,使用WeakReference来存储MyHeavyObject实例。当内存压力增大时,垃圾回收器可以自由的回收这些对象,而不会影响缓存的正常运行。

如果一个对象被垃圾回收,下次尝试从缓存中获取时,get()方法会返回null,这时我们可以重新创建对象并将其放入换从中。因此使用弱引用一定注意,一旦对象被垃圾回收,通过弱引用获取的对象可能会变成null,因此在使用钱通常会检查这一点。

内存泄露和内存溢出的理解?

内存泄漏: 内存泄露指的是程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。虽然在Java中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加。

内存泄露的常见原因:

  • 静态集合: 使用静态数据结构(如HashMap或ArrayList)存储对象,且未清理。
  • 事件监听: 未取消对事件源的监听,导致对象持续被引用。
  • 线程: 未停止的线程可能持有对象引用,无法被回收。

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

内存溢出的常见原因:

  • 大量对象创建: 程序中不断创建大量对象,超出JVM堆的限制。
  • 持久引用: 大型数据结构(如缓存、集合等)长时间持有对象引用,导致内存积累。
  • 递归调用: 深度递归导致栈溢出。

JVM内存结构有哪几种内存溢出情况?

  • 堆内存溢出: 当出现java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。原因是代码中可能存在大对象分配,或者发生了内存泄漏,导致栈多次GC后,还是无法找到一块足够大的内存容纳当前对象。
  • 栈溢出: 如果我们写一段程序不断进行递归调用且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM实际会抛出StackOverFlowError。当天如果JVM试图去扩展栈空间的时候失败,就会抛出OutOfMemoryError。
  • 元空间溢出: 元空间的溢出,会抛出java.lang.OutOfMemoryError: Metaspace。这个异常出现的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。(元空间是对java方法区的实现。
  • 直接内存内存溢出: 在使用ByteBuffer中的allocateDirect()的时候会用到,很多JavaNIO(如netty)的框架中被封装为其他的方法,出现该问题会抛出出现该问题时会抛出java.lang.OutOfMemoryError: Direct buffer memory异常。

类初始化和类加载

类加载是什么?

在这里插入图片描述

创建对象的过程?

在这里插入图片描述

  1. 类加载检查: 虚拟机遇到一条 new 指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  2. 分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
  3. 初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 进行必要设置,如对象头: 初始化零值完成后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否应用偏向锁等,对象头会有不同的设置方式。
  5. 执行init方法: 在上面工作完成后,从虚拟机视角看一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——构造函数,即class文件中的方法还没有执行,所有的字段都还为0,对象需要的其他资源和状态信息 还没有按照预定的意图构造好。所以一般来说,执行new指令后会接着执行方法, 把对象按照程序员的意图进行初始化,这样一个真正可用的对象才算完全被构造出来。

1类加载检查解释:

  1. 遇到new指令:
    • 当JVM执行到一条new指令时,这条指令的目的是创建一个类的实例。
  2. 检查常量池:
  • 每个类或接口都有一个常量池,这是该类中编译时生成的一组常量和符号的集合,存储了包括类和接口明、方法和字段的引用。
  • 当new指令执行时,JVM首先会查找常量池,以定位到一个类的符号引用。符号引用是对类的直接引用,它包含了类的全限定名。
  1. 检查类的加载状态:
    • 一旦JVM中常量池中找到了类的符号引用,接下来需要检查这个类是否已经被加载、解析和初始化。
      • 加载:是指JVM找到对应的类文件(.class文件),并把类的原始信息转化为JVM可以使用的格式。这一阶段会创建一个Class对象来代表这个类。
      • 解析:处理类、接口、字段和方法的符号引用,将这些符号引用转换为JVM可以直接引用的直接引用。
      • 初始化:涉及执行类构造器 () 方法的过程,这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。
  2. 执行类加载过程:
    • 如果类尚未被加载,那么在实例化之前,JVM必须先通过类加载器加载该类。类加载器通常会先查找这个类的字节码(在.class文件或其他资源中),并形成JVM可以处理的数据结构(即Class对象)
    • 在加载类的过程中,如果类还依赖其他类,则这些类也需要被递归加载。

对象的生命周期

对象的生命周期包括:创建、使用、销毁 三个阶段。

  • 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
  • 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
  • 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。

类加载器有哪些?

  • 启动类加载器(Bootstrap Class Loader): 这是最顶层的类加载器,负责加载Java的核心库(如位于jre/lib/rt.jar中的类),它是用C++编写的,是JVM的一部分。启动类加载器无法被java程序直接引用。
  • 扩展类加载器(Extension Class Loader): 是用Java写的,继承自ClassLoader类,负责加载Java扩展目录(jre/lib/ext或由系统变量java.ext.dirs指定的目录)下的jar包和类库。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器。
  • 系统类加载器(System Class Loader)/应用程序类加载器(Application Class Loader): 负责加载用户类路径(ClassPath)上的指定类库,是我们平时编写Java程序时默认使用的类加载器。系统类加载器的父加载器是扩展类加载器。
  • 自定义类加载器(Custom Class Loader): 开发者可以根据需求定制类的加载方式,比如从网络加载class文件、数据库、甚至是加密的文件中加载类等。自定义类加载器可以用来扩展Java应用程序的灵活性和安全性,是Java动态性的一个重要体现。

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

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

双亲委派模型的作用:

  • 保证类的唯一性: 通过委托机制,确保了所有加载请求都会传递到启动类加载器,避免了不同类加载器加载相同类的情况,保证了Java核心类库的统一性,也防止了用户自定义类覆盖核心类库的可能。
  • 保证安全性: 由于Java核心库被启动类加载器加载,而启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类,增强了系统的安全性。例如,恶意代码无法自定义一个java.lang.System类并加载到JVM中,因为这个请求会被委托给启动类加载器,而启动类加载器只会加载标准的Java库中的类。
  • 支持隔离和层次划分: 双亲委派模型支持不同层次的类加载器服务于不同的类加载需求,如应用程序类加载器加载用户代码,扩展类加载器加载扩展框架,启动类加载器加载核心库。这种层次化的划分有助于实现沙箱安全机制,保证了各个层级类加载器的职责清晰,也便于维护和扩展。
  • 简化了加载流程: 通过委派,大部分类能够被正确的类加载器加载,减少了每个加载器需要处理的类的数量,简化了类的加载过程,提高了加载效率。

讲一下类加载过程?(一点也不懂⭐️⭐️⭐️⭐️⭐️)

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:
在这里插入图片描述

  • 加载: 通过类的全限定名(包名+类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 连接: 验证、准备、解析 3 个阶段统称为连接。
    • 验证: 确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证
    • 准备: 为类中的静态字符分配内存,并设置默认的初始值,比如int的为0。被final修饰的static字段不会被设置,因为final在编译的时候就分配了。
    • 解析: 解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经存在于内存中了。
  • 初始化: 初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法(),要注意的是这里的构造器方法不是开发者写的,而是编译器自动生成的。
  • 使用: 使用类或创建对象
  • 卸载: 如果有一下情况,类就会被卸载:1. 该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。3. 类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

讲一下类的加载和双亲委派原则(不懂⭐️⭐️⭐️)

我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化。

首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。

加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。

第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:

  • 验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
  • 准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
  • 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。

最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

再来谈谈双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。

垃圾回收

什么是Java里的垃圾回收?如何触发垃圾回收?

垃圾回收(Grabage Collection,GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:

  • 内存不足时: 当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收
  • 手动请求: 虽然垃圾回收是自动的,开发者可以通过调用System.gc()或者Runtime.getRuntime().gc()建议JVM进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM参数: 启动Java应用时可以通过JVM参数来调整垃圾回收的行为,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等
  • 对象数量或内存使用到达阈值: 垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

判断垃圾的方法有哪些?

在Java中判断对象是否为垃圾(即不再被使用,可以被垃圾回收器回收)主要依据两种主流的垃圾回收算法来实现:引用计数器和可达性分析算法

引用计数器(Reference Counting)

  • 原理: 为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器+1。当引用失效时,计数器-1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
  • 缺点: 不能解决循环引用的问题:即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。

可达性分析算法(reachability Analysis)
在这里插入图片描述
Java虚拟机只要采用此算法来判断对象是否为垃圾。

  • 原理: 从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯他们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。 GC Roots对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI(JavaNative Interface)引用的对象,活跃线程的引用等。

垃圾回收算法是什么,是为了解决了什么问题?

JVM有垃圾回收机制的原因是为了解决内存管理的问题。在传统的编程语言中,开发人员需要手动分配和释放内存,这可能导致内存泄漏、内存溢出等问题。而Java作为一种高级语言,旨在提供更简单、更安全的编程环境,因此引入了垃圾回收机制来自动管理内存。

垃圾回收机制的主要目标是自动检测和回收不再使用的对象,从而释放它们所占用的内存空间。这样可以避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)。同时,垃圾回收机制还可以防止内存溢出(即程序需要的内存超过了可用内存的情况)。

通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。

垃圾回收算法有哪些?

  • 标记-清除算法: 标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
  • 复制算法: 为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
  • 标记-整理算法: 复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
  • 分代回收算法: 分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

标记清除算法的缺点是什么?

主要缺点有两个:

  • 一个是效率问题,标记和清楚过程的效率都不高。
  • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

垃圾回收算法哪些阶段会stop the world?(不懂⭐️⭐️⭐️)

stop the world(STW)是指为了执行垃圾回收(GC),所有的引用线程都必须停止工作,即所有的应用线程都必须停止工作,即整个GC完成相关服务,STW是垃圾回收过程中常见的一部分,它会影响应用的响应时间,因为在STW期间,用户或服务请求不得不等待。

下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示:
在这里插入图片描述
G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。

标记阶段停顿分析

  • 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
  • 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
  • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。

清理阶段停顿分析

  • 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。

复制阶段停顿分析

  • 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。

四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。

因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。

minorGC、majorGC、fullGC的区别,什么场景触发full 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 BC)或元空间(Java 8 AD)空间不足时。
  • 特点: Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop the world),遍历整个堆内存来查找和回收不再使用的对象。因此赢尽量减少Full GC的触发。

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

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

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
  • G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用。

区别二:STW的时间

  • CMS收集器以最小的停顿时间为目标。
  • G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)

区别三:垃圾碎片

  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,没有内存空间碎片。

区别四:垃圾回收的过程不一样
在这里插入图片描述
区别五: 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的停顿时间。

GC只会对堆进行GC吗?

JVM的垃圾回收器不仅仅会对堆进行垃圾回收,他还会堆方法区进行垃圾回收。

  1. 堆Heap: 堆是用于存储对象实例的内存区域。大部分的垃圾回收工作都发生在堆上,因为大多数对象都会被分配在堆上,而垃圾回收的重点也是回收堆中不再被引用的对象,以释放内存空间。
  2. 方法区(Method Area): 方法区是用于存储类信息、常量、静态变量等数据的区域。虽然方法去中的垃圾回收和堆有所不同,但是童颜存在对不再需要的常量,无用的类信息进行清理的过程。

原文地址:https://blog.csdn.net/qq_45895217/article/details/143828809

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!