Java基础 — Java 虚拟机 (下篇)

第 6 讲 Java虚拟机内存结构

根据《Java虚拟机规范》中的说法,Java虚拟机的内存结构可以分为公有和私有两部分。公有指的是所有线程都共享的部分,指的是Java堆方法区常量池。私有指的是每个线程的私有数据,包括:PC寄存器Java虚拟机栈本地方法栈

公有部分:Java堆、方法区、常量池

在Java虚拟机中,线程共享部分包括Java堆、方法区、常量池。

Java堆
  • Java堆指的是从JVM划分出来的一块区域,这块区域专门用于Java实例对象的内存分配,几乎所有实例对象都会在这里进行内存的分配(小对象栈上分配)。

  • Java堆根据对象存活时间的不同,Java堆还被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden区、From Surivor 0、To Survivor 1 区,默认的虚拟机配置为Eden:from:to=8:1:1。

  • 当有新对象需要分配内存时,它们会首先被分配到Eden区。当Eden区满了之后,会触发一次Minor GC(垃圾回收),这时Eden中存活的对象会被复制到From Survivor区。当再次触发GC时,Eden区和From Surivor区中存活的对象复制到To区。当对象在Suriviror区中经历了多次GC仍然存活,它们就会被移动到Old Generation中。这样的分代收集策略有助于提高垃圾回收的效率。因为大多数对象在年轻代就会很快被回收,只有少数长期存活的对象会被移动到老年代。

方法区
  • 方法区指的是存储Java类字节码数据的一块区域,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造方法等。可以看到常量池其实存放在方法区中,但《Java虚拟机规范》将常量池和方法区放在同一个等级上。

私有部分:PC寄存器、Java虚拟机栈、本地方法栈

线程私有部分为PC寄存器、Java虚拟机栈、本地方法栈。

PC寄存器
  • Program Counter 寄存器,指的是保存线程当前这个在执行的方法。任意时刻,一条Java虚拟机线程只会执行一个方法的代码,而这个被线程执行的方法称为该线程的当前方法,其地址被存在PC寄存器中。

Java虚拟机栈
  • Java虚拟机栈,这个栈与线程同时创建,用来存储栈帧,即存储局部变量与一些过程结果的地方。栈帧存储的数据包括:局部变量表、操作数栈。

本地方法栈
  • 当Java虚拟机使用其他语言(C语言)来实现指令集解释器时,也会使用本地方法栈。如果Java虚拟机不支持native方法,并且自己也不依赖传统栈,可以无需支持本地方法栈。

第 7 讲 JVM 类加载机制

  • 当javac编译器将Java源码编译成字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行整个过程,这个过程我们称之为 Java虚拟机的类加载机制。

  • JVM 执行class字节码的过程可以分为7个阶段:加载、验证、准备、解析、初始化、使用、卸载。

加载:

  • 加载阶段是类加载过程的第一个阶段,将代码数据加载到内存中。

  • 在这个阶段,JVM的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在JVM的方法区创建一个对应的Class对象,这个Class对象就是这个类这种数据的访问入口。

验证:

  • 当JVM加载完Class字节码文件并在方法区创建对应的Class对象后,JVM便会启动对该字节码流的校验。

  • 当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是按照JVM规范去写的。

  • 只有符合JVM字节码规范的文件才能被JVM正确执行。这个校验过程大致可以分为下面几个类型:
    1. JVM 规范校验

    • JVM会对字节流进行文件格式校验,判断其是否符合JVM规范,是否能被当前版本的虚拟机处理。

    1. 代码逻辑校验

    • JVM会对代码组成的数据流和控制流进行校验,确保JVM运行该字节码文件后不会出现致命错误。

准备(重点):

当完成字节码文件的校验后,JVM便会开始为类变量分配内存并初始化。

内存分配的对象
  • Java中的变量有类变量和类成员变量两种类型。类变量指的是别static修饰的变量,而其他所有类型的变量都属于类成员变量。

  • 在准备阶段,JVM只会为类变量(static修饰)分配内存,不会而类成员变量分配内存。类成员变量的内存分配徐娅等到初始化阶段才开始。

初始化的类型
  • 在准备阶段,JVM会为类变量分配内存,并为其初始化。但这里的初始化指的是为变量赋予该数据类型的“0”值、null值,不是用户代码中初始化的值。

解析:

当通过准备阶段后,JVM针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

初始化(重点):

到了初始化阶段,用户定义的Java程序代码才真正开始执行。

  • 在这个阶段,JVM会根据语句执行顺序对类对象进行初始化,一般来说当JVM遇到下面5种情况的时候会触发初始化:
    1. 遇到 new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。如:使用new关键字实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法时,就会触发。

    2. 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发初始化。

    3. 当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发父类的初始化。

    4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

    5. 当使用JDK1.7动态语言时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

使用:

  • 当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码。

卸载:

  • 当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存。

Java代码编译成字节码后,只有类初始化方法和对象初始化方法。
  • 类初始化方法
    • 编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。

  • 对象初始化方法
    • 编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

一个类的执行顺序(大概)

  1. 确定类变量的初始值

  • 在类加载准备阶段,JVM会为类变量初始化“0”值。

  1. 初始化入口方法 当进入类初始化阶段后,JVM会寻找整个main方法入口,从而初始化mian方法所在的整个类。当需要对一个类进行初始化时,首先初始化类构造器(),之后初始化对象构造器()。

  2. 初始化类构造器

  • JVM会按顺序收集类变量(static修饰)的赋值语句、静态代码块,最终构成类构造器由JVM执行。

  1. 初始化对象构造器

  • JVM会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由JVM执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成后返回。如此反复循环,最终返回 main 方法所在类。

第 8 讲 JVM垃圾回收机制

Java虚拟机的内存总是有限的,我们需要一个机制来不断回收废弃地内存,从而实现内存地循环利用,这样程序才能正常地运转下去。
不同的虚拟机有不同的实现方式,下面所说的垃圾回收都是以HotSpot虚拟机为例。

到底谁是垃圾?

  • 现在Java虚拟机判断垃圾对象使用的是:GC Root Tracing 算法。

  • 大概过程是从GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。

  • GC Root集合是一组活跃引用的集合,这些引用肯定是存活的,那么通过这些引用延伸的对象,自然也是存活的。

  • GC Root集合通常包括:所有当前被加载的Java类、Java类的引用类型静态变量、Java类的运行时常量池引用类型常量、VM的一些静态数据结构里指向GC堆里的对象的引用等等。

如何进行垃圾回收?

垃圾回收算法简单的来说有三种算法:标记清除算法、复制算法、标记压缩算法。

标记清除算法
  • 经历两个阶段,标记阶段和清除阶段。在标记阶段,标记所有由GC Root触发的可达对象。在清除阶段,清除所有未被标记的对象。

  • 问题:
    • 标记清除算法最大的问题就是空间碎片问题。如果空间碎片过多,就会导致内存空间的不连续。

  • 该算法比较适合在存活对象比较多的情况,虽然产生内存碎片,但是不需要移动太多对象。

复制算法
  • 复制算法的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,之后交换两个内存块的角色,完成垃圾回收。

  • 问题:
    • 该算法的缺点是要将内存空间折半,极大地浪费了内存空间。

  • 该算法适合存活对象比较少的情况,需要将内存空间折半,并且需要移动存活对象,但是清理过后不会有空间碎片。

标记压缩算法
  • 经历两个阶段,标记阶段、压缩阶段。在标记阶段,GC Root 引用集合 触发去标记所有对象。在压缩阶段,其则是将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。

  • 标记压缩算法可以说是标记清除算法的优化版,减少了空间碎片。

分代思想
  • 如果我们单独采用上述的任何一种算法,最终的垃圾回收效率都不会很好,因此在实际的垃圾回收算法中采用了分代算法。分代思想按照对象的生命周期长短将其分为了两个部分(新生代,老年代)。

  • 分代算法,就是根据JVM内存的不同内存区域,采用不同的垃圾回收算法。

  • 对于存活对象少的新生代区域,比较适合采用复制算法。只需复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。JVM对于新生代的内存区域划分为Eden:from:to=8:1:1,每次触发GC,就会让Eden和from(to)存活的对象复制到to(from)中。

  • 对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不需要移动太多的内存对象。

分区思想
  • 将整个堆空间划分为连续的不同小区间,如将Eden、from、to区,分成一个个小区。每个小区间独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间,可以较好地控制GC时间。

第 9 讲 JVM垃圾回收器

JVM的垃圾回收器可以分为四大类别:串行回收器、并行回收器、CMS回收器、G1回收器。

串行回收器

  • 串行回收器是指单线程进行垃圾回收的回收器。适用于单线程环境,如客户端使用。

  • 因为每次回收时只有一个线程,因此串行回收器在并发能力较弱得计算机上,其专注性和独占性得特点往往能让其有更好得性能体现。

  • 串行回收器可以在新生代和老年代使用,根据作用于不同得堆空间,分为新生代串行回收器和老年代串行回收器。

新生代串行回收器
  • 在新生串行回收器中使用的是复制算法。在串行回收器进行垃圾回收时,会触发Stop-The-World现象,即其他线程都需要暂停,等待垃圾回收完成。因此在某些情况下,其会造成较为糟糕得用户体验。

  • 使用 -XX:+UseSerialGC 参数可以指定使用新生代串行收集器和老年代串行收集器。当虚拟机在 Client 模式下运行时,其默认使用该垃圾收集器。

老年代串行回收器
  • 在老年代串行回收器中使用的是标记压缩算法。其与新生代串行收集器一样,只能串行、独占式地进行垃圾回收,因此也经常会有较长时间地Stop-The-World发生。

  • 但老年代串行回收器的好处之一,就是其可以与多种新生带回收器配合使用。

  • 要启用老年代串行回收器,可以尝试以下参数:
    • -XX:UseSerialGC,新生代、老年代都使用串行回收器。

    • -XX:UseParNewGC,新生代使用 ParNew 回收器,老年代使用串行回收器。

    • -XX:UseParallelGC,新生代使用 ParallelGC 回收器,老年代使用串行回收器。

并行回收器

  • 并行回收器在串行回收器的基础上做了改进,其使用多线程进行垃圾回收。对于并行能力强的机器,可以有效缩短垃圾回收所使用的时间。适用于多线程环境,注重吞吐量的应用。

  • 根据作用内存区域的不同,并行回收器也有三个不同的回收器:新生代 ParNew 回收器、新生代 ParallelGC 回收器、老年代 ParallelGC 回收器。

新生代 ParNew 回收器
  • 新生代 ParNew 回收器工作在新生代,其只是简单地将串行回收器多线程化,其回收策略、算法以及参数和新生代串行回收器一样。

  • 新生代 ParNew 回收器同样使用复制的垃圾回收算法,其垃圾收集过程中同样会触发 Stop-The-World 现象。但因为其使用多线程进行垃圾回收,因此在并发能力强的 CPU 上,其产生的停顿时间要短于串行回收器。

  • 但在单 CPU 或并能能力弱的系统中,并行回收器效果会因为线程切换的原因,其实际表现反而不如串行回收器。

  • 要开启新生代 ParNew 回收器,可以使用以下参数:
    • -XX:+UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器。

    • -XX:UseConcMarkSweepGC:新生代使用 ParNew 回收器,老年代使用 CMS。

    • -XX:ParallelGCThreads:指定 ParNew 回收器的工作线程数量。

新生代 Parallel GC 回收器
  • 新生代 Parallel GC 回收器与新生代 ParNew 回收器非常类似,其也是使用复制算法,都是多线程、独占式的收集器,也会导致 Stop-The-World。但其余 ParNew 回收器的一个重大不同是:其非常注重系统的吞吐量。

  • 之所以说新生代 Parallel GC 回收器非常注重系统吞吐量,是因为其有一个自适应 GC 调节策略。我们可以使用 -XX:+UseAdaptiveSizePolicy 参数打开这个策略,在这个模式下,新生代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数都会被自动调节,已达到堆大小、吞吐量、停顿时间的平衡点。

  • Parallel GC 回收器提供了两个重要参数用于控制系统的吞吐量。

  • -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间。在 ParallelGC 工作时,其会自动调整响应参数,将停顿时间控制在设置范围内。为了达到目的,其可能会使用较小的堆,但这会导致 GC 较为频繁。

  • -XX:GCTimeRatio:设置吞吐量大小,其实一个 0 - 100 的整数。假设 GCTimeRatio 的值为 n,那么系统将不花费超过 1/(1+n) 的时间用于垃圾手机。比如 GCTimeRatio 值为 19,那么系统用于垃圾收集的时间不超过 1 /(1+19) = 5%。默认情况下,它的取值是 99,即不超过 1% 的时间用于垃圾收集。

  • 新生代 Parallel GC 回收器可以使用以下参数启用:
    • -XX:+UseParallelGC:新生代使用 Parallel 回收器,老年代使用串行回收器。

    • -XX:+UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。

老年代 ParallelOldGC 回收器
  • 老年代 ParallelOldGC 回收器也是一种多线程并发的回收器,与新生代 ParallelGC 收集器一样,其也是注重吞吐量的收集器,只不过其是作用于老年代。

  • ParallelOldGC 回收器使用的是标记压缩算法,只有在 JDK 1.6 中才可以使用。我们可以使用-XX:UseParallelOldGC参数在新生代中使用 ParallelGC 收集器,在老年代中使用 ParallelOldGC 收集器。参数 -XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。

CMS回收器

  • CMS 回收器主要关注系统停顿时间,CMS 回收器全称为 Concurrent Mark Sweep,意为标记清除算法,其是一个使用多线程并行回收的垃圾回收器。适用于低延迟的应用,如Web服务器。CMS收集可能会产生空间碎片,需要与老年代串行回收器配合使用以进行碎片处理。

工作步骤:
  • CMS 的主要工作步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发充值。其中初始标记和重新标记是独占系统资源的,而其他阶段则可以和用户线程一起执行。

  • 在整个 CMS 回收过程中,默认情况下会有预清理的操作,我们可以关闭开关 -XX:-CMSPrecleaningEnabled 不进行预清理。因为重新标记是独占 CPU 的,因此如果新生代 GC 发生之后,立刻出发一次新生代 GC,那么停顿时间就会很长。为了避免这种情况,预处理时会刻意等待一次新生代 GC 的发生,之后在进行预处理。

主要参数:
  • 启动 CMS 回收器刻意使用参数:-XX:+UseConcMarkSweepGC,线程并发数量刻意通过 -XX:ConcGCThreads 或 -XX:ParallelCMSThreads 参数设定。

  • 此外,我们还可以设置 -XX:CMSInitiatingOccupancyFraction 来指定老年代空间使用阈值。当老年代空间使用率达到这个阈值时,会执行一次 CMS 回收,而不像其他回收器一样等到内存不够用的时候才进行 GC。

  • 我们之前说过标记清除算法的缺点是会产生内存碎片,因此 CMS 回收器会产生较多内存碎片。我们可以使用 XX:+UseCMSCompactAtFullCollection 参数让 CMS 在完成垃圾回收后,进行一次内存碎片整理。使用 -XX:CMSFullGCsBeforeCompaction 参数设置进行多少次 CMS 回收后,进行一次内存压缩。

  • 此外,如果希望使用 CMS 回收 Perm 区,那么则可以打开 -XX:+CMSClassUnloadingEnabled 开关。打开该开关后,如果条件允许,那么系统会使用 CMS 的机制回收 Perm 区 Class 数据。

G1回收器

  • G1 回收器是 JDK 1.7 中使用的全新垃圾回收器,从长期目标来看,其是为了取代 CMS 回收器。适用于大内存、多处理环境,能够提供可预测的停顿时间。

  • G1 回收器拥有独特的垃圾回收策略,和之前所有垃圾回收器采用的垃圾回收策略不同。从分代看,G1 依然属于分代垃圾回收器。但它最大的改变是使用了分区算法,从而使得 Eden 区、From 区、Survivor 区和老年代等各块内存不必连续。

工作步骤
  • G1 收集器的收集过程主要有四个阶段:

    • 新生代 GC

    • 并发标记周期

    • 混合收集

    • 如果需要,可能进行 FullGC

  • 新生代 GC 与其他垃圾收集器的类似,就是清空 Eden 区,将存活对象移动到 Survivor 区,部分年龄到了就移动到老年代。

  • 并发标记周期则分为:初始标记、根区域扫描、并发标记、重新标记、独占清理、并发清理阶段。其中初始标记、重新标记、独占清理是独占式的,会引起停顿。并且初始标记会引发一次新生代 GC。在这个阶段,所有将要被回收的区域会被 G1 记录在一个称之为 Collection Set 的集合中。

  • 混合回收阶段会首先针对 Collection Set 中的内存进行回收,因为这些垃圾比例较高。G1 回收器的名字 Garbage First 就是这个意思,垃圾优先处理的意思。在混合回收的时候,也会执行多次新生代 GC 和 混合 GC,从而来进行内存的回收。

  • 必要时进行 Full GC。当在回收阶段遇到内存不足时,G1 会停止垃圾回收并进行一次 Full GC,从而腾出更多空间进行垃圾回收。

相关参数
  • 打开 G1 收集器,我们可以使用参数:-XX:+UseG1GC。

  • 设置目标最大停顿时间,可以使用参数:-XX:MaxGCPauseMillis。

  • 设置 GC 工作线程数量,可以使用参数:-XX:ParallelGCThreads。

  • 设置堆使用率触发并发标记周期的执行,可以使用参数:-XX:InitiatingHeapOccupancyPercent。

总结

  • 从一开始的串行回收器,到后来的并行回收器、CMS回收器,到最后的 G1 回收器,垃圾回收器不断改进,使得垃圾回收效率不断提升。特别是分区思想诞生后,对于垃圾回收停顿时间的控制更加细腻,可以让应用有更完美的延时控制,从而呈现更好的用户体验。

第 10 讲 垃圾回收的类型

Minor GC

  • 从年轻代空间回收内存称为Minor GC,有时候也称为Young GC。

  • 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,如当Eden区满了。所以Eden区越小,越频繁执行Minor GC。

  • 当年轻代中的Eden区分配满的时候,年轻代中的部分对象会晋升到老年代,所以Minor GC后老年代的占用量通常会有所提高。

  • 所有的Minor GC都会触发Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分Eden区中的对象都能被认为是垃圾,永远也不会被复制到Survivor区或老年代空间。如果情况相反,那么Minor GC 执行时暂停的时间将会长很多。

Major GC

  • 从老年代空间回收内存称为Major GC,有时候也称为Old GC。

  • 许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。

  • 许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。

Full GC

  • FUll GC是清理整个堆空间——包括年轻代、老年代和永久代。因此Full GC可以i说是Minor GC和Major GC的结合。

  • 当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。

  • 另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC。

Stop-The_World

  • Stop-The-World,是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程有回收垃圾的时间间隔。

  • 在 Stop-The-World 这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World 时间的长短将关系到应用程序的响应时间,因此在 GC 过程中,Stop-The-World 的时间是一个非常重要的指标。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值