Java基础知识和JVM补充

I/O

read ->【准备数据 -> 数据就绪 -> 拷贝数据】 (内核) -> read返回

BIO(Blocking i/o)

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。如果BIO要处理多个客户端得i/o就只能创建多个线程来处理。

read ->【准备数据 -> 数据就绪 -> 拷贝数据】 (内核) -> read返回 , 全程阻塞

NIO (Non-blocking/New I/O)

NIO 可以看作是I/O 多路复用模型,属于同步非阻塞 IO 模型。同步非阻塞模型即是可以反复调用read,不会阻塞,用户可以一直发起read调用,而内核在数据拷贝用户空间任然是阻塞的。

read -> 准备数据 -> 数据就绪 -> {拷贝数据 -> read返回} 阻塞

但这个任然存在问题,不断进行轮询数据是否准备好,会消耗大量的CPU资源。

i/o多路复用对其进行了改进,具体做法就是,将准备数据到数据准备就绪阶段进行拆分,用户发送调用,系统准备好之后就返回一个信号,接着用户开始read,内核read返回。

image-20250109135344874

具体Java中的NIO模型,主要有三个部分:Channel,Buffer, Selector。

image-20250109135457476

AIO 是NIO的升级版,是异步 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

**image-20250109140617457

JVM

JVM是运行java程序的虚拟机软件,JVM直接运行在系统中。

一个java程序从开发到运行的全过程

程序员编写.java文件(源代码文件) 计算机使用javac.exe程序将.java文件编译成.class文件(字节码文件) 计算机使用java.exe程序将.class文件送到JVM中运行 运行的过程中随时向核心类库中调用Java编写好的程序来支撑自己编写程序的运行

Java虚拟机包含什么?运行时数据区包含什么

JVM虚拟机包含类加载器、运行时数据区以及执行引擎;运行时数据区有包含方法区、堆、本地方法栈、栈、程序计数器。后面三个为线程独有。

image-20250109214649003

堆分为几个部分?

堆主要存放对象实例和数组。分为新生代(1/3)、老年代(2/3)、大对象区、元空间(原来的老年代)(放入本地内存中)

一个创建对象的过程

image-20250109215452581

  1. 类加载检查:检查指令参数是否能在常量池(方法区)定位到一个类的符号引用。并检测其类是否被加载过、解析和初始化
  2. 分配内存:从堆中分配内存空间
  3. 初始化零值:除了对象头,都初始化零
  4. 进行必要设置:对象头
  5. 执行init方法:直接构造函数,new

补充知识:

一个对象包括对象头(Object Header)+实例数据(Instance Data) +对齐填充(Padding)

  • Mark Word :用于存储对象的运行时数据,包括对象的哈希码、GC状态、锁状态等。这部分信息可以帮助垃圾回收器(GC)管理堆内存及线程安全。

  • 类型指针(Class Pointer) :指向对象的类元数据,记录对象所属的类信息,包括类的结构、方法和字段的描述信息。

image-20250109220348338

一个类的加载过程?

image-20250109194145523

JVM 类加载过程分为加载链接初始化使用卸载这五个阶段,在链接中又包括:验证准备解析

加载:Java 虚拟机规范对 class 文件格式进行了严格的规则,但对于从哪里加载 class 文件,却非常自由。Java 虚拟机实现可以从文件系统读取、从JAR(或ZIP)压缩包中提取 class 文件。除此之外也可以通过网络下载、数据库加载,甚至是运行时直接生成的 class 文件

链接

  • 验证: 确保被加载类的正确性,验证字节流是否符合 class 文件规范
  • 准备:为类的静态变量分配内存并设置变量初始值等。final修饰在编译时就会分配。
  • 解析:解析包括解析出常量池数据和属性表信息。将符号引用替换为正常引用。

初始化:类加载完成的最后一步就是初始化,目的就是为标记常量值的字段赋值,以及执行 <clinit> 方法的过程。JVM虚拟机通过锁的方式确保 clinit 仅被执行一次

使用:程序代码执行使用阶段

卸载:程序代码退出、异常、结束等

类加载器有几种?

类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。每个 Java 类都有一个引用指向加载它的 ClassLoader类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)

JVM内置了三种类加载器

  • BootstrapClassLoader(启动类加载器,JVM内部实现):最顶层的加载类,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  • ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。

什么是双亲委派机制?

ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。意思就是当前级别启动器优先由父类进行加载。

image-20250109195752453

并且类加载器之间的父子关系多使用组合而不是继承来实现。

源代码如下:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

由此可以得到双亲委派机制的执行过程

  1. 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载
  2. 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  3. 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  4. 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常

使用双亲委派机制的好处?

避免了类的重复加载,避免核心类库被修改导致的各种异常以及安全问题

打破双亲委派机制方法?

自定义类加载器,然后重写 loadClass()。例如Tomcat

image-20250109200721716

Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。

什么是Java的垃圾回收机制,如何触发?

垃圾回收(Garbage Collection,Gc)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。主要针对堆。发生以下情况时会进行回收

  • 内存不足
  • 手动请求
  • JVM参数
  • 对象数量和内存使用达到阈值

判断垃圾的方法有哪些?

引用计数法和可达性分析算法

引用计数法:

  • 原理:为每个对象分配一个引用计数器,当有引用的时候,计数器+1,当引用失效时,计数器-1.计数器为0,代表对象不被引用,可以被回收。
  • 缺点:不能解决循环引用的问题(即多个对象相互引用)

可达性分析算法:

  • 原理:从一组GC Roots的对象出发,向下追溯它们的引用对象,及其引用的其它对象。如果一个对象到GC Roots没有任何引用链接,就认为不可达,能够被回收
  • GC Roots对象有 虚拟机栈引用的对象、 方法区在类静态属性引用的对象、本地方法栈中引用的对象、活跃线程的引用。

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

垃圾回收算法是用于自动管理内存的一种机制,其主要目标是回收不再使用或无法访问的对象所占用的内存空间,从而防止内存泄漏、降低内存使用量并提高系统性能。

能够解决

  • 内存泄漏: 一些对象分配了内存无法释放导致的浪费
  • 内存溢出:程序需要的内存超过的可用内存

垃圾回收算法有哪些?

标记-清除算法:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

会导致问题如下:

  • 效率问题:标记和清除两个过程效率都不高。
  • 空间问题:标记清除后有大量内存碎片

复制算法:为了解决算法效率和内存碎片问题。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

会导致问题如下:

  • 可用内存变小为原来的一半
  • 不适合老年代:如果存活对象数量比较大,复制性能会变得差

标记-整理算法:根据老年代的特点提出的一种标记算法。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

问题:

  • 效率不高

分代收集算法(虚拟机的垃圾收集):据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

  • 在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
  • 而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

HotSpot 为什么要分为新生代和老年代?

为了通过不同的垃圾回收策略高效管理内存、优化程序性能、减少停顿时间,并有效应对对象的生命周期特性。这样的设计使得 JVM 能够在高负载条件下保持良好的性能表现。

  • 新生代的回收 :新生代使用的是复制算法(Copying),通常会进行频繁的、快速的垃圾回收。由于新生代中对象的存活率较低,回收效率较高,回收时只需要检查少量对象。这使得新生代的回收速度快,并且内存碎片问题较少
  • 老年代的回收: 老年代中的对象生命周期较长,采用的垃圾回收算法一般更为复杂(如标记-清除或标记-整理),回收的频率较低,目的是减少停顿时间和分配消耗。因此对于老年代的回收会设置较长的间隔。

JAVA的垃圾收集器有哪些?

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Serial(串行)收集器: **新生代采用复制算法,老年代采用标记-整理算法。**只会使用一条垃圾收集线程去完成垃圾收集工作,并且暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

ParNew 收集器**(并发)**:除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。新生代采用标记-复制算法,老年代采用标记-整理算法

Parallel Scavenge 收集器( JDK1.8 默认收集器):关注吞吐量(高效率的利用 CPU)

  • -XX:+UseParallelGC: 使用 Parallel 收集器+ 老年代串行
  • -XX:+UseParallelOldGC: 使用 Parallel 收集器+ 老年代并行

Serial OLD:Serial 收集器的老年代版本

  • 作为 CMS 收集器的后备方案
  • 在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用

Parallel Scavenge OLD: P arallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 并发收集,低停顿。标记清除算法

  • 初始标记:短暂停顿,标记直接与root相连的对象
  • 并发标记:同时开启GC和用户线程,用一个闭包去记录可达对象。会跟踪记录用户对象发生的引用更新
  • 重新标记:修正并发标记期间因为用户运行导致标记变动的对象的标记对象
  • 并发清除:开启用户线程,同时GC对未标记区域进行扫除

问题:

  • CPU资源敏感
  • 无法处理浮动垃圾(被对象的引用所删除(不再可达),但还尚未被垃圾回收器回收的对象。)
  • 会有碎片空间

G1收集器(JDK9默认):G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

  • 初始标记:短暂停顿,标记直接与GC Roots相连的对象
  • 并发标记:与应用并发运行,标记所有可达对象。
  • 最终标记:处理并发标记阶段结束后残留的少量未处理的引用变更
  • 筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。G1收集器会试图首先回收那些利用率较低、垃圾较多的区域

ZGC :ZGC 也采用标记-复制算法。

引用类型有哪些?

强引用(Strong Reference) :如果一个对象具有强引用,Java垃圾收集器不会回收它,即使系统内存不足时也是如此。它的典型用法是通过直接赋值给对象变量。

Object obj = new Object();

软引用(Soft Reference) :描述一些有用但不是必需的对象。当系统内存不足时,垃圾收集器会回收这些对象。软引用可以通过 java.lang.ref.SoftReference 类创建。

弱引用(Weak Reference) :弱引用描述一些非必需对象。只有当系统进行垃圾收集时,弱引用指向的对象会被回收。可以使用 java.lang.ref.WeakReference 类来创建弱引用。

虚引用(Phantom Reference) :它并不会决定对象的生命周期。使用 java.lang.ref.PhantomReference 类可以创建虚引用。虚引用的存在仅用于在对象被垃圾回收时收到一个系统通知。

弱引用具体场景?

  • 缓存系统
  • 对象池
  • 避免内存泄漏

minorGC、majorGC、fullGC的区别?

Minor GC 指的是对年轻代内存区域(Young Generation)的垃圾收集。

Major GC 指的是对老年代内存区域(Old Generation)的垃圾收集。

Full GC 是对整个堆内存区域的垃圾收集,包括年轻代和老年代和元空间。

参考

https://javaguide.cn/java

https://xiaolincoding.com/interview/java.html

gpt