【重难点】【JVM 01】OOM 出现的原因、方法区、类加载机制、JVM 中的对象

文章目录

  • 【重难点】【JVM 01】OOM 出现的原因、方法区、类加载机制、JVM 中的对象
  • 一、OOM 出现的原因
    • 1.程序计数器
    • 2.虚拟机栈
    • 3.本地方法栈
    • 4.堆
    • 5.方法区
    • 6.直接内存
  • 二、方法区
    • 1.方法区(Method Area)
    • 2.JDK1.7 和 JDK1.8 方法区的变化
  • 三、类加载机制
    • 1.介绍
    • 2.类加载器
    • 3.类加载机制
    • 4.类加载过程
  • 四、JVM 中的对象
    • 1.对象的内存布局
    • 2.对象的访问定位
    • 3.对象的创建过程

一、OOM 出现的原因

1.程序计数器

唯一没有规定 OOM 情况的区域

2.虚拟机栈

  • StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
  • OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存

3.本地方法栈

也会出现 StackOverflowError 和 OutOfMemoryError

4.堆

堆中没有内存完成实例分配,并且堆也无法再扩展。一般来说,堆无法分配对象时会进行一次 GC,如果 GC 后仍然无法分配对象,才会报错

5.方法区

JDK 1.8之前,方法区占用内存到达最大值,且无法再申请到内存时。比如,我们使用 cglib 字节码生成框架不断生成新的类,最终使方法区内存占满

6.直接内存

直接内存出现 OOM 的原因是对该区域进行内存分配时,其内存与其他内存加起来超过最大物理内存限制(包括物理的和操作系统级别的限制)

二、方法区

1.方法区(Method Area)

方法区,也称为非堆(Non-Heap),其中主要存储加载的字节码、class/mehtod/field 等元数据对象、static 变量、JIT 编译器编译后的代码等数据。此外,方法区还包含了一个特殊的区域 ”运行时常量池“。方法区有一个最大值上限,因此,若方法区占用内存到达最大值,且无法再申请到内存时,便会抛出 OOM 异常。比如,我们使用 chlib 字节码生成框架不断生成新的类,最终使方法区内存占满,除此之外,动态语言、JSP、基于 OSGI(面向 Java 的动态模型系统)的应用都会在方法区额外产生大量的类信息

  • 加载的类字节码
    要使用一个类,首先需要将其字节码加载到 JVM 的内存中。至于类的字节码来源,可以多种多样,如 .class 文件、网络传输或者由 cglib 字节码框架直接生成
  • class/method/field 等元数据对象
    字节码加载之后,JVM 会根据其中的内容,为这个类生成 Class/Method/Field 等对象,它们用于描述一个类,通常在反射中用的比较多。不同于存储在堆中的 Java 实例对象,这两种对象存储在方法区中
  • static-final 常量、static 常量
    对于这两种类型的类成员,JVM 会在方法区为它们创建一份数据,因此同一个类的 static 修饰的类成员只有一份
  • JIT 编译器编译后的代码
    以 HotSpot 虚拟机为例,其在运行时会使用 JIT 即使编译器对热点代码进行优化,优化方式为将字节码编译成机器码。通常情况下,JVM 使用 ”解释执行“ 的方式执行字节码,即 JVM 在读取到一个字节码指令时,会将其按照预先定好的规则执行栈操作,而栈操作会进一步映射为底层的机器操作;通过 JIT 编译后,执行的机器码会直接和底层机器打交道

运行时常量池

类的字节码在加载时会被解析并生成不同的东西存入方法区。类的字节码中不仅包含了类的版本、字段、方法、接口等描述信息,还包含了一个常量池。常量池用于存放在字节码中使用到的所有字面量和符号引用(如字符串字面量),在类加载时,它们进入方法区的运行时常量池存放

运行时常量池是方法区中一个比较特殊的部分,具备动态性,也就是说,除了类加载时将常量池写入其中,Java 程序运行期间也可以向其写入常量

//使用 StringBuilder 在堆上创建字符串 abc,再使用 intern 将其放入运行时常量池
String str = new StringBuilder("abc");
str.intern();
//直接使用字符串字面量 xyz,其被放入运行时常量池
String str2 = "xyz";

2.JDK1.7 和 JDK1.8 方法区的变化



  • JDK 1.7 中字符串常量池(StringTable)为什么从永久代移到堆中?
    永久代的回收效率很低,只有 Full GC 才会触发,而字符串常量池回收效率不高,开发中会有大量字符串被创建,放到堆里能够及时回收内存
  • JDK 1.8 为什么去除永久代?
    第一点,永久代如果存放在 JVM 中,很难确定合适的大小。如果被分配在本地内存,则无需考虑大小问题
    第二点,原来在运行时会生成大量的类,比如使用反射和代理,导致经常需要 Full GC
    第三点,对永久代调优很困难
  • 元数据区从虚拟机转移到了本地内存意味着什么?
    默认情况下,元数据区的大小仅受本地内存的限制,这意味着以后不会因为永久代空间不够而抛出 OOM 异常了。JDK 1.8 以前版本的 class 和 jar 包数据存储在永久代,永久代的大小是固定的,而且项目之间无法共用公有的 class,所以很容易碰到 OOM 异常。改成元数据区后,各个项目会共享同样的 class 内存空间,比如多个项目哦都引用了 apache-common 包,在元数据区只会存储一份 apache-common 的 class,提高了内存的利用率,垃圾回收更有效率

三、类加载机制

1.介绍

类的加载指的是将类的 .class 文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区的数据结构的接口

2.类加载器

类加载器负责加载所有的类,它为所有被载入内存的类生成一个 java.lang.Class 实例对象。一旦一个类被加载到 JVM 中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入 JVM 的类也有一个唯一的标识。在 Java 中,一个类用其全限定类名作为标识。但在 JVM 中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在 pg 的包中有一个名为 Person 的类,被类加载器 ClassLoader 的实例 kl 负责加载,则该 Person 类对应的 Class 对象在 JVM 表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同的、互不兼容的

JVM 预定义的有三种类加载器,当一个 JVM 启动的时候,Java 开始使用如下三种类加载器

  • 根类加载器/启动类加载器(Bootstrap ClassLoader)
    它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。换句话说,它负责加载 $JAVA_HOME 中 jre/lib/rt.jar (包含所有核心 Java 运行环境的已编译 class 文件)里所有的 class,由 C++ 实现,不是 ClassLoader 子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以无法直接通过引用进行操作
  • 扩展类加载器(Extension ClassLoader)
    该加载器由 sum.misc.Launcher$ExtClassLoader 实现,它负责加载 DK/jre/lib/ext 目录或者由 java.ext.dirs 系统属性指定的目录中的 jar 包的类。由 Java 语言实现,父类加载器为 null
  • 系统类加载器/应用程序类加载器(System ClassLoader/Application ClassLoader)
    该类加载器由 sum.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接是用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。由 Java 语言实现,父类加载器为 ExtClassLoader

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为 JVM 自带的 ClassLoader 只是懂得从本地文件系统加载标准的 java class 文件。因此,如果编写了自己的 ClassLoader,便可以做到如下几点:

  1. 在执行非置信代码之前,自动验证数字签名
  2. 动态地创建符合用户特定需要的定制化构建类
  3. 从特定的场所取得 java class,例如数据库中和网络中

3.类加载机制

  • 全盘负责
    所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显式是用另外一个类加载器来载入
  • 双亲委派
    所谓的双亲委派,就是先让父类加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗地讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去完成
  • 缓存机制
    缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要是用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。这就是为什么修改了 Class 后,必须重启 JVM,程序所要做的修改才会生效的原因

我们重点关注一下双亲委派机制


双亲委派机制的优势:
采用双亲委派模式的好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类已经加载了该类时,就没有必要让子类加载器再加载一次。其次是考虑到安全因素,保证 Java 核心 API 中定义类型不会被随意替换。假设通过网络传递一个名为 java.lang.Integer 的类,将会通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,并且该类已经被加载。因此,就不会重新加载网络传递过来的 java.lang.Integer,而是直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意替换

4.类加载过程


类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。另外注意这里的几个阶段时按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段

加载

查找并加载类的二进制数据。加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取其定义的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的过程)是可控性最强的阶段,因为开发人员既可以是用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在 Java 堆中也创建一个 java.lang.Class 类的对象,这样便可以通过该对象访问方法区中的这些数据

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成四个阶段的检验动作:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

验证阶段非常重要,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

准备

为类的静态变量分配内存,并将其初始化为默认值。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些内存都将在方法区中分配。对于该阶段有以下三点需要注意:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在 Java 堆中
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如 0、0L、null、false 等),而不是在 Java 代码中被显式赋予的值
  • 如果类字段的字段属性表中存在 ConstValue 属性,即同时被 static 和 final 修饰,那么在准备阶段变量就会被初始化为 ConstValue 属性所指定的值

假设一个类变量的定义为:public static int value = 3

那么变量 value 在准备阶段过后的初始化值为 0,而不是 3,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 3 的 putstatic 指令是在程序编译后,存放于类构造器 <clinit>() 方法之中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行

假设上面的类变量被定义为:public static final int value = 3

编译时 javac 将会为 value 生成 ConstValue 属性,在准备阶段虚拟机就会根据 ConstValue 的设置将 value 赋值为 3。我们可以理解为 static final 常量在编译前就将其结果放入了调用它的类的常量池

解析

把类中的符号引用转换为直接引用。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行的。符号引用就是一组符号来描述目标可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

初始化

初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 java 程序代码(字节码)

在准备阶段,变量已经依次赋过系统要求的初始值(零值)。而在初始化阶段,根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者更直接地说:初始化阶段是执行类构造器 <clinit>() 方法的过程。<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 static{} 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化实际,只有当类主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  1. 创建类的实例,也就是 new 一个对象
  2. 访问类或接口的静态变量
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个子类时,会先初始化子类的父类
  6. JVM 启动时标明的启动类,即文件名和类名相同的那个类

注意:
对于一个 final 类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于 ”宏变量“,Java 编译器会在编译时直接把这个变量出现的地方替换成它的值。因此,及时程序员使用该静态变量,也不会导致该类的初始化。反之,如果 final 类型的静态变量的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化

四、JVM 中的对象

1.对象的内存布局

以 HotSpot VM 为例,对象在内存中的结构可以分为三部分:对象头(header)、实例数据(instance data)、对齐填充(padding)

对象头

对象头的结构大体相似,但不同 JVM 的具体实现使得它们略有差别。一般来说,对象头都会包含标记字、类型指针两部分信息,如果对象是数组,还会额外包含数组长度信息

  • 标记字

    存储对象自身的运行时数据(即状态),包括哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程 id、偏向时间戳等。它们的存储结构类似于 C 语言中的 :“位字段”,官方称之为 “Mark Word”(标记字)。“标记字” 以 “字” 作为基本的存储单元,即在 32 位虚拟机中,数据长度位 32 bit,而在 64 位虚拟机中,数据长度位 64 bit

    以 32 位虚拟机为例,有固定的 2 bit 用于存储锁标志位,随着锁标志位值不同,其他位存储的内容与位长度也不同。这一点类似于 C 语言中的联合结构(union),且联合的每一个成员都是位字段结构

  • 类型指针

    类型指针即对象指向它的类元数据(class metadata)的指针,虚拟机通过该指针确定这个对象是哪个类的实例。但需要注意的是,并非所有虚拟机实现中都会在对象头包含类型指针,也可以采用其他方式保留对象的类型信息

  • 数组长度

    在 Java 中,数组也属于对象,那么理所当然地需要维护数组长度,该信息放在对象头中

实例数据

示例数据即对象的字段(或称为成员变量)存储地数据信息,包含了从父类继承及自己定义的所有字段。且字段在内存中存储的顺序并不等于类中的定义顺序,它受到虚拟机策略的影响(主要看考虑到内存对齐以及使用率的问题)

对齐填充

类似于 C 结构体 struct 的内存对齐,Java 对象的内存位置也需要对齐。我们常用的 HotSpot VM 要求每个对象的起始地址位 8 字节的整数倍,也就是说,若一个对象结束地址非 8 字节整数倍,则需要占位符进行填充以保证对齐

2.对象的访问定位

虚拟机规定,需要通过栈上的 “reference”(引用)来操作具体对象。对于该规定,目前有两种主流的实现方式

  • 通过句柄(handler)实现:该种方式会在堆中划出一块 “句柄池” 内存空间,每个栈上的引用直接指向句柄池中的句柄,而句柄又会维护对象指针和类型指针。使用句柄带来的好处是:栈上的 reference 存储稳定的句柄地址,GC 造成的对象移动只会导致句柄中相应的指向地址改变,而 reference 地址不改变

  • 通过直接指针实现(direct point)实现:即在对象的对象头中维护类型指针。栈的 reference 指向对象,而对象头中的类型指针指向对象类型数据。使用直接指针的好处是:对象的访问速度快,节省了指针二次寻址的开销

3.对象的创建过程

对象的创建过程要经历以下几个阶段

类加载

  • 检查到 new 指令
  • 虚拟机检查在常量池中是否有该类的符号引用,包括该符号引用代表的类是否已被加载、解析、初始化
  • 没有,则先加载类
  • 有的话,直接创建对象

内存分配

1、内存分配的方式

一个对象所需内存在类加载时便可确定,内存分配方式有两种:

  • 指针碰撞法
    若 java 堆中内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把指针指向空闲空间那边挪动一段与对象大小相等的距离
  • 空闲列表法
    若堆内存不规整,就无法通过简单的移动指针分配内存。这种情况下虚拟机会维护一个列表记录哪些内存可用,分配时查找并更新列表

使用哪种方式取决于内存是否规整,而内存是否规整取决于垃圾收集器的 GC 算法。典型如 Serial、ParNew 这两种收集器,它们在 GC 时带有压缩整理的功能,因此系统会采用 “指针碰撞” 的方式分配内存。而 CMS 这种基于 Mark-Swep(标记-清除)算法的垃圾收集器,则会采用 “空闲列表法” 分配内存

2、内存分配的安全

需要注意的是,若多个线程同时申请分配内存,如果不加以同步控制,则会导致内存分配不对。不同的虚拟机会采用不同的机制避免线程安全问题

  • 同步锁定:通过 CAS 配上失败重试的方式保证更新操作的原子性
    注:CAS,即 CPU 硬件同步原语,全称为 Compare And Swap,若比较不对则失败
  • TLAB:即线程分配缓冲区。在堆中预先为每个线程分配一小块内存,线程在各自分配的内存上进行内存分配来保证安全。只有当 TLAB 用尽并申请新的 TLAB 时,才进行同步锁定

内存初始化

内存初始化指的是将对象分配到的内存所有位置重置为 0(不包括对象头)。若对象通过 TLAB 分配的,该过程会提前至 “内存分配” 执行

对象头初始化

设置对象的对象头信息

对象实例数据初始化

设置对象的实例数据信息,即成员变量值。只有这步完成了,一个真正的对象才产生并能提供给我们使用

【重难点】【JVM 01】OOM 出现的原因、方法区、类加载机制、JVM 中的对象相关推荐

  1. JVM 各种OOM问题与解决方法

    转自:http://zhaohe162.blog.163.com/blog/static/38216797201110232341953/ 1.OOM for Heap=>例如:java.lan ...

  2. 计算机考研408必考重难点整理(2022考纲大改后,陆续更新中。。)

    2022年的408大纲变化很大,让同学们直呼鸭梨山大. 整理复习了一轮之后,心里依旧慌的一匹 距离考试还有75天,所以是时候好好把408的必考重难点整理出一个体系来了. 我们首先来看一下大纲变动 以下 ...

  3. JVM,堆,栈,方法区之间的关系

    什么是JVM? JVM就是Java虚拟机,它包含堆,栈以及方法区. 堆 1. 堆中存放的是 对象实例 // 堆中存放的就是 new Student() Student s = new Student( ...

  4. 小学阅读方法六种_小学语文重难点|阅读理解的解答技巧+方法(合集)

    阅读理解,一直是小学阶段孩子们除写作以外最头疼的部分,很多孩子每次考虑的时候阅读理解都会被扣掉大量的分数.那么孩子们应该如何更好的进行阅读和理解呢?今天我们就来为孩子们分享一些关于小学语文阅读理解的解 ...

  5. JVM的堆、栈、方法区

    堆 解决数据存储的问题 数据怎么存放,存放在哪里 栈 解决程序的运行问题 程序如何执行,如果处理数据 方法区 解决堆栈信息的产生,是先决条件 辅助堆栈的快永久区Perm 比如 创建一个对象User U ...

  6. 【我的Android进阶之旅】你了解adb device unauthorized的原因 和 adb授权机制的中adbkey与adbkey.pub的作用吗?

    一.问题描述 最近在某些第三方的硬件平台上开发一款APP,然后提测给测试人员测试.然后测试人员无法使用adb命令安装APP. 运行adb devices命令 提示 xxx设备 unauthorized ...

  7. JVM学习笔记之-方法区,栈、堆、方法区的交互关系,方法区的理解,设置方法区大小与OOM,方法区的内部结构,方法区使用举例

    栈.堆.方法区的交互关系 运行时数据区结构图 从线程共享与否的角度来看 栈,堆,方法区的交互关系 方法区的理解 方法区在哪里? <Java虚拟机规范>中明确说明:"尽管所有的方法 ...

  8. JavaScript重难点解析6(Promise)

    JavaScript重难点解析6(Promise 概念 为什么要使用Promise Promise 的状态 Promise 对象的值 Promise工作流程 基本用法 Promise其他方法 asyn ...

  9. JVM运行时数据区---方法区(演变和垃圾回收)

    方法区演进细节与垃圾回收 方法区演进细节 永久代演进过程: 首先明确:只有 Hotspot 才有永久代.BEA JRockit.IBMJ9 等来说,是不存在永久代的概念的.原则上如何实现方法区属于虚拟 ...

  10. java方法区超详细汇总,方法区到底是干什么用的?不懂方法区不能说了解jvm!

    目录 一.运行时数据区结构图 二.栈.堆.方法区的交互关系 三.方法区的理解 官方文档 方法区在哪里 方法区的基本理解 HotSpot中方法区的演进 四.设置方法区大小与OOM 设置方法区内存的大小 ...

最新文章

  1. SDN(软件定义网络)
  2. CF388D-Fox and Perfect Sets【dp,线性基】
  3. 计算机 编程 教程 pdf,计算机专业教程-第3章编程接口介绍.pdf
  4. LeetCode MySQL 1454. 活跃用户(连续dense_rank排名函数)
  5. linux资源使用统计指南,指南:工作量分析文档
  6. Error running tomcat8 Address localhost:1099 is already in use 错误解决
  7. Qt5.7+Opencv2.4.9人脸识别(二)人脸采集
  8. C#初学者们,请离代码生成器远点!!!
  9. 在二叉树中有两个结点m和n,若m是n的祖先,则使用后序遍历可以找到从m到n的路径
  10. nvidia ubuntu 驱动升级_解决 Ubuntu 在启动时冻结的问题
  11. dither(抖动)
  12. 【枚举与countDownLatch的应用】
  13. cad lisp 二次抛物线_用Cad画二次抛物线.doc
  14. AWVS扫描Web应用程序
  15. BZOJ4864: [BeiJing 2017 Wc]神秘物质(Splay)
  16. PDF如何插入新的PDF页面
  17. 最全elk7.1.1单机配置: ELK+Filebeat+Kafka!
  18. Java实现简单的宠物商店管理系统
  19. R语言实现主成分分析与典型相关分析
  20. 文章伪原创方法(如何伪原创使文章快速收录)

热门文章

  1. c52单片机控制l298n步进电机角度_【设计图文】单片机实现的步进电机控制系统(开题报告+论文+文献综述+外文翻译+DWG图纸)...
  2. 红帽linux免费吗,红帽宣布面向16个系统以下的小型生产环境免费提供RHEL
  3. java工商银行项目_ChaosBlade 在工商银行混沌工程体系中的应用实践
  4. 滨州智能dcs系统推荐_推荐一:智能变电站监控系统典型作业培训教材
  5. 计算机窗口设计java实验,Java银行取款异常处理计算器设计图形用户界面设计实验报告.doc...
  6. oracle 游标的替代,Oracle中replace函数和translate函数以及简单的游标
  7. Statefulset:部署有状态的多副本应用
  8. shell脚本--批量测试主机连通性ping IP
  9. 关于爬虫数据的解析器设计
  10. 转:C++反汇编揭秘2 – VC编译器的运行时错误检查(RTC)