0%

JVM必备知识点

1. Java 运行时数据区域

Java运行时数据区域可分为:方法区虚拟机栈程序计数器本地方法栈

image-20220407231400224
  • 方法区是所有线程共享的数据区
  • 虚拟机栈程序计数器本地方法栈是线程隔离的数据区

(1)程序计数器

用于记录下一条需要执行的字节码指令的地址

  • 分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响,独立存储,也就是线程私有
  • 程序计数器是内存区域中唯一不会出现溢出问题的区域。,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

(2)Java 虚拟机栈

Java 虚拟机栈是线程私有的,生命周期与线程相同。每个方法被执行时,Java 虚拟机会同步创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 如果线程请求的栈深度大于虚拟机所运行的深度,将抛出 StackOverflowError 异常。
  • 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OurOfMemoryError 异常(HotSpot 虚拟机的栈容量是不可以动态扩展的,只要线程申请栈空间成功了就不会出现 OOM,但是如果申请时就失败,仍然会抛出 OOM)。
  • 垃圾回收不涉及栈内存。
  • 栈内存越大,那么能运行的线程越少(因为物理机的内存是固定的,且栈是线程私有的)

(3)本地方法栈

本地方法栈和虚拟机栈发挥的作用是十分类似的,区别在于虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。有的 Java 虚拟机(比如 HotSpot 虚拟机)直接将本地方法栈和虚拟机栈合二为一。

(4)Java 堆

Java 堆是虚拟机所管理的内存中最大的一块,会被所有线程共享,在虚拟机启动时就被创建,主要作用就是存放对象实例。Java 堆是垃圾收集器管理的内存区域,也可被称为 GC 堆。

  • Java 堆一般是可扩展的,通过参数 -Xmx 和 -Xms 设定占用内存大小。
  • 如果在 Java 堆中没有内存完成实例分配,且堆无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。
  • 堆中对象由于是线程共享的,因此需要考虑线程安全问题。

(5)方法区

方法区也是多个线程共享的内存区域,在虚拟机启动时被创建,用于存储已被虚拟机加载的类型信息、常量静态变量、即时编译器编译后的代码缓存等数据

  • 在 JDK8 之前,HotSpot 虚拟机中使用永久代来实现方法区,容易发生内存溢出,在 JDK8 后使用元空间来代替永久代,元空间并不在堆内存中,而是直接占用的本地内存,因此其最大大小收到本地内存的限制。

    方法区和永久代以及元空间是什么关系呢?

    方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间。

  • 如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

  • 运行时常量池是方法区的一部分。

    • 在 Class 文件中会有常量表,用于存放编译期生成的各种字面量和符号引用,一旦类加载之后,这部分信息就会存放到运行时常量池中(实际不存储符号引用,而是存储由符号引用翻译出来的直接引用)。

    • 相比于 Class 文件常量池,运行时常量池的特点是具有动态性,因为常量不一定只有编译器才能产生,运行时也看产生新的常量。比如使用 String 类的 intern 方法生成常量,此方法是一个本地方法,在 JDK7 及以后的作用为:

      • 如果字符串常量池中已经包含一个等于此字符串对象的字符串,则返回池中这个字符串的 String 对象的引用(注意不是原字符串的引用,而是池中的);
      • 如果不存在,则将此 String 对象的引用添加到常量池中,并返回此 String 对象的引用。
    • 注意:从 JDK7 起,原本存放在永久代的字符串常量池被移至 Java 堆中

    • 通过加入 -XX:+PrintStringTableStatistics 可以大于串池的统计信息。

2. Java 创建一个对象的过程

  • 类加载检查:当 Java 虚拟机遇到一条字节码 new 指令时,首先检查这个指令的参数能否在常量池中定位到一个类的字符引用,并且检查这个符号引用的类是否已被加载、解析和初始化过,如果没有,则先执行相应的类加载过程。

  • 分配内存:在类加载检查之后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

    • 指针碰撞
      • 适用场合 :堆内存规整(即没有内存碎片)的情况下。
      • 原理 :被使用过的内存全部整合到一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
      • 使用该分配方式的 GC 收集器:Serial, ParNew(带压缩整理过程)
    • 空闲列表
      • 适用场合 : 堆内存不规整的情况下。
      • 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
      • 使用该分配方式的 GC 收集器:CMS

    在实际开发过程中,创建对象是很频繁的事情,即使仅仅修改一个指针所指向的位置,并发情况下,也不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时是用来原来的指针来分配内存的情况。解决方案:

    • 对分配内存空间的动作进行同步处理——使用 CAS 配上失败重试的方式保证更新操作的原子性。
    • 把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区汇中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用 TLAB,可以通过 -XX: +/-UseTLAB 参数来设定。
  • 初始化零值:内存分配完成之后,虚拟机需要将分配到的内存空间初始化为零值(不包括对象头),这一操作保证了对象的示例字段在 Java 代码中可以不赋值就直接使用,默认初始化为零值。

  • 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  • 执行构造方法:通过构造函数,为对象属性赋值。

3. 对象的内存布局

在 HotSpot 虚拟机中,对象在堆内存中的存储布局分为三部分:对象头实例数据对齐填充

  • 对象头部分有两类信息:
    • 第一类用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、对象持有的锁、偏向线程 ID、偏向时间戳等。官方称此类信息为『Mark Word』。
    • 另一类是类型指针,即对象指向它的类型元数据的指针,通过此指针来确定该对象是哪个类的实例。数组较为特殊,额外存放用于记录数组长度的数据。
    • 对象头在 32 位虚拟机中占用 32 个比特,在 64 位虚拟机中占用 64 个比特。
  • 实例数据部分是对象真正存储的有效信息,即各种类型的字段内容。
  • 对齐填充部分并不是必然存在的,没有特殊含义,只是起到占位符的作用。HotSpot 虚拟机要求对象起始地址必须是 8 字节的整数倍,那么任何对象的大小都必须是 8 字节的整数倍。而对象头部分已被设计为 8 字节的倍数,因此如果示例数据部分没有对齐的话,就需要通过对象填充进行补全。

4. 对象的访问定位

创建对象自然是为了使用此对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的定位、访问方式由虚拟机具体实现而定,目前主流的访问方式有:句柄直接指针

  • 句柄

    如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  • 直接指针

    如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址,如果只是访问对象本身的话,即不需要再多一次间接访问的开销。

    HotSpot 虚拟机使用的是这种方式。

    这两种对象访问方式各有优势。

    • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时(垃圾收集时移动对象是非常普遍的行为)只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
    • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

5. OutOfMemoryError 异常

对于 Java 的运行时数据区域而言,除了程序计数器外,其他几个区域都可能会出现 OutOfMemoryError 异常。

  • Java 堆溢出。

    • 通过 -Xms参数设置堆内存最小值,-Xmx设置堆内存最大值,比如 -Xms20m -Xms20m
    • 如果溢出原因是内存泄漏,则分析如何让垃圾回收器对其进行回收
    • 如果溢出问题不是内存泄漏,也就是内存中的对象必须存活,则需要增加堆参数(-Xms、-Xmx)或者检查代码
  • 虚拟机栈和本地方法栈溢出

    • 通过 -Xss参数设置栈容量,比如 -Xss128k
    • 由于 HotSpot 虚拟机不支持栈的动态扩展(其他的可能支持),使得线程运行时不会因为扩展而出现内存溢出的情况,只会因为栈容量无法容纳新的栈帧而出现 StackOverflowError 异常(栈帧过多 /栈容量太小)。
  • 方法区溢出

    • -XX:MaxMetaspaceSize 用于设置元空间的最大值。默认为-1,即不限制
    • -XX:MetaSpaceSize 用于指定元空间的初始空间大小。
    • -XX:minMetaspaceFreeRation 用于在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。

6. JVM 中的常量池

JVM常量池主要分为 Class 文件常量池、运行时常量池,字符串常量池,以及基本类型包装类对象常量池

  • Class文件常量池。class文件是一组以字节为单位的二进制数据流,在 Java 代码的编译期间,我们编写的 Java文件就被编译为 .class 文件格式的二进制数据存放在磁盘中,其中就包括 class 文件常量池。
  • 运行时常量池:运行时常量池相对于 class 常量池一大特征就是具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自 class 常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是 String.intern()。
  • 字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在 HotSpot VM 中,它是一个叫做 StringTable 的全局表。在字符串常量池中维护的是字符串实例的引用,底层 C++ 实现就是一个 Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。 
  • 基本类型包装类对象常量池:Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这 5 种整型的包装类也只是在对应值小于等于 127 时才可使用对象池,也即对象不负责创建和管理大于 127 的这些类的对象。

7. 如何判断对象是否死亡

判断一个对象是否存活,分为两种算法:引用计数法和可达性分析算法;

  • 引用计数算法

    在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值减一;任何时刻计数器为零的对象就是不可能再被使用的。

    优点在于原理简单,判定效率高;

    缺点在于占用了额外的内存空间并且算法虽然看起来简单,但是却有很多例外情况要考虑,必须配合大量额外处理才能保证正确的工作,比如单纯的引用计数就很难解决对象之间的相互循环引用问题,因此一般 Java 虚拟机均不采用这种算法!

    在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
    */
    public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
    ReferenceCountingGc objA = new ReferenceCountingGc();
    ReferenceCountingGc objB = new ReferenceCountingGc();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;

    }
    }
  • 可达性分析算法

    通过一系列称为『GC Roots』的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。(根对象可以简单理解为肯定不能被垃圾回收的对象)

    在 Java 中可以作为 GC Roots 的对象有以下几种:

    • 虚拟机栈中引用的对象,比如当前正在运行的方法使用到的局部变量、参数等
    • 方法区中类静态属性引用的变量,比如 Java 类的引用类型静态变量
    • 方法区常量池引用的对象,比如字符串常量池里的引用
    • 本地方法栈 JNI (Java Native Interface)引用的对象
    • Java 虚拟机内部的引用,如基本类型数据对应的 Class 对象,一些常驻的异常对象(比如 NullPointException、OutOfMemoryException)等,还有系统类加载器
    • 被同步锁(synchronized 关键字)持有的对象
    • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

    注意:当一个对象被可达性分析算法判定为不可达时,也不一是非死不可的,要真正宣告一个对象死亡,最多会经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会第一次标记。随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法

    • 假如对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
    • 如果对象被判定有必要执行 finalize() 方法,那么该对象会被放置到一个名为 F-Queue 的队列之中,并在稍后由一条虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。注意,如果此方法执行缓慢,虚拟机不一定会等待其运行结束(因为其他在队列中的对象一直处于等待,可能导致整个内存子系统的崩溃)。

    在 finalize()方法中,对象可以『拯救』自己不被垃圾回收,只要重新与引用链上的任何一个对象建立关联即可(只能有一次,因为一个对象的 finalize()方法最多只会被系统自动调用一次),那么在第二次标记时就会将它移出“即将回收”的集合,反之该对象就会被回收了。

    finalize() 方法运行代价昂贵,不确定性大,无法保证各个对象的调用顺序,如今官方明确声明为不推荐使用的语法。finalize() 方法能做的,使用 try-finally 或者其他方法都可以做的更好、更及时。

8. Java 中引用的分类

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

Java 分为以下四种引用,引用强度依次降低。

  • 强引用

    最传统的『引用』,指在代码中普遍存在的引用赋值,比如 Object obj = new Object; 只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题

  • 软引用

    用于描述一些还有用,但非必须的对象。只要这种引用的对象,在系统内存不足时,会把这些对象列进回收范围之中进行第二次回收,可使用 SoftReference 类实现软引用。如果内存空间足够,垃圾回收器就不会回收它。软引用可用来实现内存敏感的高速缓存。

  • 弱引用

    用于描述哪些非必须的对象,但强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。可使用 WeakReference 类实现弱引用。

    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

  • 虚引用

    最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为对象设置虚引用关联的唯一目的只是为了在这个对象被垃圾收集器回收时收到一个系统通知。可使用 PhantomReference 类实现虚引用。

9. 回收方法区

方法区上进行垃圾回收性价比不高,主要回收两部分内容:废弃的常量和不再使用的类型。

如何判断一个常量是废弃常量?

假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池了。

如何判断一个类是无用的类?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类”

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

10. 垃圾收集算法【重要】

  • 标记-清除算法

    分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后,统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程。

    缺点:

    • 时间效率不稳定,标记和清除过程的执行效率均随着对象回收数量的增长而降低,如果回收对象很多,那么标记-复制算法的效率将会大大降低。
    • 空间碎片问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够大的连续内存而不得不提前触发下一次垃圾收集动作。
  • 标记-复制算法

    将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。

    解决了标记-清除算法的空间碎片问题以及当存在大量可回收对象执行效率低的问题。

    缺点:

    • 空间浪费,将可用内存缩小为了原来的一半。
    • 时间效率不稳定,复制过程的执行效率均随着对象存活数量的增长而降低,如果存活对象很多,那么标记-复制算法的效率将会大大降低。
  • 标记-整理算法

    标记过程同标记-清除算法一致,但不是直接对可回收对象进行清理,而是让所有存活对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

    标记-整理算法与标记清除算法的本质差异是前者是一种非移动式的回收算法,后者是移动式的。

    • 不移动对象会存在空间碎片的问题。
    • 移动对象会存在 『Stop The Word』的问题,即移动操作必须全程暂停用户应用程序才能进行。
  • 分代收集理论

    在程序运行过程中,存在以下两个分代假说:

    • 弱分代假说:绝大多数对象都是朝生夕灭的。
    • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

    根据这两个分代假说,可以分析出一种设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。大多数朝生夕灭的对象放在一起,以较高频率进行垃圾收集,主要关注哪些需要保留,而少部分难以消亡的对象放在一起,以较低频率进行垃圾收集,主要关注哪些需要回收。从而兼顾垃圾收集的时间开销和内存的空间有效利用。

    在 HotSpot 虚拟机中,将 Java 堆分为新生代和老年代

    • 新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用标记-复制算法来完成收集,新生代中存活的对象会逐步晋升到老年代中存放。新生代中又分为伊甸园区和幸存区,幸存区中又分为 from 区和 to 区。
    • 老年代中因为对象存活率高、没有额外空间对它进行分配担保,使用标记-复制算法效率很低,可使用标记—清除算法(CMS收集器采用)或标记—整理算法(Parallel old收集器采用)来进行回收。

    名词解释:

    Partial GC(部分收集),指仅仅收集部分 Java 堆的垃圾收集,可分为以下几种:

    1. Minor GC(新生代收集):指对新生代的垃圾收集。
    2. Major GC(老年代收集):指对老年代的垃圾收集。(目前只有 CMS 收集器会单独收集老年代)
    3. Mixed GC (混合收集):指对整个新生代和部分老年代的垃圾收集。(目前只有 G1 收集器有混合收集方式)

    Full GC(整堆收集):指对整个 Java 堆和方法区的垃圾收集。

    回收算法类型 优点 缺点
    标记清除算法 不需要移动对象,简单有效 标记、清除过程效率低,产生内存碎片
    标记复制算法 清理速度快,没有内存碎片产生 内存使用率低,有可能产生频繁复制问题
    标记整理算法 简单高效,没有内存碎片产生 仍然需要移动局部对象

11. JVM 分代收集

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收。

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。新生代又划分为 3 个分区:Eden 区、To Survivor、From Survivor,默认占比是 8:1:1。进一步划分的目的是更好地回收内存,或者更快地分配内存。

新生代的垃圾回收(又称 Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

老年代的垃圾回收(又称 Major GC)通常使用“标记-清理”或“标记-整理”算法。

image-20220407232323960

一般转化流程如下:

  • 对象一般优先在 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

    • 在 Eden 区执行了一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
    • Eden 区再次 GC 时,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
    • 每移动一次,对象年龄就会加 1,当对象年龄大于一定阀值会直接移动到老年代。此阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;
    • Minor GC 会一直重复这样的过程,在这个过程中,有可能当某次 Minor GC 后,Survivor 的”From”区域空间不够用,有一些还达不到进入老年代条件的实例会放不下,那么放不下的部分会提前进入老年代。
  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,Full GC 清理整个内存堆 ,包括年轻代和老年代

12. 内存分配与回收策略

  1. 对象优先在 Eden 分配

    大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

  2. 大对象直接进入老年代

    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

  3. 长期存活的对象进入老年代

    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold 用来定义年龄的阈值。

  4. 动态对象年龄判定

    虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

  5. 空间分配担保

    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

    如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

13. Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

  • 调用 System.gc()

    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

  • 老年代空间不足

    老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

    为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  • 空间分配担保失败

    使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

  • JDK 1.7 及以前的永久代空间不足

    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

    当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

    为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

  • Concurrent Mode Failure

    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

14. 空间分配担保原则

如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。

在执行每次 YoungGC 之前,JVM 会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候 JVM 就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 YoungGC,尽快这次 YoungGC 是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。

在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:

  • ① YoungGC 后,存活对象小于 survivor 大小,此时存活对象进入 survivor 区中
  • ② YoungGC 后,存活对象大于 survivor 大小,但是小于老年大可用空间大小,此时直接进入老年代。
  • ③ YoungGC 后,存活对象大于 survivor 大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC 后,老年代还是没有足够的空间,此时就会发生 OOM 内存溢出了。

15. 垃圾收集器

使用命令java -XX:+PrintCommandLineFlags -version 可以查看 jdk 默认使用的垃圾收集器

HotSpot 虚拟机的垃圾收集器:

(1)Serial 收集器

一个单线程工作的收集器,在进行垃圾回收时,必须暂停其他所有工作线程,直到它收集结束,即 『Stop The Word』。Serial收集器对于运行在客户端模式下的虚拟机是一个很好的选择。

image-20220321202156528

(2)ParNew 收集器

可以认为是Serial 收集器的多线程版本,使用多个线程进行垃圾收集,在多核CPU环境下有着比Serial更好的表现,其它方面基本与Serial 一致。在 JDK7遗留的系统 中,它是不少服务器模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。

在单核心处理器的环境中,parNew 由于存在线程开销,不会比 Serial 收集器有更好的效果。

image-20220321202615088

(3)Parallel Scavenge 收集器【JDK 8 默认】

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)

比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。

若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。

(4)Serial Old 收集器

老年代单线程收集器,Serial收集器的老年代版本,采用标记整理算法,主要意义也是供客户端模式下的 HotSpot虚拟机使用。

(5)Parllel Old 收集器【JDK 8 默认】

Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先。使用多线程和标记-整理(Mark-Compact)算法。

(6)CMS 收集器【重要】

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是一种并发收集器,采用的是 Mark-Sweep 算法。

采用的是”标记-清除算法”,整个过程分为4步

  1. 初始标记,标记 GC Roots 能直接关联到的对象,时间很短。所以这里用的是单线程,会导致 Stop the world。
  2. 并发标记,进行可达性分析过程(从直接关联对象开始遍历整个对象图),时间很长,用的是多线程,但是不需要暂停用户线程,而是一起并发运行
  3. 重新标记,修正并发标记期间的变动部分,时间比初始标记长,但远小于并发标记时间,会导致 Stop The World)。
  4. 并发清除,回收内存空间,时间超长,但不需要移动存活对象,所以可以与用户线程并发运行

由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

image-20220321203821956

优点:并发收集、低停顿

缺点:

  • 对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程挺对,但因为占用了一部分线程(或者说 CPU的计算能力),会导致应用程序变慢,吞吐率下降
  • 无法处理浮动垃圾。因为在并发标记和清理阶段,用户线程还在运行,自然就会产生新的垃圾,但这些垃圾出现在标记之后,此次垃圾收集中就无法处理掉它们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。
  • 由于采用的标记 - 清除算法,会产生大量的内存碎片。CMS 为了解决这个问题,会在多数时间采用标记-清除算法,暂时容忍内存碎片的存在,而当内存空间的碎片化程度已经大到影响对象分配时,会采用标记-整理算法收集一次,以获得规整的内存空间,但是标记-整理算法会移动对象,因此会使得 Stop The World 的时间变长!

(7)Garbage First 收集器【JDK 9 默认】

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,开创了收集器面向局部收集的设计思路和基于 Region 的堆内存布局形式。

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆空间划分为多个大小相等的独立区域(Region),每个 Region 都可以成为 Eden空间、Survivor空间、老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region 中有一类特殊的 Humongous 区域,专门用来存储大对象。只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数 -XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为2的N次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待

image-20220321220345669

G1可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的 Mixed GC模式,即混合 GC 模式。

G1 是一款面向服务端应用的收集器,能充分利用多CPU、多核环境,是一款并行与并发收集器,并且能建立『停顿预测模型』(能够支持指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过 N 毫秒这样的目标)。 G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region, 这也就是 Garbage-First 名称的来由。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证子G1收集器在有限的时间内可以获取尽可能高的收集效率。

运作步骤:

  1. 初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
  2. 并发标记(Concurrent Marking):从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
  3. 最终标记(Final Marking):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
  4. 筛选回收(Live Data Counting and Evacuation):更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。

image-20220321204052341

特点:

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

16. 类加载的过程

虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class 对象;

类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析 3 个部分统称为连接(Linking)。如图所示:

image-20220407232850497

加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

类加载过程如下:

  • 加载,加载分为三步:

    1. 通过类的全限定性类名获取该类的二进制流;
    2. 将该二进制流的静态存储结构转为方法区的运行时数据结构;
    3. 在堆中为该类生成一个 Class 对象,作为方法区这些数据的访问入口(存放堆内存中)。
  • 验证:验证该 class 文件中的字节流信息复合虚拟机的要求,不会威胁到 JVM 的安全;

    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  • 准备准备阶段是正式为类变量分配内存并设置类变量初始值的阶段

    • 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
    • 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
    • 这里所设置的初始值”通常情况”下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。
  • 解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

    • 符号引用就是一组符号来描述目标,可以是任何字面量。
    • 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
    • 在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
  • 初始化:初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

    • <clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值。
    • 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。
    • 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
    • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
  • 卸载

    卸载类即该类的 Class 对象被 GC。

    卸载类需要满足 3 个要求:

    1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
    2. 该类没有在其他任何地方被引用
    3. 该类的类加载器的实例已被 GC

    所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

    只要想通一点就好了,jdk 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

17. 类加载器

类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;

类加载器分为以下四种:

  • 启动类加载器(BootStrapClassLoader):用来加载 Java 核心类库,无法被 Java 程序直接引用,,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
  • 扩展类加载器(Extension ClassLoader):用来加载 Java 的扩展库,主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  • 应用程序类加载器(AppClassLoader):它根据 Java 的类路径来加载类,一般来说,Java 应用的类都是通过它来加载的;
  • 自定义类加载器:由 Java 语言实现,继承自 ClassLoader;

18. 双亲委派模型

如果一个类加载器收到一个类加载的请求,它首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类;

好处:

Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。可以防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个 java.lang.String 类,那么就无法保证类的唯一性。

例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

实现方式:

以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试调用自己的 findClass() 去加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}

if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

如何打破双亲委派模型

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法

打破双亲委派机制的例子,为什么要打破?

如果有基础类型想要调回用户的代码,就需要打破双亲委派机制。原因是全盘负责原则:当一个类加载器负责加载某个 Class 时,该 Class 所引用的其他 Class 也由该类加载器负责载入,除非显示使用另一个加载器来载入。

  • JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。

  • OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。

  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。

    tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:

    • 对于各个 webapp中的 classlib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
    • jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;
    • 热部署。

19. JVM 监控、故障处理工具

命令行工具

  • jps:JVM Process Status,类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • jstat:jstat(JVM statistics Monitoring) 是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap:jmap(JVM Memory Map) 命令用于生成 heap dump 文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现 OOM 的时候·自动生成 dump 文件。 jmap 不仅能生成dump文件,还阔以查询finalize执行队列、Java 堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
  • jhat:jhat(JVM Heap Analysis Tool)命令是与 jmap 搭配使用,用来分析 jmap 生成的 dump,jhat 内置了一个微型的 HTTP/HTML 服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为 jhat 是一个耗时并且耗费硬件资源的过程,一般把服务器生成的 dump 文件复制到本地或其他机器上进行分析。
  • jstack:jstack 用于生成 Java 虚拟机当前时刻的线程快照。jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果 Java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 Java stack 和 native stack 的信息,从而可以轻松地知道 Java 程序是如何崩溃和在程序何处发生问题,比如检测死锁。

可视化工具

  • JConsole:Java 监视与管理控制台

    JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监视本地及远程服务器的 java 进程的内存使用情况。你可以在控制台输出console命令启动或者在 JDK 目录下的 bin 目录找到jconsole.exe然后双击启动

  • Visual VM:多合一故障处理工具

    VisualVM 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息。在 VisualVM 的图形用户界面中,您可以方便、快捷地查看多个 Java 应用程序的相关信息。

-------本 文 结 束 感 谢 您 的 阅 读-------