当前位置: 首页 > news >正文

java基础(六)jvm

1. JVM内存的五大核心区域 + 一个帮手

想象JVM运行程序时,需要划分不同区域干不同的事。主要分为这五大块,外加一个特殊帮手:

1.1 程序计数器 (Program Counter Register) - 你的“任务进度条”

  • 干啥的: 专门记录当前线程执行代码执行到哪一行了!就像你看书时用书签记住看到哪一页。

  • 特点:

    • 每个线程独一份,互不干扰(线程私有)。

    • 是唯一绝对不会发生内存溢出(OutOfMemoryError)的区域。

    • 执行Java方法时,记录下一条指令地址;执行本地方法(如C/C++写的)时,值为空(undefined)。

  • 为啥必须私有? 想象多个工人(线程)共用一台机器(CPU),CPU轮流给每人干活。工人A干到一半被叫停时,计数器帮A记住干到哪步了;等轮回来,A才能接着干。每个人的进度都不同,当然需要自己的“小本本”!

1.2 虚拟机栈 (Java Virtual Machine Stacks) - 你的“方法工作间”

  • 干啥的: 每个线程执行方法时,都需要一个临时空间,存放这个方法自己的东西:局部变量、方法参数、计算的中间结果、方法结束后该回哪去。

  • 怎么工作: 每调用一个方法,就在栈里压入一个“工作台”(叫栈帧)。方法执行完,就把这个工作台弹出销毁。

  • 特点:

    • 每个线程独有一个栈。

    • 局部变量存在这里(基本类型如int num=10;10,或者对象的引用地址)。

    • 可能出问题:

      • StackOverflowError:方法调用太深(比如无限递归自己),工作间堆满了。

      • OutOfMemoryError如果JVM允许动态扩展栈但内存不足,或者创建线程时申请栈内存失败,就会发生(相对少见,但可能)。

1.3 本地方法栈 (Native Method Stacks) - “外来帮手”的工作间

  • 干啥的: 和虚拟机栈类似,但是给JVM调用的本地方法(Native方法,比如用C/C++写的)用的。

  • 特点: 在常用的HotSpot虚拟机里,它和虚拟机栈是合在一起的。同样可能发生StackOverflowErrorOutOfMemoryError

1.4 Java堆 (Java Heap) - 超级“对象大仓库”

  • 干啥的: 存放所有你new出来的对象实例和数组! 这是JVM内存中最大最重要的一块区域。

  • 特点:

    • 所有线程共享! 大家都从这里取对象。

    • 程序一启动就创建。

    • 垃圾回收(GC)的主战场

    • 空间不够且无法扩展时,会OutOfMemoryError: Java heap space

  • 内部结构 (重点!): 为了更好地管理对象和垃圾回收,堆又细分成几块:

    • 新生代 (Young Generation): 存放刚出生、活不久的对象。分三小格:

      • Eden区 (伊甸园): 新对象出生地!new出来的对象绝大部分先放这里。空间较小

      • Survivor区 (幸存者区): 两个大小相等的区域(通常叫FromTo,或者S0S1)。经历一次垃圾回收(Minor GC)还活着的对象,会从Eden移到其中一个Survivor区。两个区来回倒腾,活过几次GC的“老油条”才能晋升。

    • 老年代 (Old Generation / Tenured Generation): 存放活得久的对象。从新生代熬过几次GC(默认15次)晋升上来的对象,或者特别大的对象(避免在新生代频繁挪动),都住这里。空间较大

    • 大对象去哪? 比如一个超级大的数组(需要连续大块内存)。它们通常直接进老年代!为啥?

      • 新生代空间小,放大对象容易塞满,频繁触发GC,性能差。

      • 大对象在新生代频繁挪动容易产生碎片(小空隙),后面可能没地方放新的大对象。

      • 老年代空间大,放大对象更合适。(注意:具体策略可能因JVM实现和参数略有不同)

    • TLAB (Thread-Local Allocation Buffer): 堆是共享的,但频繁new对象时抢堆锁影响效率。JVM给每个线程在Eden区划一小块私有空间(TLAB),线程优先在这里分配对象,提升效率,分配满了才去锁堆申请新TLAB。

1.5 方法区 (Method Area) - “图书馆”+“档案室” (JDK8+ 叫 元空间 Metaspace)

  • 干啥的: 存储已被JVM加载的类信息(类名、父类、方法签名、字段描述)、运行时常量池静态变量(static)即时编译器(JIT)编译好的机器码

  • 特点:

    • 逻辑上是堆的一部分,但有个别名“非堆”(Non-Heap)。

    • JDK8后重大变化:永久代(PermGen)被移除,改为元空间(Metaspace),使用操作系统的本地内存(不再占用堆空间),解决了老版本永久代容易内存溢出的问题。

    • 内存不足时也会OutOfMemoryError: Metaspace

  • 运行时常量池 (Runtime Constant Pool): 是方法区的一部分。存放类文件里写好的常量(如字符串字面量"abc"、数字100、符号引用),并且运行时也能往里面加新常量(比如String.intern()方法)。

1.6 直接内存 (Direct Memory) - “外部仓库”

  • 干啥的: 不属于JVM规范定义的内存区域! 通过NIO库(ByteBuffer.allocateDirect())申请使用的操作系统本地内存

  • 优点: 让数据少在Java堆和操作系统内存之间来回拷贝(零拷贝),显著提升IO性能(文件读写、网络通信快很多)。

  • 缺点: 虽然快,但总量受你电脑总内存限制,申请太多或忘记释放(依赖Cleaner机制和PhantomReference通知)也会导致OutOfMemoryError: Direct buffer memory

2. 堆(Heap) vs 栈(Stack) 大不同!

特点堆 (Heap)栈 (Stack)
用途存对象实例 (new出来的东西) 和数组存局部变量、方法参数、操作数栈、返回地址等
生命周期对象由GC决定何时回收 (不确定)方法结束,栈帧销毁,局部变量立即消失 (确定)
速度相对较慢 (分配、回收、GC有开销)非常快 (操作简单,内存分配只是移动指针)
空间很大,可动态扩展 (-Xmx)较小,固定大小 (-Xss)
线程所有线程共享每个线程私有
存啥?对象本身基本类型的值对象的引用地址(指针)

关键点:栈里存对象本身吗? 不是!栈里存的是基本类型值(如int a = 10;里的10)和对象的引用地址(理解成指向堆里那个真实对象的门牌号)。比如 MyObject obj = new MyObject();

  • new MyObject()里创建了一个真实对象。

  • obj 这个变量存在的当前方法栈帧里,它的值就是堆里那个对象的门牌号(引用地址)

3. 方法区里有什么宝贝?

  • 类元数据(类名、父类、方法签名、字段描述、常量池等)

  • 运行时常量池(各种字面常量、符号引用 -> 解析后的直接引用)

  • 静态变量(static修饰的变量)

  • 即时编译器(JIT)编译好的本地机器码(优化后执行更快)

  • (在永久代时代) 类的方法字节码 (JDK8+的元空间通常不存字节码本身)

4. String字符串住哪里?有啥讲究?

  • String对象本身也是对象,所以实例里。

  • JVM搞了个字符串常量池(String Pool)来优化字符串:

    • 在哪? JDK7及之后移到堆里(之前放在方法区的永久代)。

    • 作用: 对于字面量方式创建字符串(String s1 = "abc";),JVM会先去常量池找有没有"abc"

      • 如果有,直接把池里的引用给你(不创建新堆对象)。

      • 如果没有,就在堆里的字符串常量池区域创建一个"abc"对象,再把引用给你,并记录到池里。

      • 这样相同的字符串字面量只存一份,省内存。

    • intern()方法: 可以将一个字符串对象动态地放入常量池(如果池中没有),并返回池中对象的引用。

  • 经典面试题:String s = new String("abc"); 创建了几个对象?

    1. "abc"是个字面量。JVM先去字符串常量池找:

      • 如果池里没有:在堆里的常量池区域创建一个"abc"对象 (1个)。

      • 无论池里有没有"abc"new String(...)都会在堆里(常量池区域之外)创建一个全新的String对象 (另1个),这个新对象的内容(char[])指向常量池里的"abc"char[](或拷贝一份,取决于JVM实现)。

    2. 变量s(存的是堆里新对象的地址)存在里。

    • 结论: 总是创建至少1个堆对象(new出来的那个)。如果常量池原本没有"abc",则总共创建2个对象(1个在池里,1个是new出来的);如果常量池已有"abc",则总共创建1个对象(只有new出来的那个)。

5. 引用类型有哪几种?强弱分明!

Java里有4种引用强度,决定了对象被GC回收的优先级:

5.1 强引用 (Strong Reference) - “铁哥们”

  • 最常见!

Object obj = new Object(); 这种就是。
  • 特点: 只要强引用还在(并且可达),GC绝不会回收这个对象。它是对象存活的最强保证obj = null;后,对象就变不可达(如果没其他引用),可被回收。

5.2 软引用 (Soft Reference) - “好朋友”

  • 代表: java.lang.ref.SoftReference

  • 特点: 描述一些还有用但非必需的对象(比如缓存)。只有在系统内存快不够用,要发生OOM之前,GC才会回收它们。适合做缓存(内存不足时自动清理)。SoftReference.get() 可能返回 null

5.3 弱引用 (Weak Reference) - “点头之交”

  • 代表: java.lang.ref.WeakReference

  • 特点: 强度比软引用更弱。下一次GC发生时(无论Minor还是Full GC),不管当前内存是否充足,都会回收只被弱引用关联的对象。适合做缓存避免内存泄漏(比如WeakHashMap的key)。WeakReference.get() 可能在GC后返回 null

  • 举个栗子:弱引用缓存 (优化版,强调检查null)

    import java.lang.ref.WeakReference;
    import java.util.HashMap;
    import java.util.Map;
    ​
    public class YA33CacheExample {
    ​private Map<String, WeakReference<MyHeavyObject>> cache = new HashMap<>();
    ​public MyHeavyObject get(String key) {// 1. 从缓存拿弱引用WeakReference<MyHeavyObject> ref = cache.get(key);// 2. 关键!弱引用可能还在Map里,但对象可能已被GC回收,所以要用get()取对象MyHeavyObject obj = (ref != null) ? ref.get() : null;
    ​// 3. 如果对象是null(被回收了或第一次访问)if (obj == null) {obj = createExpensiveObject(key); // 创建代价高的大对象cache.put(key, new WeakReference<>(obj)); // 用弱引用包装后放入缓存}return obj;}
    ​private MyHeavyObject createExpensiveObject(String key) {// 模拟创建一个大对象或耗时操作return new MyHeavyObject();}
    ​private static class MyHeavyObject {private byte[] largeData = new byte[1024 * 1024 * 10]; // 10MB数据}
    }
    • WeakReference包装大对象放入缓存。

    • 当内存吃紧时,GC会自动回收这些只被弱引用指向的大对象。

    • 关键点: 下次get一定要检查ref.get()是否为null! 如果为null,说明对象已被GC回收,需要重新创建并缓存。

5.4 虚引用 (Phantom Reference) - “影子朋友”

  • 代表: java.lang.ref.PhantomReference (必须配合ReferenceQueue使用)

  • 特点: 最弱的引用。一个对象是否有虚引用,完全不影响它啥时被回收。你无法通过虚引用拿到对象本身(get()总是返回null)。

  • 唯一作用: 对象被GC回收时,你能收到一个通知(通过ReferenceQueue)。常用于精细化管理堆外内存(比如NIO的DirectByteBuffer),确保在回收Java对象时同步释放关联的本地内存,防止内存泄漏。PhantomReference 入队发生在对象被完全回收(finalized)之后

6. 内存泄漏 vs 内存溢出 (OOM)

6.1 内存泄漏 (Memory Leak) - “垃圾占着地方不走”

  • 定义: 程序中的对象已经不再使用了,但因为某些原因(比如还被意外的引用着)无法被垃圾回收(GC)回收。这些“垃圾”占着内存不释放,导致可用内存越来越少

  • 后果: 长期累积,最终可能触发内存溢出(OOM),程序崩溃。

  • 常见原因:

    • 静态集合滥用:staticHashMap, ArrayList等长期持有对象引用不放(尤其是集合只增不减时)。

    • 未关闭资源: 数据库连接(Connection)、文件流(InputStream/OutputStream)、网络连接(Socket)、SwingGraphics对象等打开后忘记关闭(或close()没执行到)。

    • 监听器未注销: 注册了事件监听器,对象不用了却没取消注册,导致被事件源长期引用。

    • ThreadLocal使用不当: 用完没调用remove()清理,尤其在线程池(线程复用)场景,会导致线程的ThreadLocalMap中该Entry的value无法释放(虽然key是弱引用会回收,但value是强引用)。

    • 内部类持有外部类: 非静态内部类实例会隐式持有外部类实例的强引用。如果外部类实例本应回收,但内部类实例还被其他地方引用着,就会导致外部类泄漏。

    • 缓存未清理: 使用缓存(如Map)时,对象不再使用但未从缓存中移除。

6.2 内存溢出 (OutOfMemoryError, OOM) - “地方实在不够用了”

  • 定义: JVM在申请内存时,没有足够的空间来创建新对象了,并且经过垃圾回收后仍然无法获得足够空间,JVM就会抛出OutOfMemoryError错误。

  • 常见类型 & 原因:

    • java.lang.OutOfMemoryError: Java heap space堆内存真不够了。原因:创建太多对象、严重内存泄漏、配置堆太小(-Xmx设小了)、加载超大文件到内存。

    • java.lang.OutOfMemoryError: Metaspace元空间(存类信息)不够了。原因:加载了海量类(大量反射、动态代理、CGLib/ASM生成类、OSGi应用)。

    • java.lang.OutOfMemoryError: Direct buffer memory直接内存不够了。原因:NIO操作申请太多DirectByteBuffer没释放(或GC没触发Cleaner)或-XX:MaxDirectMemorySize设小了。

    • java.lang.OutOfMemoryError: unable to create new native thread:创建线程需要的栈空间不够了。原因:线程创建太多(-Xss设太大或系统限制)、进程地址空间耗尽(32位系统)。

    • java.lang.OutOfMemoryError: Requested array size exceeds VM limit:尝试分配一个超过堆限制的数组(如Integer.MAX_VALUE)。

    • java.lang.StackOverflowError:通常是虚拟机栈或本地方法栈溢出。原因:方法调用太深(无限递归最常见)、栈帧过大(局部变量太多或太大)。(严格来说属于Error,但常与OOM一起讨论)

  • 关系: 长期严重的内存泄漏必然导致内存溢出(OOM)。但OOM也可能是瞬间需要的内存确实超过了JVM配置的最大值(如加载一个超大文件),不一定是泄漏。StackOverflowError通常不是内存泄漏引起。

7. 类加载过程 - 从.class文件到可用类

类需要被加载到JVM才能使用。这个过程分几步走:

加载 (Loading):

找到.class文件(文件系统、网络、JAR包等),读入二进制数据,在方法区创建代表这个类的java.lang.Class对象(作为访问入口)。

  1. 链接 (Linking): 分三步:

    • 验证 (Verification): 检查.class文件格式对不对(魔数、版本),字节码合不合法(指令、跳转),符号引用是否有效,是否符合语言安全约束。防止有害字节码。

    • 准备 (Preparation):的静态变量(static)分配内存(在方法区),并设置默认初始值(如int是0,booleanfalse,对象引用是null)。注意:final static常量(编译期常量)这里就赋值了。

    • 解析 (Resolution): 把类、方法、字段的符号引用(常量池中的名字)变成具体的直接引用(内存地址或偏移量)。解析可能在初始化前发生,也可能延迟到首次使用时(如invokedynamic)。

  2. 初始化 (Initialization): 执行类的构造器<clinit>()方法(编译器自动收集所有static变量赋值和static{}块生成)。给静态变量赋程序员写的初始值父类先初始化。初始化是类加载的最后一步,也是真正执行类中Java代码的开始。

8. 双亲委派模型 - “孩子有事先找爹”

类加载器有层级关系(像家族):

  1. 启动类加载器 (Bootstrap ClassLoader): 顶级大佬(通常C++实现),加载JAVA_HOME/lib目录下的核心库(如rt.jar, charsets.jar)。

  2. 扩展类加载器 (Extension ClassLoader): 继承自java.lang.ClassLoader(Java实现),加载JAVA_HOME/lib/ext目录或java.ext.dirs系统变量指定目录的jar包。

  3. 应用程序类加载器 (Application ClassLoader / System ClassLoader): 继承自java.lang.ClassLoader(Java实现),加载用户程序ClassPath-cp-classpath指定)下的类。我们平时默认用的就是它

  4. 自定义类加载器 (Custom ClassLoader): 用户自己写的加载器,继承java.lang.ClassLoader。可定制加载来源(网络、加密文件等)。

  5. tomcat类加载器:tomcat自定义的加载器

双亲委派原则:

  • 当一个类加载器收到加载请求时,先不自己加载,而是递归地委托给父加载器去尝试加载。

  • 只有当所有父加载器都表示找不到这个类时(在自己的加载路径下找不到),子加载器才会自己尝试加载

好处:

  • 安全: 防止用户写个java.lang.Objectjava.lang.String类覆盖核心库的(父加载器会先加载到核心的)。

  • 避免重复: 保证同一个类(由同一个类加载器加载)只被加载一次。

破坏双亲委派:

  • 历史原因: JDK1.2引入双亲委派之前的老代码(如ClassLoaderfindClass()方法)。

  • SPI (Service Provider Interface): 如JDBC驱动。核心接口在rt.jar(由Bootstrap加载),但实现类(驱动)在ClassPath下。需要线程上下文类加载器(Thread Context ClassLoader)来加载。

  • 热部署/热替换: OSGi、Tomcat等容器需要动态加载、卸载模块,需要自定义类加载器并打破委派。

  • 隔离: 同一个应用加载不同版本库(如不同Web应用加载不同Spring版本),需要各自独立的类加载器。

9. 垃圾回收 (GC) - JVM的自动清洁工

9.1 什么是GC?

GC就是JVM自动回收不再使用(不可达)的对象所占用的内存,避免内存泄漏和手动管理的麻烦。

9.2 怎么判断对象是垃圾?

主流用 可达性分析算法 (Reachability Analysis)

  • 从一组 “GC Roots” 对象(根对象集合)出发。GC Roots包括:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。

    • 方法区中类静态属性(static)引用的对象。

    • 方法区中常量(final static)引用的对象。

    • 本地方法栈中JNI(即native方法)引用的对象(全局引用)。

    • Java虚拟机内部的引用(如基本类型对应的Class对象,常驻异常对象NullPointerException等)。

    • 所有被同步锁(synchronized)持有的对象。

  • 看哪些对象能被这些根对象直接或间接引用到(形成引用链)。

  • 如果一个对象到GC Roots没有任何引用链相连,它就是不可达的,是垃圾,可以被回收。

9.3 垃圾回收算法 - 清洁工的打扫方式

  • 标记-清除 (Mark-Sweep):

    • 步骤:先标记垃圾 -> 直接清除。

    • 缺点: 效率不高(扫描+清除),产生内存碎片(不连续空间)。

  • 复制 (Copying):

    • 步骤:把内存分两块(A/B),只用A。A满了,把A里活着的对象复制到B,清空A。下次用B。

    • 优点: 无碎片,分配快(顺序分配)。

    • 缺点: 浪费一半内存。存活对象多时效率低。

    • 适用: 新生代(存活对象少)。

  • 标记-整理 (Mark-Compact):

    • 步骤:先标记垃圾 -> 让所有活着的对象向一端“搬家” -> 清理边界外空间。

    • 优点: 无碎片,内存利用率高。

    • 缺点: “搬家”开销大。

    • 适用: 老年代

  • 分代收集 (Generational Collection): 最常用策略!

    • 核心思想:根据对象存活周期不同,将堆划分为新生代(Young)和老年代(Old)。

    • 新生代: 对象“朝生夕死”,回收频繁。采用复制算法(Eden + 2x Survivor)。

    • 老年代: 对象存活时间长。采用标记-清除标记-整理算法。

    • 对象晋升: 新生代对象熬过一定次数(默认15)GC后,晋升到老年代。大对象也可能直接进老年代。

9.4 垃圾回收器 - 不同的清洁工团队 (简要概述主流)

  • Serial / Serial Old: 单线程,简单高效(无并发开销),适合Client模式或小内存。

  • ParNew: Serial的多线程版(新生代并行),需配合CMS使用。

  • Parallel Scavenge / Parallel Old: JDK8默认组合。关注吞吐量(Throughput = 用户代码时间 / (用户代码时间 + GC时间))。适合后台计算。

  • CMS (Concurrent Mark-Sweep): (JDK14废弃) 老年代收集器,目标是减少停顿时间。采用“标记-清除”,过程复杂(初始标记->并发标记->重新标记->并发清除)。有碎片问题,并发阶段占用CPU。

  • G1 (Garbage-First): JDK9+默认! 将堆分成多个大小相等的Region(物理不连续),可预测停顿时间模型。采用“标记-整理”算法。特点:整体看是“标记-整理”,局部(Survivor->Survivor/Survivor->Old)看是“复制”。兼顾吞吐量和低延迟,适合大堆。无碎片。

  • ZGC: JDK15+生产可用。追求超低停顿(<10ms),适合超大堆(TB级)。采用染色指针、读屏障等技术。不分代(逻辑分代)。

  • Shenandoah: OpenJDK提供,类似ZGC,低停顿目标。采用Brooks指针、读/写屏障。不分代(逻辑分代)。

9.5 Minor GC / Major GC / Full GC - 清洁范围不同

类型打扫范围触发条件特点常用算法 (分代)
Minor GC (Young GC)只扫新生代 (Eden + Survivor)Eden区满了频繁,快,停顿短复制 (Eden -> S, S间复制)
Major GC (Old GC)主要扫老年代 (通常伴随一次Minor GC)老年代满了 / 根据策略(如CMS阈值) / 对象晋升太快失败较少,较慢,停顿较长标记-清除 / 标记-整理
Full GC扫整个堆 + 元空间 (可能包含永久代/压缩堆)1. 老年代满且Major GC搞不定 2. 元空间满 3. System.gc()调用 (建议-XX:+DisableExplicitGC禁用) 4. 堆空间担保失败 5. CMS并发失败 6. JVM自身策略(如HeapDumpOnOutOfMemoryError前)最慢!停顿最长!尽量避免!取决于老年代/整堆收集器设置

9.6 Stop-The-World (STW) - “世界暂停”/

  • GC进行某些关键步骤时(比如枚举GC Roots、对象标记阶段的部分过程、对象移动),为了保证结果的准确性(对象引用关系不变)和安全性,必须暂停所有应用线程,就像全世界都停止了。

  • 这是GC产生停顿的主要原因。优化GC的核心目标之一就是减少STW的时间和频率。像CMS、G1、ZGC、Shenandoah都在努力通过并发(应用线程和GC线程同时运行)来减少STW。

10. GC不止扫堆!

JVM的垃圾回收器主要清理堆里的对象垃圾,但也会清理方法区(元空间)!它会清理不再使用的类信息(类卸载)、废弃的常量等。类卸载条件苛刻(类ClassLoader被回收、类所有实例被回收、类Class对象没被引用)。元空间的垃圾回收通常由Full GC触发。

http://www.lryc.cn/news/615506.html

相关文章:

  • 微信小程序中实现表单自动填充功能的方法
  • Linux网络子系统架构分析
  • P1025 [NOIP 2001 提高组] 数的划分 题解
  • 基于麦克风阵列电机噪声振动监测解决方案技术解析
  • “自动报社保 + 查询导出 ” 的完整架构图和 Playwright C# 项目初始化模板
  • BroadcastChannel:轻松实现前端跨页面通信
  • 06-docker容器常用命令
  • 全栈:JDBC驱动版本和SQLserver版本是否有关系?怎么选择JDBC的版本号?
  • 自然语言交互与数据库智能客户端比对
  • SpringBoot配置生效优先级
  • 机器学习第七课之支持向量机SVM
  • Java Callback 实现线程切换以及与Kotlin原理关系
  • 数码管的使用(STC8)
  • Pytest中实现自动生成测试用例脚本代码
  • Java Stream 使用 Fork/Join框架的分治任务模型
  • Windows 安装 Xinference 速记
  • CPU缓存(CPU Cache)和TLB(Translation Lookaside Buffer)缓存现代计算机体系结构中用于提高性能的关键技术
  • 【线性代数】线性方程组与矩阵——(2)矩阵与线性方程组的解
  • 计算机网络:深入了解CIDR地址块如何利用VLSM进行子网划分的过程
  • 前端视角下关于 WebSocket 的简单理解
  • 如何在 Ubuntu 24.04 LTS Linux 上安装 Azure Data Studio
  • 【排序算法】④堆排序
  • 基于STM32H5的非循环GPDMA链表使用
  • LangChain-Unstructured 基础使用:PDF 与 Markdown 处理解析
  • 基于IPD体系的研发项目范围管理
  • 【网络与爬虫 52】Scrapyd-k8s集群化爬虫部署:Kubernetes原生分布式爬虫管理平台实战指南
  • 一个app项目周期是多久?
  • Java异常:认识异常、异常的作用、自定义异常
  • 世界时(Universal Time, UT)的定义与详解
  • 小学数学训练闭环:出题、作答、批改一体化方案实践