Java EE:9.JVM(课件内容-上篇)

目录

JVM(课件内容)

[1.JVM 简介](#1.JVM 简介)

[1.1 JVM 发展史](#1.1 JVM 发展史)

[1.Sun Classic VM](#1.Sun Classic VM)

[2.Exact VM](#2.Exact VM)

[3.HotSpot VM](#3.HotSpot VM)

[目前 HotSpot 占用绝对的市场地位,称霸武林](#目前 HotSpot 占用绝对的市场地位,称霸武林)

4.JRockit

[JRockit 是专注于服务器端应用,目前在 HotSpot 的基础上,移植 JRockit 的优秀特性](#JRockit 是专注于服务器端应用,目前在 HotSpot 的基础上,移植 JRockit 的优秀特性)

[5.J9 JVM](#5.J9 JVM)

[6.Taobao JVM(国产研发)](#6.Taobao JVM(国产研发))

[1.2 JVM 和《Java虚拟机规范》](#1.2 JVM 和《Java虚拟机规范》)

[2.JVM 运行流程](#2.JVM 运行流程)

[JVM 执行流程](#JVM 执行流程)

[3.JVM 运行时数据区](#3.JVM 运行时数据区)

[3.1 堆(线程共享)](#3.1 堆(线程共享))

[3.2 Java虚拟机栈(线程私有)](#3.2 Java虚拟机栈(线程私有))

什么是线程私有?

[3.3 本地方法栈(线程私有)](#3.3 本地方法栈(线程私有))

[3.4 程序计数器(线程私有)](#3.4 程序计数器(线程私有))

[3.5 方法区(线程共享)](#3.5 方法区(线程共享))

[JDK 1.8 元空间的变化](#JDK 1.8 元空间的变化)

运行时常量池

小结

[3.6 内存布局中的异常问题](#3.6 内存布局中的异常问题)

①Java堆溢出

②虚拟机栈和本地方法栈溢出

[4.JVM 类加载](#4.JVM 类加载)

①类加载过程

1)加载

2)验证

3)准备

4)解析

5)初始化

②双亲委派模型

什么是双亲委派模型?

双亲委派模型的优点

③破坏双亲委派模型


JVM(课件内容)

课程目标:

1.了解 JVM 的发展史

2.了解 JVM 运行原理

3.掌握 JVM 基本组成

4.掌握 JVM 垃圾回收算法

5.掌握类加载机制

6.掌握 JMM

版本更新内容:

1.JVM 运行时数据区所有部分的作用做了一个说明(解决了为什么需要这些区域的问题)补充了一些图片

2.方法区的实现:永久代/元空间举例说明(汽车的动能提供装置),添加了图片

3.添加图片:JVM 演示内存溢出时,Idea 设置的图片

4.新增垃圾收集器的作用,以及为什么有那么多的垃圾收集器的原因

5.一个对象的一生吗,JVM 总结和执行流程图

6.破坏双亲委派模型的案例(SPI→JDBC)

1.JVM 简介

1.1 JVM 发展史

1.Sun Classic VM

早在 1996 年 Java1.0 版本的时候,Sun 公司发布了一款名为 Sun Classic vm 的Java虚拟机,它同时也是世界上第一款商业Java虚拟机,JDK1.4时完全被淘汰

这款虚拟机内部只提供解释器

如果使用 JIT 编译器,就需要进行外挂,但是一旦使用了 JIT 编译器,JIT 就会接管虚拟机的执行系统

解释器就不再工作,解释器和编译器不能配合工作

现在 HotSpot 内置了此虚拟机

2.Exact VM

为了解释上一个虚拟机问题,JDK1.2时,sun 提供了此虚拟机

Exact 具备现代高性能虚拟机的雏形,包含了一下功能:

1.热点探测(将热点代码编译为字节码加速程序执行)

2.编译器与解析器混合工作模式

只在 Solaris 平台短暂使用,其他平台上还是 classic vm

英雄气短,终被 Hptspot 虚拟机替换

3.HotSpot VM

HotSpot 历史

1.最初由一家名为"Longview Technologies"的小公司设计

2.1997年,此公司被 Sun 收购;2009年,Sun 公司被甲骨文收购

3.JDK1.3 时,HotSpot VM 成为默认虚拟机

目前 HotSpot 占用绝对的市场地位,称霸武林

不管是现在仍在广泛使用 JDK6,还是使用比较多的 JDK8中,默认的虚拟机都是HotSpot

Sun/Oracle JDK 和 OpenJDK 的默认虚拟机,从服务器、桌面到移动端、嵌入式都有应用

名称中的 HotSpot 指的就是它的热点代码探测技术,它能通过计数器找到最具编译价值的代码,触发即时编译(JIT)或栈上替换;通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡

4.JRockit

JRockit 是专注于服务器端应用,目前在 HotSpot 的基础上,移植 JRockit 的优秀特性

它可以不太关注程序的启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行

大量的行业基准测试显示,JRockit JVM 是世界上最快的 JVM

使用 JRockit 产品,客户已经体验到了显著的性能提高(一些超过了70%)和硬件成本的减少(达50%)

优势:全面的Java运行时解决方案组合

JRockit 面向延迟敏感型应用的解决方案 JRockit Real Time 提供以毫秒或微秒级的 JVM 响应时间,适合财务、军事指挥、电信网络的需要

MissionControl 服务套件,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具

2008,BEA被 Oracle 收购

Oracle 表达了整合两大优秀虚拟机的工作,大致在 JDK8中完成,整合的方式是在 HotSpot 的基础上,移植 JRockit 的优秀特性

5.J9 JVM

全称:IBM Technology for Java Virtual Machine,简称 IT4J,内部代号:J9

市场定位于Hotspot 接近,服务器端、桌面应用、嵌入式等多用途JVM,广泛用于 IBM 的各种 Java 产品

目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的 Java 虚拟机(在 IBM 自己的产品上稳定)

2017 年左右,IBM 发布了开源 J9 JVM,命名 OpenJ9,交给 Eclipse 基金会管理,也称为 Eclipse Open J9

6.Taobao JVM(国产研发)

6.Taobao JVM(国产研发)

由 AliJVM 团队发布。阿里,国内使用 Java 最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品

基于 OpenJDK 开发了自己的定制版本 AlibabaJDK,简称 AJDK。是整个阿里 Java 体系的基石

基于 OpenJDK HotSpot JVM 发布国内第一个优化、深度定制且开源的高性能服务器版 Java 虚拟机,它具有以下特点(了解即可):

1.创新的 GCIH(GC invisible heap)技术实现了 off - heap,即将生命周期较长的 Java 对象从 heap 中移到 heap 之外,并且 GC 不能管理 GCIH 内部的 Java 对象,以此达到降低 GC 的回收评率和提升 GC 的回收效率的目的

2.GCIH 中的对象还能够在多个 Java 虚拟机进程中实现共享

3.使用 crc32 指令实现 JVM intrinsic 降低 JNI 的调用开销

4.PMU hardware 的 Java profiling tool 和诊断协助功能

5.针对大数据场景的 ZenGC

taobao JVM 应用在阿里产品上性能高,硬件严重依赖 intel 的 CPU,损失了兼容性,但提高了性能,目前已经在淘宝、天猫上线,把 Oracle 官方 JVM 版本全部替换了

1.2 JVM 和《Java虚拟机规范》

以上的各种 JVM 版本,比如 HotSpot 和 J9 JVM,都可以看做是不同厂商实现 JVM 产品的具体实现,而它们(JVM)产品的实现必须要符合《Java 虚拟机规范》,《Java 虚拟机规范》是 Oracle 发布 Java 领域最重要和最权威的著作,它完整且详细的描述了 JVM 的各个组成部分

PS:本文以下部分,默认都是使用 HotSpot,也就是 Oralce Java 默认的虚拟机为前提来进行介绍的

2.JVM 运行流程

JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?

JVM 执行流程

程序在执行之前先要把 Java 代码转换成字节码(class 文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area),而字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器**执行引擎(Execution Engine)**将字节码翻译成底层系统指令再交由 CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface)来实现整个程序的功能,这就是这 4个主要组成部分的职责与功能

总结来看,JVM 主要通过分为以下 4个部分,来执行 Java 程序的,它们分别是:

1.类加载器(ClassLoader)

2.运行时数据区(Runtime Data Area)

3.执行引擎(Execution Engine)

4.本地库接口(Native Interface)

3.JVM 运行时数据区

JVM 运行时数据区域也叫内存布局,但需要注意的时它和 Java内存模型(Java Mermory Model,简称 JMM)完全不同,属于完全不同的两个概念,它由以下 5大部分组成:

3.1 堆(线程共享)

堆的作用:程序中创建的所有对象都保存在堆中

我们常见的 JVM 参数设置 -Xms10m 最小启动内存是针对堆的,-Xmx10m 最大运行内存也是针对堆的

ms 是 memory start 简称,mx 是 memory max 的简称

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代,新生代还有 3个区域:一个 Endn + 两个 Survivor(S0/S1)

垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清除掉

3.2 Java虚拟机栈(线程私有)

Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈

Java 虚拟机栈中包含了以下 4 部分:

1.局部变量表:存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小,简单来说就是存放方法参数和局部变量

2.操作栈:每个方法会生成一个先进后出的操作栈

3.动态链接:指向运行时常量池的方法引用

4.方法返回地址:PC 寄存器的地址

什么是线程私有?

由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令,因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们就把类似这类区域称之为"线程私有"的内存

3.3 本地方法栈(线程私有)

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的

3.4 程序计数器(线程私有)

程序计数器的作用:用来记录当前线程执行的行号的

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器

如果当前线程正在执行的是一个 Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址:如果正在执行的是一个 Native 方法,这个计数器值为空

程序计数器内存区域是唯一一个在 JVM 规范中没有规定任何 OOM 情况的区域!

3.5 方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的

在《Java 虚拟机规范》中把此区域称之为"方法区",而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)

PS:永久代(PermGen)和元空间(Metaspace)是 HotSpot 中对《Java虚拟机规范》中方法区的实现,它们三者之间的关系就好比,对于一辆汽车来说它定义了一个部分叫做"动能提供装置",但对于不同的汽车有不同的实现技术,比如对于燃油车来说,它的"动能提供装置"的实现技术就是汽油发动机(简称发动机),而对于电动汽车来说,它的"动能提供装置"的实现就是电动发动机(简称电机),发动机和电机就相当于永久代和元空间一样,它是对于"制动器"也就是方法区定义的实现

JDK 1.8 元空间的变化

1.对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内存的参数影响了,而是与本地内存的大小有关

2.JDK 8 中将字符串常量池移动到了堆中

运行时常量池

运行时常量池是方法去的一部分,存放字面量与符号引用

字面量:字符串(JDK 8 移动到堆中)、final 常量、基本数据类型的值

符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

小结

3.6 内存布局中的异常问题

①Java堆溢出

Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免 GC 清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常

上节中已经讲到了,可以设置 JVM 参数 -Xms:设置堆地最小值、-Xmx:设置堆最大值,下面我们来看一个 Java 堆 OOM 的测试,测试以下代码之前先设置 IDEA 的启动参数,如下图所示:

PS:JVM 参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError

范例:观察 Java Heap OOM

java 复制代码
/**
 * JVM 参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
 * @author 38134
 *
 */
public class Test {
    static class OOMObject {

    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();

        while(true) {
            list.add(new OOMObject());
        }
    }
}

Java 堆内存的 OOM 异常是实际应用中最常见的内存溢出情况,当出现 Java 堆内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会进一步提示"Java heap space",当出现"Java heap space"则很明确的告知我们,OOM 发生在堆上

此时要对Dump 出来的文件进行分析,以 MAT 为例,分析问题的产生到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)

内存泄露:泄露对象无法被 GC

内存溢出:内存对象确实还应该存活,此时要根据 JVM 堆参数与物理内存相比较检查是否还应该把 JVM 堆内存调大,或者检查对象的生命周期是否过长

以上是我们处理 Java 堆内存的简单方法,处理具体这类问题需要的工具以及知识我们放到下面第四小节具体来说

②虚拟机栈和本地方法栈溢出

由于我们 HotSpot 虚拟机栈与本地方法栈合二为一,因此对于 HotSpot 来说,栈容量只需要由 -Xss 参数来设置

关于虚拟机栈会产生的两种异常:

如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出 StackOverFlow 异常

如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出 OOM 异常

范例:观察 StackOverFlow 异常(单线程环境下)

java 复制代码
/**
 * JVM参数为:-Xss128k
 * @author 38134
 *
 */
public class Test {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        Test test = new Test();
        try {
            test.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length: "+test.stackLength);
            throw e;
        }
    }
}

出现 StackOverflowError 异常时有错误堆栈可以阅读,比较号找到问题所在,如果使用虚拟机默认参数,栈深度在多数情况下达到 1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用

如果是因为多线程导致的内存溢出问题,在不能减少些线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程

范例:观察多线程下的内存溢出异常

java 复制代码
/**
 * JVM参数为:-Xss2M
 * @author 38134
 *
 */
public class Test {

    private void dontStop() {
        while(true) {
            
        }
    }
    public void stackLeakByThread() {
        while(true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.stackLeakByThread();
    }
}

以上代码运行需谨慎,先记得保存手头所有工作

4.JVM 类加载

①类加载过程

从上面的图片我们可以看出整个 JVM 执行的流程中,和程序员关系最密切的就是类加载的过程了,所以接下来我们来看下类加载的执行流程

对于一个类来说,它的生命周期是这样的:

其中前 5 步时固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来说总共分为以下几个步骤:

1.加载

2.连接

a.验证

b.准备

c.解析

3.初始化

下面我们分别来看每个步骤的具体执行内容

1)加载

"加载"(Loading)阶段是整个"类加载"(Class Loading)过程中的一个阶段,它和类加载(Class Loading)是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了

在加载 Loading 阶段,Java 虚拟机需要完成以下三件事情:

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

2)验证

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证选项:

文件格式验证

字节码验证

符号引用验证

3)准备

准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段

比如此时有这样一行代码:

public static int value = 123

它是初始化 value 的 int 值为 0,而非 123

4)解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程

5)初始化

初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序,初始化阶段就是执行类构造器方法的过程

②双亲委派模型

提到类加载机制,不得不提的一个概念就是"双亲委派模型"

站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一些,自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派得类加载架构器

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即 $JAVA_HOME/lib 目录。扩展器加载器。加载 lib / ext 目录下的类

应用程序类加载器:加载我们写的应用程序

自定义类加载器:根据自己的需求定制类加载器

双亲委派模型的优点

1.避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了

2.安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了

③破坏双亲委派模型

双亲委派模型虽然有其优点,但在某些情况下也存在一定的问题,比如 Java 中 SPI(Service Provider Interface,服务提供接口)机制中的 JDBC 实现

小知识:SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启动框架扩展和替换组件,SPI 的作用就是为这些被扩展的 API 寻找服务实现

JDBC 的 Driver 接口定义在 JDK 中,其实现由各个数据库的服务器来提供,比如 MySQL 驱动包,我们先来看下 JDBC 的核心使用代码:

java 复制代码
public class JdbcTest {
    public static void main(String[] args){
        Connection connection = null;
        try {
            connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/your_database", "username", "password");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        System.out.println(connection.getClass().getClassLoader());
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(Connection.class.getClassLoader());
    }
}

然后我们进入 DriverManager 的源码类就会发现它是存在系统的 rt.jar 中的,如下图所示:

由双亲委派模型的加载流程可知 rt.jar 是有顶级父类 Bootstrap ClassLoader 加载的,如下图所示:

而当我们进入它的 getConnection 源码是却发现,它在调用具体的类实现时,使用的是子类加载器(线程上下文加载器 Thread.currentThread().getContextClassLoader)来加载具体的数据库数据库包(如 MySQL 的 jar 包),源码如下:

java 复制代码
@CallerSensitive
public static Connection getConnection(String url,
    java.util.Properties info) throws SQLException {
    return (getConnection(url, info, Reflection.getCallerClass()));
}

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            //获取线程上下为类加载器
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }
    println("DriverManager.getConnection(\"" + url + "\")");
    SQLException reason = null;

    for(DriverInfo aDriver : registeredDrivers) {
        // isDriverAllowed 对于 mysql 连接 jar 进行加载
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName())
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }
    }
    if (reason != null)     {
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}

这样一来就破坏了双亲委派模型,因为 DriverManager 位于 rt.jar 包,由 BootStrap 类加载器加载,而其 Driver 接口的实现类是位于服务商提供的 Jar 包中,是由子类加载器(线程上下文加载器 Thread.currentThread().getContextClassLoader)来加载的,这样就破坏了双亲委派模型了(双亲委派模型讲的是所有类都应该交给父类来加载,但 JDBC 显然并不能这样实现),它的交互流程图如下所示:

相关推荐
xiaoqiMikko1 小时前
Spring Cloud Config 这个 CVE 的补丁,Maven Central 上根本没有
java·spring boot
残月心殇 请珍惜枸1 小时前
.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
java·jvm·.net
用户3126874877201 小时前
Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解
java·spring boot
Bonnie_12151 小时前
08-JUC并发基础-AQS-补充共享锁
java·开发语言
添添1 小时前
我用这3个Web Performance API,把首屏加载从3.2秒干到了0.8秒
面试
Zane19941 小时前
线程池入门:7 大核心参数与 4 种拒绝策略
java·后端
Zldaisy3d1 小时前
连续纤维增材制造的机翼已飞上天,复材打印在低空飞行器上还需翻过几道坎?
java·前端·数据库
CoderYanger1 小时前
Java EE:9.JVM(课件内容-下篇)
java·jvm·程序人生·面试·职场和发展·java-ee·学习方法
我命由我123452 小时前
Kotlin 面向对象 - Kotlin 类变量与类方法
java·服务器·后端·java-ee·kotlin·android jetpack·android runtime