关于java:java开发之JVM基础知识分享

39次阅读

共计 12655 个字符,预计需要花费 32 分钟才能阅读完成。

虚拟机运行机制

JVM 运行在操作系统上,不与硬件设施间接交互。

Java 程序执行流程:Java 源代码文件(Hello.java)被编译器编译成字节码文件(Hello.class),字节码文件被 JVM 中的解释器编译成机器码在不同操作系统上运行。

Java 程序具体运行过程:

Java 源文件被编译成字节码文件

JVM 将字节码文件通过 JVM 外部解释器编译成与操作系统对应的机器码

机器码调用相应操作系统的 Native Method 库执行相应办法

Java 跨平台的起因:每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是雷同的。

虚拟机实例生命周期形容:在一个 Java 过程开始运行后,虚拟机就开始实例化,有多个过程启动就会实例化多个虚拟机实例。过程退出或敞开,则虚拟机实例销毁,java 培训在多个虚拟机实例之间不能共享数据。

虚拟机内部结构

JVM 包含一个类加载器子系统(Class Loader Subsystem)、运行时数据区(Runtime Data Area)、执行引擎(Engine)和本地接口库(Nateive Interface Library)。

本地接口库调用本地办法库(Native Method Library)与 OS 交互。

JVM 外部结构图如下:

其中:

类加载器子系统用于将编译好的字节码文件(.class)加载到 JVM。

运行时数据区用于存储在 JVM 运行过程中产生的数据,包含程序计数器(PC)、办法区(Method Area)、本地办法区(Native Method Area)、虚拟机栈(JVM Stack)、虚拟机堆(JVM Heap)。

执行引擎包含即时编译器(JIT Compiler)和垃圾回收器(Garbage Collection)。

即时编译器用于将字节码编译成具体的机器码。

垃圾回收器用于治理内存,回收在运行过程中不再应用的对象。

本地接口库用于调用操作系统的本地办法库实现具体的指令操作。

虚拟机与多线程

在多核操作系统上,JVM 容许在一个过程内同时并发执行多个线程。

JVM 中的线程与操作系统中的线程是互相对应的。

JVM 线程的调度是交给操作系统负责的。

有些程序语言有协程的概念,如 Golang 的并发,协程能够粗略地看成是轻量级的线程,一个协程并非对应一个操作系统线程,而是多个协程对应一个操作系统线程,协程之间通过调度器协调。

这种设计有以下的益处:

轻量级,资源占用较少。

调度是基于语言层面,缩小操作系统线程调度的开销。

从 JVM 角度看,Java 线程的执行流程:

筹备:实现 JVM 线程的本地存储、缓冲区调配、同步对象、栈、程序计数器等初始化工作。

创立:调用操作系统接口创立一个与之对应的原生线程。

调度:操作系统负责调度所有线程,并为其调配 CPU 工夫片。

执行:在原生线程初始化结束时,调用 Java 线程的 run() 办法执行线程逻辑。

完结:在 run() 办法逻辑执行结束后,开释原生线程和 Java 线程所对应的资源。

回收:在 Java 线程运行完结时,原生线程随之被回收。

在 JVM 后盾运行的线程次要有:

虚拟机线程(JVM Thread):虚拟机线程在 JVM 达到平安点(Safe Point)时呈现。

周期性工作线程:通过定时器调度线程来实现周期性操作的执行。

GC 线程:GC 线程反对 JVM 中不同的垃圾回收流动。

编译器线程:编译器线程在运行时将字节码动静编译成本地平台机器码,是 JVM 跨平台的具体实现。

信号散发线程:接管发送到 JVM 的信号并调用 JVM 办法。

虚拟机内存区域

JVM 的内存区域分为:

线程公有区域:线程公有区域包含的组件有程序计数器、虚拟机栈、本地办法区。

线程共享区域:虚拟机堆、办法区。

间接内存

线程公有区域的生命周期与线程雷同,随线程的启动而创立,随线程的完结而销毁。

在 JVM 中,每个 Java 线程与操作系统本地线程间接映射,因而这部分内存区域的存在与否和本地线程的启动和销毁对应。

线程共享区域随虚拟机的启动而创立,随虚拟机的敞开而销毁。

间接内存又称为对外内存,间接内存不是 JVM 运行时数据区的一部分,但在并发编程中被频繁调用。

JDK 的 NIO 模块提供的基于 Channel 与 Buffer 的 I / O 操作就是基于堆外内存实现的,NIO 模块通过调用 Native Method Library 间接在操作系统上调配堆外内存,而后应用 DirectByteBuffer 对象作为这块内存的援用对内存进行操作,Java 过程能够通过堆外内存技术防止在 Java 堆和 Native 堆中来回复制数据带来的资源占用和性能耗费,因而堆外内存在高并发利用场景下被宽泛应用(Netty、Flink、HBase、Hadoop 都有用到堆外内存)。

程序计数器

程序计数器(PC)属于线程公有区域,程序计数器是惟一无内存溢出(Out of Memory)问题的区域。

程序计数器是一块很小的内存空间,用于存储以后运行的线程所执行的字节码的行号指示器。

每个运行中的线程都有一个独立的程序计数器,在办法正在执行时,该办法的程序计数器记录的是实时虚拟机字节码指令的地址。

注:如果该办法执行的是 native 办法,则程序计数器的值为空(Undefined)。

虚拟机栈

虚拟机栈(JVM Stack)属于线程公有区域,形容 Java 办法的执行过程。

虚拟机栈是形容 Java 办法的执行过程的内存模型,它在以后栈帧(Stack Frame)中次要存储了以下信息:

局部变量表

操作数栈

动静链接

办法进口

同时,栈帧用来存储局部运行时数据及其数据结构,解决动静链接(Dynamic Linking)办法的返回值和异样分派(Dispatch Exception)。

栈帧:栈帧用来记录办法的执行过程。

办法被执行时,虚构机会为其创立一个与之对应的栈帧。

虚拟机栈中的入栈操作:办法的执行。

虚拟机栈中的出栈操作:办法的返回。

无论办法是失常运行实现,还是异样(抛出了在办法内未被捕捉的异样)退出,都认为办法运行完结。

线程运行及栈帧的变动过程如下:

线程 1 在 CPU1 上运行,线程 2 在 CPU2 上运行,在 CPU 资源不够时其余线程将处于期待状态(图中的线程 N),期待获取 CPU 工夫片。而在线程外部,每个办法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程以后只有一个栈帧处于活动状态。

本地办法区

本地办法区(Native Method Area)和虚拟机栈的作用相似,区别是虚拟机栈是为执行 Java 办法服务的,本地办法区是为 Native 办法服务的。

虚拟机堆

虚拟机堆(JVM Heap),也称为运行时数据区,虚拟机堆是线程共享的。

在 JVM 运行过程中创立的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾回收器进行垃圾回收的最次要的内存区域。

因为古代 JVM 采纳分代收集算法,因而 Java 堆从 GC(Garbage Collection,垃圾回收)的角度还能够细分为:新生代、老年代和永恒代。

办法区

办法区(Method Area),也被称为永恒代,用于存储常量、动态变量、类信息、JIT 编译后的机器码、运行时常量池等数据。

JVM 把 GC 分代收集扩大至办法区,即应用 Java 堆的永恒代来实现办法区,这样 JVM 的垃圾收集器就能够像治理 Java 堆一样治理这部分内存。永恒带的内存回收次要针对常量池的回收和类的卸载,因而可回收的对象很少。

常量被存储在运行时常量池(Runtime Constant Pool)中,是办法区的一部分。动态变量也属于办法区的一部分。在类信息(Class 文件)中岂但保留了类的版本、字段、办法、接口等形容信息,还保留了常量信息。

在即时编译后,代码的内容将在执行阶段(类加载实现后)被保留在办法区的运行时常量池中。Java 虚拟机对 Class 文件每一部分的格局都有明确的规定,只有合乎 JVM 标准的 Class 文件能力通过虚拟机的查看,而后被装载、执行。

虚拟机运行时内存

JVM 的运行时内存也叫做 JVM 堆,从 GC 的角度能够将 JVM 堆分为新生代、老年代和永恒代。

其中,新生代默认占 1 / 3 堆空间,老年代默认占 2 / 3 堆空间。

新生代又分为 Eden 区、ServivorFrom 和 ServivorTo 区。

Eden 区默认占 8 /10 新生代空间。

ServivorForm 区和 ServivorTo 区默认别离占 1 /10 新生代空间。

JVM 堆分代分区的构造如下:

JVM 新创建的对象(除了大对象)都会被寄存在新生代,默认占 1 / 3 堆内存空间。

因为 JVM 会频繁创建对象,所以新生代会频繁登程 MinorGC 进行垃圾回收。

新生代

新生代分为 Eden 区(8/10 新生代空间)、ServivorFrom 区(1/10 新生代空间)、ServivorTo 区(1/10 新生代空间)。

Eden 区:Java 新创建的对象首先会被寄存在 Eden 区,如果新创建的对象属于大对象,则间接将其调配到老年代。

大对象的定义和具体的 JVM 版本、堆大小和垃圾回收策略无关,个别为 2KB~128KB,可通过 XX:PretenureSizeThreshold 设置其大小。

在 Eden 区的内存空间有余时会触发 MinorGC,对新生代进行一次垃圾回收。

ServivorTo 区:保留上一次 MinorGC 时的幸存者。

ServivorFrom 区:将上一次 MinorGC 时的幸存者作为这一次 MinorGC 的被扫描者。

新生代的 GC 过程叫做 MinorGC,采纳复制算法实现,具体过程如下:

将 Eden 区和 ServivorFrom 区中存活的对象复制到 ServivorTo 区,如果某对象的年龄达到老年代的规范,则将其复制到老年代,同时将这些对象年龄加 1。

对象降职老年代的规范由 XX:MaxTenuringThreshold 设置,默认为 15。

如果 ServivorTo 区的内存空间不够,则也间接将其复制到老年代。

如果对象属于大对象,也间接将其复制到老年代。

清空 Eden 区和 ServivorFrom 区对象。

将 ServivorTo 区和 ServivorFrom 区调换,原来的 ServivorTo 区成为下一次 GC 时的 ServivorFrom 区。

老年代

老年代次要寄存长生命周期对象和大对象。

老年代的 GC 过程叫做 MajorGC,在老年代,对象比较稳定,MajorGC 不会被频繁触发。

在进行 MajorGC 之前,JVM 会进行一次 MinorGC,在 MinorGC 过后依然呈现老年代空间有余或无奈找到足够大的间断内存空间调配给新创建的大对象时,会触发 MajorGC 进行垃圾回收流动,开释 JVM 的内存空间。

MajorGC 采纳标记革除算法,该算法首先会扫描所以对象并标记存活的对象,而后回收未被标记的对象,并开释内存空间。

因为要先扫描老年代的所有对象再回收,所以 MajorGC 的耗时比拟长,MajorGC 的标记革除算法容易产生内存碎片。

在老年代没有内存空间可调配时,会抛出 OOM 异样。

永恒代

永恒代指内存的永恒保留区域,次要寄存 Class 和 Meta(元数据)的信息。

Class 在类加载时被放入永恒代。

永恒代和老年代、新生代不同,GC 不会在程序运行期间对永恒代的内存进行清理,这也导致了永恒代的内存会随着加载的 Class 文件的减少而减少,在加载的 Class 文件过多时会抛出 Out OfMemory 异样,比方 Tomcat 援用 Jar 文件过多导致 JVM 内存不足而无奈启动。

须要留神的是,在 Java 8 中永恒代曾经被元数据区(也叫作元空间)取代。

元数据区的作用和永恒代相似,二者最大的区别在于:元数据区并没有应用虚拟机的内存,而是间接应用操作系统的本地内存。

因而,元空间的大小不受 JVM 内存的限度,只和操作系统的内存无关。

在 Java 8 中,JVM 将类的元数据放入本地内存(Native Memory)中,将常量池和类的动态变量放入 Java 堆中,这样 JVM 可能加载多少元数据信息就不再由 JVM 的最大可用内存(MaxPermSize)空间决定,而由操作系统的理论可用内存空间决定。

垃圾回收与算法

确定垃圾

Java 采纳援用计数法和可达性剖析来确定对象是否须要被回收。

援用计数法容易产生循环援用问题。

可达性剖析通过根搜索算法(GC Roots Tracing)来实现。

根搜索算法以一系列 GC Roots 的点作为终点向下搜寻,在一个对象到任何 GC Roots 都没有援用链相连时,阐明其已死亡。

根搜索算法次要针对栈中的援用、办法区中的动态援用和 JNI 中的援用开展剖析。

援用计数法

在 Java 中如果要操作对象,就必须先获取该对象的援用,因而能够通过援用计数法来判断一个对象是否能够被回收。

在为对象增加一个援用时,援用计数加 1;在为对象删除一个援用时,引进计数减 1;如果一个对象的援用计数为 0,则示意此刻该对象没有被援用,能够被回收。

援用计数法容易产生循环援用问题。

循环援用指两个对象互相援用,导致它们的援用始终存在,而不能被回收。

可达性剖析

为了解决援用计数法的循环援用问题,Java 还采纳了可达性剖析来判断对象是否能够被回收。

可达性剖析的过程:

首先定义一些 GC Roots 对象,而后以这些 GC Roots 对象作为终点向下搜寻,如果在 GC roots 和一个对象之间没有可达门路,则称该对象是不可达的。

不可达对象要通过至多两次标记能力断定其是否能够被回收,如果在两次标记后该对象依然是不可达的,则将被垃圾收集器回收。

罕用垃圾回收算法

Java 中罕用的垃圾回收算法有:

标记革除(Mark-Sweep)算法

复制(Copying)算法

标记整顿(Mark Compact)算法

分代收集(Generational Collecting)算法

标记革除算法

标记革除算法是根底的垃圾回收算法,其过程分为标记和革除阶段。

在标记阶段标记所以须要回收的对象,在革除阶段革除可回收的对象并开释其所占用的内存空间。

因为标记革除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因而如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无奈取得间断可用空间的问题。

复制算法

复制算法是为了解决标记革除算法内存碎片化的问题而设计的。

复制算法的基本原理:

首先将内存划分为两块大小相等的内存区域,即区域 1 和区域 2,新生成的对象都被寄存在区域 1 中。

在区域 1 内的对象存储满后会对区域 1 进行一次标记,并将标记后依然存活的对象全副复制到区域 2 中,这时区域 1 将不存在任何存活的对象,间接清理整个区域 1 的内存即可。

复制算法的内存清理效率高且易于实现,但因为同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因而存在大量的内存节约。

同时,在零碎中有大量长时间存活的对象时,这些对象将在内存区域 1 和内存区域 2 之间来回复制而影响零碎的运行效率。

因而,该算法只在对象为“朝生夕死”状态时运行效率较高。

标记整顿算法

标记整顿算法联合了标记革除算法和复制算法的长处,其标记阶段和标记革除算法的标记阶段雷同,在标记实现后将存活的对象移到内存的另一端,而后革除该端的对象并开释内存。

分代收集算法

无论是标记革除算法、复制算法还是标记整顿算法,都无奈对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。

因而,针对不同的对象类型,JVM 采纳了不同的垃圾回收算法,该算法被称为分代收集算法。

分代收集算法依据对象的不同类型将内存划分为不同的区域,JVM 将堆划分为新生代和老年代。

新生代次要寄存新生成的对象,其特点是对象数量多然而生命周期短,在每次进行垃圾回收时都有大量的对象被回收。

老年代次要寄存大对象和生命周期长的对象,因而可回收的对象绝对较少。

因而,JVM 依据不同的区域对象的特点抉择了不同的算法。

目前,大部分 JVM 在新生代都采纳了复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,须要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因而采纳复制算法能平安、高效地回收新生代大量的短生命周期的对象并开释内存。

JVM 将新生代进一步划分为一块较大的 Eden 区和两块较小的 Servivor 区,Servivor 区又分为 ServivorFrom 区和 ServivorTo 区。

JVM 在运行过程中次要应用 Eden 区和 ServivorFrom 区,进行垃圾回收时会将在 Eden 区和 ServivorFrom 区中存活的对象复制到 ServivorTo 区,而后清理 Eden 区和 ServivorFrom 区的内存空间。

老年代次要寄存生命周期较长的对象和大对象,因此每次只有大量非存活的对象被回收,因此在老年代采纳标记革除算法。

在 JVM 中还有一个区域,即办法区的永恒代,永恒代用来存储 Class 类、常量、办法形容等。

在永恒代次要回收废除的常量和无用的类。

JVM 内存中的对象次要被调配到新生代的 Eden 区和 ServivorFrom 区,在多数状况下会被间接调配到老年代。

在新生代的 Eden 区和 ServivorFrom 区的内存空间有余时会触发一次 GC,该过程被称为 MinorGC。

在 MinorGC 后,在 Eden 区和 ServivorFrom 区中存活的对象会被复制到 ServivorTo 区,而后 Eden 区和 ServivorFrom 区被清理。

如果此时在 ServivorTo 区无奈找到间断的内存空间存储某个对象,则将这个对象间接存储到老年代。

若 Servivor 区的对象通过一次 GC 后依然存活,则其年龄加 1。

在默认状况下,对象在年龄达到 15 时,将被移到老年代。

援用类型

在 Java 中,所有皆对象,对象的操作是通过该对象的援用(Reference)实现的。

在 Java 中,援用类型有 4 种:

强援用

软援用

弱援用

虚援用

强援用

在 Java 中最常见的就是强援用。

强援用:在把一个对象赋给一个援用变量时,这个援用变量就是一个强援用。

有强援用的对象肯定为可达性状态,所以不会被垃圾回收机制回收。

因而,强援用是造成 Java 内存透露(Memory Link)的次要起因。

软援用

软援用:软援用通过 SoftReference 类实现。

如果一个对象只有软援用,则在零碎内存空间有余时该对象将被回收。

弱援用

弱援用:弱援用通过 WeakReference 类实现。

如果一个对象只有弱援用,则在垃圾回收过程中肯定会被回收。

虚援用

虚援用:虚援用通过 PhantomReference 类实现。

虚援用和援用队列联结应用,次要用于跟踪对象的垃圾回收状态。

分代收集算法

JVM 依据对象存活周期的不同将内存划分为新生代、老年代和永恒代,并依据各年代的特点别离采纳不同的 GC 算法。

新生代与复制算法

新生代次要存储短生命周期的对象,因而在垃圾回收的标记阶段会标记大量已死亡的对象及大量存活的对象,因而只须要选用复制算法将大量存活的对象复制到内存的另一端并清理原区域的内存即可。

老年代与标记整顿算法

老年代次要寄存长生命周期的对象和大对象,可回收的对象个别较少,因而 JVM 采纳标记整顿算法进行垃圾回收,间接开释死亡状态的对象所占用的内存空间即可。

分区收集算法

分区算法将整个堆空间划分为间断的大小不同的小区域,对每个小区都独自进行内存应用和垃圾回收,这样做的益处是能够依据每个小区域内存的大小灵便应用和开释内存。

分区收集算法能够依据零碎可承受的进展工夫,每个都疾速回收若干个小区域的内存,以缩短垃圾回收零碎进展的工夫,最初以屡次并行累加的形式逐渐实现整个内存区域的垃圾回收。

如果垃圾回收机制一次回收整个堆内存,则须要更长的零碎进展工夫,长时间的零碎进展将影响零碎运行的稳定性。

垃圾收集器

Java 堆内存分为新生代和老年代。

新生代次要存储短生命周期的对象,适宜应用复制算法进行垃圾回收。

老年代次要存储长生命周期对象和大对象,适宜应用标记整顿算法进行垃圾回收。

JVM 针对新生代和老年代别离提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有 SerialOld、ParallelOld、CMS,还有针对不同区域的 G1 分区收集算法。

Serial

Serial:单线程、复制算法。

Serial 垃圾收集器基于复制算法实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其余所以工作线程,直到垃圾收集完结。

Serial 垃圾收集器采纳了复制算法,简略、搞笑,对于单 CPU 运行环境来说,没有线程交互开销,能够取得最高的单线程垃圾收集效率,因而 Serial 垃圾收集器是 JVM 运行在 Client 模式下的新生代的默认垃圾收集器。

ParNew

ParNew:多线程、复制算法。

ParNew 垃圾收集器是 Serial 垃圾收集器的多线程实现,同样采纳了复制算法,它采纳多线程模式工作,除此之外和 Serial 收集器简直一样。

ParNew 垃圾收集器在垃圾收集过程中会暂停所有其余工作线程,是 Java 虚拟机运行在 Server 模式下的新生代的默认垃圾收集器。

ParNew 垃圾收集器默认开启与 CPU 等同数量的线程进行垃圾回收,在 Java 利用启动时可通过 -XX:ParallelGCThreads 参数调节 ParNew 垃圾收集器的工作线程数。

Parallel Scavenge

Parallel Scavenge:多线程、复制算法。

Parallel Scavenge 收集器是为进步新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在零碎吞吐量上有很大的优化,能够更高效地利用 CPU 尽快实现垃圾回收工作。

Parallel Scavenge 通过自适应调节策略进步零碎吞吐量,提供了三个参数用于调节、管制垃圾回收的进展工夫及吞吐量,别离是管制最大垃圾收集进展工夫的 -XX:MaxGCPauseMillis 参数,管制吞吐量大小的 -XX:GCTimeRatio 参数和管制自适应调节策略开启与否的 UseAdaptiveSizePolicy 参数。

Serial Old

Serial Old:单线程、标记整顿算法。

Serial Old 垃圾收集器是 Serial 垃圾收集器的老年代实现,同 Serial 一样采纳单线程执行,不同的是,Serial Old 针对老年代长生命周期的特点基于标记整顿算法实现。

Serial Old 垃圾收集器是 JVM 运行在 Client 模式下的老年代的默认垃圾收集器。

新生代的 Serial 垃圾收集器和老年代的 Serial Old 垃圾收集器可搭配应用,别离针对 JVM 的新生代和老年代进行垃圾回收,其垃圾收集过程如图所示。

在新生代采纳 Serial 垃圾收集器基于复制算法进行垃圾回收,未被其回收的对象在老年代被 Serial Old 垃圾收集器基于标记整顿算法进行垃圾回收。

Parallel Old

Parallel Old:多线程、标记整顿算法。

Parallel Old 垃圾收集器采纳多线程并发进行垃圾回收,它依据老年代长生命周期的特点,基于多线程的标记整顿算法实现。

Parallel Old 垃圾收集器在设计上优先思考零碎吞吐量,其次思考进展工夫等因素,如果系统对吞吐量的要求较高,则能够优先思考新生代的 Parallel Scavenge 垃圾收集器和老年代的 Parallel Old 垃圾收集器的配合应用。

新生代的 Parallel Scavenge 垃圾收集器和老年代的 Parallel Old 垃圾收集器的搭配运行过程如图。

新生代基于 Parallel Scavenge 垃圾收集器的复制算法进行垃圾回收,老年代基于 Parallel Old 垃圾收集器的标记整顿算法进行垃圾回收。

CMS

CMS(Concurrent Mark Sweep)垃圾收集器是为老年代设计的垃圾收集器,其次要目标是达到最短的垃圾回收进展工夫,基于线程的标记革除算法实现,以便在多线程并发环境下以最短的垃圾收集进展工夫进步零碎的稳定性。

CMS 的工作机制绝对简单,垃圾回收过程蕴含如下 4 个步骤。

初始标记:只标记和 GC Roots 间接关联的对象,速度很快,须要暂停所有工作线程。

并发标记:和用户线程一起工作,执行 GC Roots 跟踪标记过程,不须要暂停工作线程。

从新标记:在并发标记过程中用户线程持续运行,导致在垃圾回收过程中局部对象的状态发生变化,为了确保这部分对象的状态正确性,须要对其从新标记并暂停工作线程。

并发革除:和用户线程一起工作,执行革除 GC Roots 不可达对象的工作,不须要暂停工作线程。

CMS 垃圾收集器在和用户线程一起工作时(并发标记和并发革除)不须要暂停用户线程,无效缩短了垃圾回收时零碎的进展工夫,同时因为 CMS 垃圾收集器和用户线程一起工作,因而其并行度和效率也有很大晋升。

G1

G1(Garbage First)垃圾收集器为了防止全区域垃圾收集引起的零碎进展,将堆内存划分为大小固定的几个独立区域,独立应用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后盾保护一个优先级列表,在垃圾回收过程中依据零碎容许的最长垃圾收集工夫,优先回收垃圾最多的区域。

G1 垃圾收集器通过内存区域独立划分应用和依据不同优先级回收各区域垃圾的机制,确保了 G1 垃圾收集器在无限工夫内取得最高的垃圾收集效率。

绝对于 CMS 收集器,G1 垃圾收集器两个突出的改良。

基于标记整顿算法,不产生内存碎片。

能够准确地管制进展工夫,在不就义吞吐量的前提下实现短进展垃圾回收。

类加载机制

JVM 的类加载阶段

JVM 的类加载分为 5 个阶段:加载、验证、筹备、解析、初始化。

在类初始化实现后就能够应用该类的信息,在一个类不再被须要时能够从 JVM 中卸载。

加载

加载:加载是指 JVM 读取 Class 文件,并且依据 Class 文件形容创立 java.lang.Class 对象的过程。

类加载过程次要蕴含将 Class 文件读取到运行时区域的办法区内,在堆中创立 java.lang.Class 对象,并封装类在办法区的数据结构的过程,在读取 Class 文件时既能够通过文件的模式读取,也能够通过 JAR 包、WAR 包读取,还能够通过代理主动生成 Class 或其余形式读取。

验证

验证:验证次要用于确保 Class 文件合乎以后虚拟机的要求,保障虚拟机本身的平安,只有通过验证的 Class 文件能力被 JVM 加载。

筹备

筹备:筹备次要工作是在办法区中为类变量分配内存空间并设置类中变量的初始值。

初始值指不同数据类型的默认值,这里须要留神 final 类型的变量和非 final 类型的变量在筹备阶段的数据初始化过程不同。

栗如,一个成员变量的定义如下:

public static long value = 1000;

在以上代码中,动态变量 value 在筹备阶段的初始值是 0,将 value 设置为 1000 的动作是在对象初始化时实现的,因为 JVM 在编译阶段会将动态变量的初始化操作定义在结构器中。然而,如果将变量 value 申明为 final 类型:

public static final int value = 1000;

则 JVM 在编译阶段后会为 final 类型的变量 value 生成其对应的 ConstantValue 属性,虚拟机在筹备阶段会依据 ConstantValue 属性将 value 赋值为 1000。

解析

解析:解析是指 JVM 会将常量池中的符号援用替换为间接援用。

初始化

初始化:初始化次要通过执行类结构器的 <client> 办法为类进行初始化。

<client> 办法是在编译阶段由编译器主动收集类中动态语句块和变量的赋值操作组成的。

JVM 规定,只有在父类的 <client> 办法都执行胜利后,子类中的 <client> 办法才能够被执行。

在一个类中既没有动态变量赋值操作也没有动态语句块时,编译器不会为该类生成 <client> 办法。

在产生以下几种状况时,JVM 不会执行类的初始化流程。

常量在编译时会将其常量值存入应用该常量的类的常量池中,该过程不须要调用常量所在的类,因而不会触发该常量类的初始化。

在子类援用父类的动态字段时,不会触发子类的初始化,只会触发父类的初始化。

定义对象数组,不会触发该类的初始化。

在应用类名获取 Class 对象时不会触发类的初始化。

在应用 Class.forName 加载指定的类时,能够通过 initialize 参数设置是否须要对类进行初始化。

在应用 ClassLoader 默认的 loadClass 办法加载类时不会触发该类的初始化。

类加载器

JVM 提供了 3 品种加载器,别离是启动类加载器、扩大类加载器和应用程序类加载器。

类加载器分为:启动类加载器、扩大类加载器、应用程序类加载器、自定义加载器。

启动类加载器:负责加载 Java_HOME/lib 目录中的类库,或通过 -Xbootclasspath 参数指定门路中被虚拟机认可的类库。

扩大类加载器:负责加载 Java_HOME/lib/ext 目录中的类库,或通过 java.ext.dirs 零碎变量加载指定门路中的类库。

应用程序类加载器:负责加载用户门路(classpath)上的类库。

除了上述 3 品种加载器,咱们也能够通过继承 java.lang.ClassLoader 实现自定义的类加载器。

双亲委派机制

JVM 通过双亲委派机制对类进行加载。

双亲委派机制指一个类在收到类加载申请后不会尝试本人加载这个类,而是把该类加载申请向上委派给其父类去实现,其父类在接管到该类加载申请后又会将其委派给本人的父类,以此类推,这样所有的类加载申请都被向上委派到启动类加载器中。

若父类加载器在接管到类加载申请后发现自己也无奈加载该类(通常起因是该类的 Class 文件在父类的类加载门路中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被胜利加载,若找不到该类,则 JVM 会抛出 ClassNotFoud 异样。

双亲委派类加载机制的类加载流程如下:

将自定义加载器挂载到应用程序类加载器。

应用程序类加载器将类加载申请委托给扩大类加载器。

扩大类加载器将类加载申请委托给启动类加载器。

启动类加载器在加载门路下查找并加载 Class 文件,如果未找到指标 Class 文件,则交由扩大类加载器加载。

扩大类加载器在加载门路下查找并加载 Class 文件,如果未找到指标 Class 文件,则交由应用程序类加载器加载。

应用程序类加载器在加载门路下查找并加载 Class 文件,如果未找到指标 Class 文件,则交由自定义加载器加载。

在自定义加载器下查找并加载用户指定目录下的 Class 文件,如果在自定义加载门路下未找到指标 Class 文件,则抛出 ClassNotFoud 异样。

双亲委派机制的外围是保障类的唯一性和安全性。

例如在加载 rt.jar 包中的 java.lang.Object 类时,无论是哪个类加载器加载这个类,最终都将类加载申请委托给启动类加载器加载,这样就保障了类加载的唯一性。

如果在 JVM 中存在包名和类名雷同的两个类,则该类将无奈被加载,JVM 也无奈实现类加载流程。

OSGI

OSGI(Open Service Gateway Initiative)是 Java 动态化模块化零碎的一系列标准,旨在为实现 Java 程序的模块化编程提供根底条件。

基于 OSGI 的程序能够实现模块级的热插拔性能,在程序降级更新时,能够只针对须要更新的程序进行停用和重新安装,极大进步了系统升级的安全性和便捷性。

OSGI 提供了一种面向服务的架构,该架构为组件提供了动静发现其余组件的性能,这样无论是退出组件还是卸载组件,都能被零碎的其余组件感知,以便各个组件之间能更好地协调工作。

OSGI 岂但定义了模块化开发的标准,还定义了实现这些标准所依赖的服务与架构,市场上也有成熟的框架对其进行实现和利用,但只有局部利用适宜采纳 OSGI 形式,因为它为了实现动静模块,不再遵循 JVM 类加载双亲委派机制和其余 JVM 标准,在安全性上有所就义。

正文完
 0