您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

java开发之JVM基础知识分享

时间:2022-10-22 14:29:46  来源:网易号  作者:IT爱好者小尚

虚拟机运行机制

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

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

Java程序具体运行过程:

 

  1. Java源文件被编译成字节码文件
  2. JVM将字节码文件通过JVM内部解释器编译成与操作系统对应的机器码
  3. 机器码调用相应操作系统的Native Method库执行相应方法

 

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线程的执行流程:

 

  1. 准备:完成JVM线程的本地存储、缓冲区分配、同步对象、栈、程序计数器等初始化工作。
  2. 创建:调用操作系统接口创建一个与之对应的原生线程。
  3. 调度:操作系统负责调度所有线程,并为其分配CPU时间片。
  4. 执行:在原生线程初始化完毕时,调用Java线程的 run()方法执行线程逻辑。
  5. 结束:在 run()方法逻辑执行完毕后,释放原生线程和Java线程所对应的资源。
  6. 回收:在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,采用复制算法实现,具体过程如下:

 

  1. 将Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,如果某对象的年龄达到老年代的标准,则将其复制到老年代,同时将这些对象年龄加1。
  • 对象晋升老年代的标准由 XX:MaxTenuringThreshold设置,默认为 15。
  • 如果ServivorTo区的内存空间不够,则也直接将其复制到老年代。
  • 如果对象属于大对象,也可直接将其复制到老年代。
  1. 清空Eden区和ServivorFrom区对象。
  2. 将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个步骤。

 

  1. 初始标记:只标记和GC Roots直接关联的对象,速度很快,需要暂停所有工作线程。
  2. 并发标记:和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程。
  3. 重新标记:在并发标记过程中用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正确性,需要对其重新标记并暂停工作线程。
  4. 并发清除:和用户线程一起工作,执行清除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会将常量池中的符号引用替换为直接引用。

初始化

初始化:初始化主要通过执行类构造器的

方法为类进行初始化。

方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的。

JVM规定,只有在父类的

方法都执行成功后,子类中的

方法才可以被执行。

在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成

方法。

在发生以下几种情况时,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异常。

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

 

  1. 将自定义加载器挂载到应用程序类加载器。
  2. 应用程序类加载器将类加载请求委托给扩展类加载器。
  3. 扩展类加载器将类加载请求委托给启动类加载器。
  4. 启动类加载器在加载路径下查找并加载 Class文件,如果未找到目标 Class文件,则交由扩展类加载器加载。
  5. 扩展类加载器在加载路径下查找并加载 Class文件,如果未找到目标 Class文件,则交由应用程序类加载器加载。
  6. 应用程序类加载器在加载路径下查找并加载 Class文件,如果未找到目标 Class文件,则交由自定义加载器加载。
  7. 在自定义加载器下查找并加载用户指定目录下的 Class文件,如果在自定义加载路径下未找到目标 Class文件,则抛出 ClassNotFoud异常。

 

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

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

如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程。

OSGI

OSGI(Open Service Gateway Initiative)是Java动态化模块化系统的一系列规范,旨在为实现Java程序的模块化编程提供基础条件。

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

OSGI提供了一种面向服务的架构,该架构为组件提供了动态发现其他组件的功能,这样无论是加入组件还是卸载组件,都能被系统的其他组件感知,以便各个组件之间能更好地协调工作。

OSGI不但定义了模块化开发的规范,还定义了实现这些规范所依赖的服务与架构,市场上也有成熟的框架对其进行实现和应用,但只有部分应用适合采用OSGI方式,因为它为了实现动态模块,不再遵循JVM类加载双亲委派机制和其他JVM规范,在安全性上有所牺牲。



Tags:JVM   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Graalvm 替代 JVM 真的可以带来巨大的性能优势吗?
介绍Spring Boot有助于轻松开发独立的、可用于生产的 Spring 应用程序。它对 Spring 平台和第三方库采用固执己见的方法:以最少的配置简化设置过程。优势: 易于使用:Spring Boo...【详细内容】
2023-12-25  Search: JVM  点击:(130)  评论:(0)  加入收藏
理解Java虚拟机(JVM):优化代码执行效率的内部机制
Java虚拟机(Java Virtual Machine,JVM)是Java程序运行的核心组件,它负责将Java源代码编译成字节码并执行。JVM具有内部机制来优化代码的执行效率,包括即时编译(Just-In-Time Compi...【详细内容】
2023-12-14  Search: JVM  点击:(220)  评论:(0)  加入收藏
深入了解Java的GC原理,掌握JVM 性能调优!
JVM性能调优是一个复杂的过程,需要结合具体的应用程序特性和需求来进行调优。不同的应用场景可能需要不同的调优策略。对于 Java 开发人员来说,进行程序的性能优化是很有挑战...【详细内容】
2023-12-12  Search: JVM  点击:(196)  评论:(0)  加入收藏
GC是什么?为什么要GC?JVM 垃圾回收算法有哪些?
Major GC 老年代区域的垃圾回收,老年代空间不足时,会先尝试触发Minor GC。Minor GC之后空间还不足,则会触发Major GC,Major GC速度比较慢,暂停时间长。图片1 Java垃圾回收机制(GC...【详细内容】
2023-12-07  Search: JVM  点击:(225)  评论:(0)  加入收藏
JVM由那些部分组成,运行流程是什么?
思考: JVM由那些部分组成,运行流程是什么?1.JVM由那些部分组成,运行流程是什么?JVM是什么好处:一次编写,到处运行自动内存管理,垃圾回收机制思考:JVM由哪些部分组成,运行流程是什么?...【详细内容】
2023-12-06  Search: JVM  点击:(205)  评论:(0)  加入收藏
JVM的调优常用参数
调优目的JVM调优的目的是为了提高Java应用程序的性能和稳定性。通过优化JVM的配置和参数设置,可以减少内存占用、提高垃圾回收效率、优化线程管理等,从而提升应用程序的响应速...【详细内容】
2023-11-10  Search: JVM  点击:(201)  评论:(0)  加入收藏
JVM 解释和编译指南
Java 是一种跨平台的编程语言。程序源代码会被编译为 字节码bytecode,然后字节码在运行时被转换为 机器码machine code。解释器interpreter 在物理机器上模拟出的抽象计算机...【详细内容】
2023-11-07  Search: JVM  点击:(366)  评论:(0)  加入收藏
深入理解并发编程艺术之JVM内存模型
java内存模型由来我们知道不同的计算机硬件和操作系统的,所遵循的规范以及计算机内存模型是有区别的,也就意味着我们开发的程序放在某个计算机硬件和操作系统上运行是正常的,而...【详细内容】
2023-10-27  Search: JVM  点击:(433)  评论:(0)  加入收藏
OOM异常会导致JVM退出吗?
熟悉Java开发的人,应该会经常遇到的异常:OOM,那么这个异常会导致 JVM 虚拟机退出吗?结论Java虚拟机(JVM)在运行Java应用时,可能会遇到内存不足的情况,从而抛出OutOfMemoryError(OOM)。...【详细内容】
2023-10-13  Search: JVM  点击:(243)  评论:(0)  加入收藏
JVM是如何判定对象已死的?学JVM必会的知识!
大家好,我是 BookSea。作为一名Java程序员,我们每天都在程序里不停地去new对象,但是你知道这些被new出来的对象,最后是怎么被回收的吗?在堆里面存放着Java世界中几乎所有的对象实...【详细内容】
2023-10-08  Search: JVM  点击:(333)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(15)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(27)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(57)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(70)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(73)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(90)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(107)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(99)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(78)  评论:(0)  加入收藏
站内最新
站内热门
站内头条