JVM

目录

1. JVM的运行机制

2. 多线程

3. JVM的内存区域

4. JVM的运行时内存

新生代: Eden区、ServivorTo区和ServivorFrom区

老年代

永久代

5. 垃圾回收算法

如何回收算法?

 Java中常用的垃圾回收算法

6. Java中的4种引用类

7. 分代收集算法和分区收集算法

分代收集算法

分区收集算法

8. 垃圾收集器

9. JVM的类加载机制

JVM的类加载阶段

类加载器

双亲委派机制

OSGI


1. JVM的运行机制

JVM是用于运行Java字节码的虚拟机,包括一套字节码指令集一组程序寄存器、一个虚拟机栈、一个虚拟机堆,一个方法区和一个垃圾回收器。JVM运行在操作系统上,不予硬件设备直接交互。

Java源文件在通过编译器之后被编译成相应的.Class文件(字节码 文件), class文件又被JVM中的解释器编译成机器码在不同的操作系统(Windows、Linux、 Mac)上运行。每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台的原因。在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据。

  • Java程序的具体运行过程如下:
  • Java源文件被编译器编译成字节码文件
  • JVM将字节码文件编译成相应操作系统的机器码
  • 机器码调用相应操作系统的本地方法库执行相应的方法。

Java虚拟机包括一个类加载器子系统(Class LoaderSubSystem)、运行时数据区(Runtime Data Area)、执行引擎和本地接口库。本地接口库通过本地方法库与操作提供交互。

  • 类加载器子系统用于将编译好的.class文件加载到JVM中
  • 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器、方法区、本地方法区、虚拟机栈和虚拟机堆
  • 执行引擎包括即时编译器和垃圾回收器,即时编译器用于将Java字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象。
  • 本地方法库用于调用操作系统的本地方法库完成具体的指令操作。

2. 多线程

在多核操作系统上,JVM运行在一个进程内同时并发执行多个线程。JVM中的线程和操作系统中的线程是相互对应的,在JVM线程中的本地存储、缓冲区分配、同步对象、栈、程序计数器等准备工作都完成时,JVM会调用操作系统的接口创建一个与之对应的原生线程;在JVM线程运行结束时,原生线程随之会被回收。操作系统负责调度所有线程,并为其分配CPU时间片,在原生线程初始化完毕后,就会调用Java线程的run()执行该线程;在线程结束时,会释放原生线程和Java线程所对应的资源。

在JVM后台运行的线程主要有:

  • 虚拟机线程: 虚拟机线程在JVM到达安全点时出现
  • 周期性任务线程:通过定时器调度线程来实现周期性操作的执行
  • GC线程:GC线程支持JVM中不同的垃圾回收活动
  • 编译器线程:编译器线程在运行时将字节码动态编译成本地机器码,是JVM跨平台的具体实现
  • 信号分发线程:接收发送到JVM的信号并调用JVM方法。

3. JVM的内存区域

  • 线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。在JVM中,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存在与否和本地线程的启动和销毁对应。
  • 线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁
  • 直接内存也叫堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。JDK的NIO模块提供基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的。NIO模块通过调用Native函数直接在操作系统上分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程可以通过堆外内存计数避免在Java堆和Native堆中来回复制数据带来的资源占用和性能消耗,因此堆外内存在高并发应用场景下被广泛使用(Netty,Flink,HBase,Hadoop都有用到堆外内存)。

(1)程序计数器: 线程私有,无内存溢出问题(唯一没有Out Of Memory的区域)。

存储当前运行的线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,记录的是实时虚拟机字节码指令的地址;如果该方法的是Native方法,则程序技术器的值为空(Undefined)

(2)虚拟机栈

描述Java方法的执行过程,它在当前栈帧(Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未被捕获的异常),都视为方法运行结束,束。下图展示了线程运行及栈帧变化的过程。线程1在CPU1上运行,线程2在CPU2上运行,在CPU资源不够时其他线程将处于等待状态(如图中等待的线程N),等待获取CPU时间片。而在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态。

(3)本地方法栈:线程私有

本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务

(4)堆:运行时数据区

在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域。由于现代JMV采用分代收集算法,因此Java堆从GC的角度好可以细分为新生代,老年代,永久代(方法区)。

(5) 方法区 线程共享

用于存储常量,静态变量,类信息,即使编译器编译后的机器码,运行时常量池等数据。

 JVM可以像管理Java堆一样管理这块区域,主要针对的是常量池和回收和类的卸载,可回收的对象很少。

4. JVM的运行时内存

新生代: Eden区、ServivorTo区和ServivorFrom区

JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden区、ServivorTo区和ServivorFrom区,如小所述:

  • (1) Eden区:Java新创建的对象首先会被存放到Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可通过XX:pretenureSizeThreshold设置其大小。在Eden区的内存不足时会触发MinorGC,对新生代进行一次垃圾回收。
  • (2) ServivorTo区:保留上一次MinorGC时的幸存者
  • (3)ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者。

新生代的GC过程叫作MinorGC,采用复制算法实现,具体过程如下。

  • (1)在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由XX:MaxTenuringThreshold设置,默认为15),将其复制到老年代,同时把这些对象的年龄加1;如果ServivorTo区内存空间不够,则也直接将其复制到老年代;如果对象属于大对象,则也直接复制到老年代。
  • (2) 清空Eden区和ServivorFrom区中的对象
  • (3)将ServivorTo区与ServivorFrom区互换,ServivorTo区成为下一次GC时的ServivorFrom区。(这里没有再复制啊!!!!)

老年代

主要存放有长生命周期的对象大对象。 老年代的GC叫作MajorGC。在老年代,对象比较稳定,MajorGC不会被频繁触发。在MajorGC前, JVM会进行一次MinorGC,在MinorGC后出现老年代空间不足或无法找到足够大的空间来存放老年对象时,会触发MajorGc进程来进行垃圾回收,释放JVM的内存空间。

MajorGC采用标记清除算法,该算法会先扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out Of Memory异常。

永久代

指内存的永久保存区域,主要存放Class和元数据信息(Meta data). Class类在加载时被放入永久代。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常,比如Tomcat引用Jar文件过多导致JVM内存不足无法启动。

需要注意的是,在Java8(只针对hotSpot)中永久代被元数据区(元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区没有使用虚拟机的内存,而是直接使用操作系统的本地内存,因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。

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

5. 垃圾回收算法

如何回收算法?

Java采用引用计数法和可达性分析来确定对象是否应该被回收,其中, 引用计数法容易导致循环引用的问题,可达性分析通过根搜索算法(GC Roots Tracing)来实现。根搜索算法以一系列GC Roots的点作为起点向下搜索,在一个对象到任何的GC Roots都没有引用链相连时,说明其已经死亡。根搜索算法主要对栈中的引用,方法区中的静态引用和JNO中的引用展开分析。

 Java中常用的垃圾回收算法

(1)  标记清除(效率低,内存碎片)

标记阶段标记所有需要回收的对象,清除阶段清除可回收的对象并释放其所占用的内存空间。

(2)复制

为了解决标记清除算法内存碎片化的问题而设计的。复制算法首先将内存划分为两块大小相等的内存区域,复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费,同时,在系统中有大量长时间存活的对象,这些对象在内存区域1和内存区域之间来回复制而影响系统的运行效率。因此, 该算法只在对象为"朝生夕死"状态时运行效率高。

(3) 标记整理

结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存。

(4) 分代收集

无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法。

分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收都有大量的对象被回收;新生代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM根据不同的区域对象的特点选择了不同的算法。

目前,大部分JVM在

  • 新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全、高效地回收新生代大量的短生命周期的对象并释放内存。
  • 老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法。
  • 在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class类、常量、方法描述等。在永久代主要回收废弃的常量和无用的类

6. Java中的4种引用类

在Java中一切皆对象,对象的操作是通过该对象的引用(Reference)实现的,Java中的引用类型有4种,分别为强引用、软引用、弱引用和虚引

  • (1) 强引用: 在Java中最常见的就是强引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。 有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因。
  • (2) 软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收。
  • (3) 弱引用: 通过WeakReference类来实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被实现
  • (4) 虚引用: 虚引用通过PhantomReference类实现,虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。

7. 分代收集算法和分区收集算法

分代收集算法

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

1.新生代与复制算法

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

2. 老年代与标记清除

老年代主要存放长生命周期的对象和大对象,可回收的对象一般较少,因此JVM采用标记清除算法进行垃圾回收,直接释放死亡状态的对象所占用的内存空间即可。

分区收集算法

分区算法将整个堆空间划分为连续的大小不同的小区域,对每个小区域都单独进行内存使用和垃圾回收,这样做的好处是可以根据每个小区域内存的大小灵活使用和释放内存。

分区收集算法可以根据系统可接受的停顿时间,每次都快速回收若干个小区域的内存,以缩短垃圾回收时系统停顿的时间,最后以多次并行累加的方式逐步完成整个内存区域的垃圾回收。如果垃圾回收机制一次回收整个堆内存,则需要更长的系统停顿时间,长时间的系统停顿时间将影响系统运行的稳定性。

8. 垃圾收集器

9. JVM的类加载机制

JVM的类加载阶段

JVM的类加载分为5个阶段:加载、验证、准备、解析、初始化。在类初始化完成后就可以使用该类的信息,在一个类不再被需要时可以从JVM中卸载,如图1-20所示。

(1)加载

指JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang.Class对象,并封装类在方法区的数据结构的过程,在读取Class文件时既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过代理自动生成Class或其他方式。

(2) 验证

主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载

(3)准备

工作是在方法区中为类变量分配内存空间并设置类中变量的初始值。初始值指不同数据类型的默认值,这里需要注意final类型的变量非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。

(4) 初始化

主要通过执行类构造器的<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异常

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

  • (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 规范,在安全性上有所牺牲。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值