JVM原理学习笔记总结

1.1 JVM的运行机制

JVM Java Virtual Machine )是用于运行 Java 字节码的虚拟机,包括一
套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、
一个方法区和一个垃圾回收器。 JVM 运行在操作系统之上,不与硬件
设备直接交互。
Java 源文件在通过编译器之后被编译成相应的 .Class 文件(字节码文
件), .Class 文件又被 JVM 中的解释器编译成机器码在不同的操作系
统( Windows Linux Mac )上运行。每种操作系统的解释器都是不
同的,但基于解释器实现的虚拟机是相同的,这也是 Java 能够跨平台
的原因。在一个 Java 进程开始运行后,虚拟机就开始实例化了,有多
个进程启动就会实例化多个虚拟机实例。进程退出或者关闭,则虚拟
机实例消亡,在多个虚拟机实例之间不能共享数据。
Java 程序的具体运行过程如下。
1 Java 源文件被编译器编译成字节码文件。
2 JVM 将字节码文件编译成相应操作系统的机器码。
3 )机器码调用相应操作系统的本地方法库执行相应的方法。
Java 虚拟机包括一个类加载器子系统( Class Loader SubSystem )、运
行时数据区( Runtime Data Area )、执行引擎和本地接口库( Native
Interface Library )。本地接口库通过调用本地方法库( Native Method
Library)与操作系统交互
类加载器子系统用于将编译好的 .Class 文件加载到 JVM 中;
运行时数据区用于存储在 JVM 运行过程中产生的数据,包括程序计
数器、方法区、本地方法区、虚拟机栈和虚拟机堆;
执行引擎包括即时编译器和垃圾回收器,即时编译器用于将 Java
节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使
用的对象;
本地接口库用于调用操作系统的本地方法库完成具体的指令操作。
1.2 多线程
在多核操作系统上, JVM 允许在一个进程内同时并发执行多个线程。
JVM 中的线程与操作系统中的线程是相互对应的,在 JVM 线程的本地
存储、缓冲区分配、同步对象、栈、程序计数器等准备工作都完成
时, JVM 会调用操作系统的接口创建一个与之对应的原生线程;在
JVM 线程运行结束时,原生线程随之被回收。操作系统负责调度所有
线程,并为其分配 CPU 时间片,在原生线程初始化完毕时,就会调用
Java 线程的 run() 执行该线程;在线程结束时,会释放原生线程和 Java
线程所对应的资源。
JVM 后台运行的线程主要有以下几个。
虚拟机线程( JVMThread ):虚拟机线程在 JVM 到达安全点
SafePoint )时出现。
周期性任务线程:通过定时器调度线程来实现周期性操作的执行。
GC 线程: GC 线程支持 JVM 中不同的垃圾回收活动。 编译器线程:编译器线程在运行时将字节码动态编译成本地平台机
器码,是 JVM 跨平台的具体实现。
信号分发线程:接收发送到 JVM 的信号并调用 JVM 方法。
1.3 JVM 的内存区域
JVM 的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方
法区)、线程共享区域(堆、方法区)和直接内存
线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程
的结束而销毁。在 JVM 内,每个线程都与操作系统的本地线程直接映
射,因此这部分内存区域的存在与否和本地线程的启动和销毁对应。
线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。
直接内存也叫作堆外内存,它并不是 JVM 运行时数据区的一部分,但
在并发编程中被频繁使用。 JDK NIO 模块提供的基于 Channel
Buffer I/O 操作方式就是基于堆外内存实现的, NIO 模块通过调用
Native 函数库直接在操作系统上分配堆外内存,然后使用
DirectByteBuffer 对象作为这块内存的引用对内存进行操作, Java 进程
可以通过堆外内存技术避免在 Java 堆和 Native 堆中来回复制数据带来
的资源占用和性能消耗,因此堆外内存在高并发应用场景下被广泛使
用( Netty Flink HBase Hadoop 都有用到堆外内存)。
1.3.1 程序计数器:线程私有,无内存溢出问题
程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行
的字节码的行号指示器。每个运行中的线程都有一个独立的程序计数
器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字 节码指令的地址;如果该方法执行的是 Native 方法,则程序计数器的
值为空( Undefined )。
程序计数器属于 线程私有 的内存区域,它是唯一没有 Out Of
Memory (内存溢出)的区域。
1.3.2 虚拟机栈:线程私有,描述 Java 方法的执行过程
虚拟机栈是描述 Java 方法的执行过程的内存模型,它在当前栈帧
Stack Frame )中存储了局部变量表、操作数栈、动态链接、方法出
口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理
动态链接( Dynamic Linking )方法的返回值和异常分派( Dispatch
Exception )。
栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一
个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈
和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未
被捕获的异常),都视为方法运行结束。线程 1 CPU1 上运行,线程 2 CPU2 上运行,在 CPU
资源不够时其他线程将处于等待状态
等待获取 CPU 时间片。而在线程内部,每个方法的执行和返回都对应
一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活
动状态。
1.3.3 本地方法区:线程私有
本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行 Java 方法
服务,本地方法栈为 Native 方法服务。
1.3.4 堆:也叫作运行时数据区,线程共享 JVM 运行过程中创建的对象和产生的数据都被存储在堆中,堆是被
线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存
区域。由于现代 JVM 采用分代收集算法 ,因此 Java 堆从 GC Garbage
Collection ,垃圾回收)的角度还可以细分为:新生代、老年代和永久
代。
1.3.5 方法区:线程共享
方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编
译器编译后的机器码、运行时常量池等数据
JVM GC 分代收集扩展至方法区,即使用 Java 堆的永久代来实现方法
区,这样 JVM 的垃圾收集器就可以像管理 Java 堆一样管理这部分内
存。永久带的内存回收主要针对常量池的回收和类的卸载,因此可回
收的对象很少。
常量被存储在运行时常量池( Runtime Constant Pool )中,是方法区的
一部分。静态变量也属于方法区的一部分。在类信息( Class 文件)中
不但保存了类的版本、字段、方法、接口等描述信息,还保存了常量
信息。
在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在
方法区的运行时常量池中。 Java 虚拟机对 Class 文件每一部分的格式都
有明确的规定,只有符合 JVM 规范的 Class 文件才能通过虚拟机的检
查,然后被装载、执行。
1.4 JVM 的运行时内存
JVM 的运行时内存也叫作 JVM 堆,从 GC 的角度可以将 JVM 堆分为新生
代、老年代和永久代。其中新生代默认占 1/3 堆空间,老年代默认占
2/3 堆空间,永久代占非常少的堆空间。新生代又分为 Eden 区、 ServivorFrom 区和 ServivorTo 区, Eden 区默认占 8/10 新生代空间,
ServivorFrom 区和 ServivorTo 区默认分别占 1/10新生代空间
1.4.1 新生代: 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 区的内存空间不够,
则也直接将其复制到老年代;如果对象属于大对象(大小为 2KB
128KB 的对象属于大对象,例如通过
XX:PretenureSizeThreshold=2097152 设置大对象为 2MB 1024×1024×2Byte=2097152Byte=2MB ),则也直接将其复制到老年
代。
2 )清空 Eden 区和 ServivorFrom 区中的对象。
3 )将 ServivorTo 区和 ServivorFrom 区互换,原来的 ServivorTo 区成为
下一次 GC 时的 ServivorFrom 区。
1.4.2 老年代
老年代主要存放有长生命周期的对象和大对象。老年代的 GC 过程叫作
MajorGC 。在老年代,对象比较稳定, MajorGC 不会被频繁触发。在
进行 MajorGC 前, JVM 会进行一次 MinorGC ,在 MinorGC 过后仍然出
现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对
象时,会触发 MajorGC 进行垃圾回收,释放 JVM 的内存空间。
MajorGC 采用标记清除算法,该算法首先会扫描所有对象并标记存活
的对象,然后回收未被标记的对象,并释放内存空间。
因为要先扫描老年代的所有对象再回收,所以 MajorGC 的耗时较长。
MajorGC 的标记清除算法容易产生内存碎片。在老年代没有内存空间
可分配时,会抛出 Out Of Memory 异常。
1.4.3 永久代
永久代指内存的永久保存区域,主要存放 Class Meta (元数据)的信
息。 Class 在类加载时被放入永久代。永久代和老年代、新生代不同,
GC 不会在程序运行期间对永久代的内存进行清理,这也导致了永久代
的内存会随着加载的 Class 文件的增加而增加,在加载的 Class 文件过多
时会抛出 Out Of Memory 异常,比如 Tomcat 引用 Jar 文件过多导致 JVM
内存不足而无法启动。
需要注意的是,在 Java 8 中永久代已经被元数据区(也叫作元空间)
取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据 区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因
此,元空间的大小不受 JVM 内存的限制,只和操作系统的内存有关。
Java 8 中, JVM 将类的元数据放入本地内存( Native Memory )中,
将常量池和类的静态变量放入 Java 堆中,这样 JVM 能够加载多少元数
据信息就不再由 JVM 的最大可用内存( MaxPermSize )空间决定,而
由操作系统的实际可用内存空间决定。
1.5 垃圾回收与算法
1.5.1 如何确定垃圾
Java 采用引用计数法和可达性分析来确定对象是否应该被回收,其
中,引用计数法容易产生循环引用的问题,可达性分析通过根搜索算
法( GC Roots Tracing )来实现。根搜索算法以一系列 GC Roots 的点作
为起点向下搜索,在一个对象到任何 GC Roots 都没有引用链相连时,
说明其已经死亡。根搜索算法主要针对栈中的引用、方法区中的静态
引用和 JNI中的引用展开分析
1. 引用计数法
Java 中如果要操作对象,就必须先获取该对象的引用,因此可以通
过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引
用时,引用计数加 1 ;在为对象删除一个引用时,引进计数减 1 ;如
果一个对象的引用计数为 0 ,则表示此刻该对象没有被引用,可以被
回收。
引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,
导致它们的引用一直存在,而不能被回收
2. 可达性分析
为了解决引用计数法的循环引用问题, Java 还采用了可达性分析来判
断对象是否可以被回收。具体做法是首先定义一些 GC Roots 对象,然
后以这些 GC Roots 对象作为起点向下搜索,如果在 GC roots 和一个对
象之间没有可达路径,则称该对象是不可达的。不可达对象要经过至
少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍
然是不可达的,则将被垃圾收集器回收。
1.5.2 Java 中常用的垃圾回收算法
Java 中常用的垃圾回收算法有标记清除( Mark-Sweep )、复制
Copying )、标记整理( Mark-Compact )和分代收集( Generational
Collecting )这 4种垃圾回收算法
1. 标记清除算法
标记清除算法是基础的垃圾回收算法,其过程分为标记和清除两个阶
段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的
对象并释放其所占用的内存空间
由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可
用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内
存碎片化的问题,继而引起大对象无法获得连续可用空间的问题。
2. 复制算法 复制算法是为了解决标记清除算法内存碎片化的问题而设计的。复制
算法首先将内存划分为两块大小相等的内存区域,即区域 1 和区域
2 ,新生成的对象都被存放在区域 1 中,在区域 1 内的对象存储满后会
对区域 1 进行一次标记,并将标记后仍然存活的对象全部复制到区域
2 中,这时区域 1 将不存在任何存活的对象,直接清理整个区域 1 的内
存即可
复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内
存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量
的内存浪费。同时,在系统中有大量长时间存活的对象时,这些对象
将在内存区域 1 和内存区域 2 之间来回复制而影响系统的运行效率。
因此,该算法只在对象为 朝生夕死 状态时运行效率较高。
3. 标记整理算法
标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和
标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存
的另一端,然后清除该端的对象并释放内存
4. 分代收集算法
无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类
型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾
回收。因此,针对不同的对象类型, 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 时,将被移到
老年代。 1.6 Java 中的 4 种引用类型
Java 中一切皆对象,对象的操作是通过该对象的引用( Reference
实现的, Java 中的引用类型有 4 种,分别为强引用、软引用、弱引用和
虚引用
1 )强引用:在 Java 中最常见的就是强引用。在把一个对象赋给一个
引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为
可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成
Java 内存泄漏( Memory Link )的主要原因。
2 )软引用:软引用通过 SoftReference 类实现。如果一个对象只有软
引用,则在系统内存空间不足时该对象将被回收。
3 )弱引用:弱引用通过 WeakReference 类实现,如果一个对象只有
弱引用,则在垃圾回收过程中一定会被回收。
4 )虚引用:虚引用通过 PhantomReference 类实现,虚引用和引用队
列联合使用,主要用于跟踪对象的垃圾回收状态。
1.7 分代收集算法和分区收集算法
1.7.1 分代收集算法
JVM 根据对象存活周期的不同将内存划分为新生代、老年代和永久
代,并根据各年代的特点分别采用不同的 GC 算法。
1. 新生代与复制算法
新生代主要存储短生命周期的对象,因此在垃圾回收的标记阶段会标
记大量已死亡的对象及少量存活的对象,因此只需选用复制算法将少 量存活的对象复制到内存的另一端并清理原区域的内存即可。
2. 老年代与标记整理算法
老年代主要存放长生命周期的对象和大对象,可回收的对象一般较
少,因此 JVM 采用标记整理算法进行垃圾回收,直接释放死亡状态的
对象所占用的内存空间即可。
1.7.2 分区收集算法
分区算法将整个堆空间划分为连续的大小不同的小区域,对每个小区
域都单独进行内存使用和垃圾回收,这样做的好处是可以根据每个小
区域内存的大小灵活使用和释放内存。
分区收集算法可以根据系统可接受的停顿时间,每次都快速回收若干
个小区域的内存,以缩短垃圾回收时系统停顿的时间,最后以多次并
行累加的方式逐步完成整个内存区域的垃圾回收。如果垃圾回收机制
一次回收整个堆内存,则需要更长的系统停顿时间,长时间的系统停
顿将影响系统运行的稳定性。
1.8 垃圾收集器
Java 堆内存分为新生代和老年代:新生代主要存储短生命周期的对
象,适合使用复制算法进行垃圾回收;老年代主要存储长生命周期的
对象,适合使用标记整理算法进行垃圾回收。因此, JVM 针对新生代
和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾
收集器有 Serial ParNew Parallel Scavenge ,针对老年代提供的垃圾
收集器有 Serial Old Parallel Old CMS ,还有针对不同区域的 G1 分区
收集算法
1.8.1 Serial 垃圾收集器:单线程,复制算法
Serial 垃圾收集器基于复制算法实现,它是一个单线程收集器,在它正
在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结
束。
Serial 垃圾收集器采用了复制算法,简单、高效,对于单 CPU 运行环境
来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因
Serial 垃圾收集器是 Java 虚拟机运行在 Client 模式下的新生代的默认
垃圾收集器。
1.8.2 ParNew 垃圾收集器:多线程,复制算法
ParNew 垃圾收集器是 Serial 垃圾收集器的多线程实现,同样采用了复
制算法,它采用多线程模式工作,除此之外和 Serial 收集器几乎一样。
ParNew 垃圾收集器在垃圾收集过程中会暂停所有其他工作线程,是
Java 虚拟机运行在 Server 模式下的新生代的默认垃圾收集器。
ParNew 垃圾收集器默认开启与 CPU 同等数量的线程进行垃圾回收,在
Java 应用启动时可通过 -XX:ParallelGCThreads 参数调节 ParNew 垃圾收
集器的工作线程数。
1.8.3 Parallel Scavenge 垃圾收集器:多线程,复制算法
Parallel Scavenge 收集器是为提高新生代垃圾收集效率而设计的垃圾收
集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可
以更高效地利用 CPU 尽快完成垃圾回收任务。
Parallel Scavenge 通过自适应调节策略提高系统吞吐量,提供了三个参
数用于调节、控制垃圾回收的停顿时间及吞吐量,分别是控制最大垃
圾收集停顿时间的 -XX:MaxGCPauseMillis 参数,控制吞吐量大小的 -
XX:GCTimeRatio 参数和控制自适应调节策略开启与否的
UseAdaptiveSizePolicy 参数。 1.8.4 Serial Old 垃圾收集器:单线程,标记整理算法
Serial Old 垃圾收集器是 Serial 垃圾收集器的老年代实现,同 Serial 一样
采用单线程执行,不同的是, Serial Old 针对老年代长生命周期的特点
基于标记整理算法实现。 Serial Old 垃圾收集器是 JVM 运行在 Client
式下的老年代的默认垃圾收集器。
新生代的 Serial 垃圾收集器和老年代的 Serial Old 垃圾收集器可搭配使
用,分别针对 JVM的新生代和老年代进行垃圾回收。在新生代采用 Serial 垃圾收集器基于复制算法进行垃
圾回收,未被其回收的对象在老年代被 Serial Old 垃圾收集器基于标记
整理算法进行垃圾回收。
1.8.5 Parallel Old 垃圾收集器:多线程,标记整理算法
Parallel Old 垃圾收集器采用多线程并发进行垃圾回收,它根据老年代
长生命周期的特点,基于多线程的标记整理算法实现。 Parallel Old
圾收集器在设计上优先考虑系统吞吐量,其次考虑停顿时间等因素,
如果系统对吞吐量的要求较高,则可以优先考虑新生代的 Parallel
Scavenge 垃圾收集器和老年代的 Parallel Old 垃圾收集器的配合使用。
新生代的 Parallel Scavenge 垃圾收集器和老年代的 Parallel Old 垃圾收集
器的搭配运行过程 。新生代基于 Parallel Scavenge 垃圾收
集器的复制算法进行垃圾回收,老年代基于 Parallel Old 垃圾收集器的
标记整理算法进行垃圾回收。
1.8.6 CMS 垃圾收集器 CMS Concurrent Mark Sweep )垃圾收集器是为老年代设计的垃圾收
集器,其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记
清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间
提高系统的稳定性。
CMS 的工作机制相对复杂,垃圾回收过程包含如下 4 个步骤。
1 )初始标记:只标记和 GC Roots 直接关联的对象,速度很快,需
要暂停所有工作线程。
2 )并发标记:和用户线程一起工作,执行 GC Roots 跟踪标记过
程,不需要暂停工作线程。
3 )重新标记:在并发标记过程中用户线程继续运行,导致在垃圾
回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正
确性,需要对其重新标记并暂停工作线程。
4 )并发清除:和用户线程一起工作,执行清除 GC Roots 不可达对
象的任务,不需要暂停工作线程。
CMS 垃圾收集器在和用户线程一起工作时(并发标记和并发清除)不
需要暂停用户线程,有效缩短了垃圾回收时系统的停顿时间,同时由
CMS 垃圾收集器和用户线程一起工作,因此其并行度和效率也有很
大提升
1.8.7 G1 垃圾收集器
G1 Garbage First )垃圾收集器为了避免全区域垃圾收集引起的系统
停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域
的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个
优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,
优先回收垃圾最多的区域。 G1 垃圾收集器通过内存区域独立划分使用 和根据不同优先级回收各区域垃圾的机制,确保了 G1 垃圾收集器在有
限时间内获得最高的垃圾收集效率。相对于 CMS 收集器, G1 垃圾收集
器两个突出的改进。
基于标记整理算法,不产生内存碎片。
可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃
圾回收。
1.9 Java 网络编程模型
1.9.1 阻塞 I/O 模型
阻塞 I/O 模型是常见的 I/O 模型,在读写数据时客户端会发生阻塞。阻
I/O 模型的工作流程为:在用户线程发出 I/O 请求之后,内核会检查
数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;在内存数
据就绪后,内核将数据复制到用户线程中,并返回 I/O 执行结果到用户
线程,此时用户线程将解除阻塞状态并开始处理数据。典型的阻塞 I/O
模型的例子为 data = socket.read() ,如果内核数据没有就绪, Socket 线
程就会一直阻塞在 read() 中等待内核数据就绪。
1.9.2 非阻塞 I/O 模型
非阻塞 I/O 模型指用户线程在发起一个 I/O 操作后,无须阻塞便可以马
上得到内核返回的一个结果。如果内核返回的结果为 false ,则表示内
核数据还没准备好,需要稍后再发起 I/O 操作。一旦内核中的数据准备
好了,并且再次收到用户线程的请求,内核就会立刻将数据复制到用
户线程中并将复制的结果通知用户线程。
在非阻塞 I/O 模型中,用户线程需要不断询问内核数据是否就绪,在内
存数据还未就绪时,用户线程可以处理其他任务,在内核数据就绪后
可立即获取数据并进行相应的操作。典型的非阻塞 I/O 模型一般如下: 1.9.3 多路复用 I/O 模型
多路复用 I/O 模型是多线程并发编程用得较多的模型, Java NIO 就是基
于多路复用 I/O 模型实现的。在多路复用 I/O 模型中会有一个被称为
Selector 的线程不断轮询多个 Socket 的状态,只有在 Socket 有读写事件
时,才会通知用户线程进行 I/O 读写操作。
因为在多路复用 I/O 模型中只需一个线程就可以管理多个 Socket (阻塞
I/O 模型和非阻塞 1/O 模型需要为每个 Socket 都建立一个单独的线程处
理该 Socket 上的数据),并且在真正有 Socket 读写事件时才会使用操
作系统的 I/O 资源,大大节约了系统资源。
Java NIO 在用户的每个线程中都通过 selector.select() 查询当前通道是否
有事件到达,如果没有,则用户线程会一直阻塞。而多路复用 I/O 模型
通过一个线程管理多个 Socket 通道,在 Socket 有读写事件触发时才会
通知用户线程进行 I/O 读写操作。因此,多路复用 I/O 模型在连接数众
多且消息体不大的情况下有很大的优势。尤其在物联网领域比如车载
设备实时位置、智能家电状态等定时上报状态且字节数较少的情况下
优势更加明显,一般一个经过优化后的 16 32GB 服务器能承载约 10
台设备连接。
非阻塞 I/O 模型在每个用户线程中都进行 Socket 状态检查,而在多路复
I/O 模型中是在系统内核中进行 Socket 状态检查的,这也是多路复用
I/O 模型比非阻塞 I/O 模型效率高的原因。
多路复用 I/O 模型通过在一个 Selector 线程上以轮询方式检测在多个
Socket 上是否有事件到达,并逐个进行事件处理和响应。因此,对于
多路复用 I/O 模型来说,在事件响应体(消息体)很大时, Selector 线
程就会成为性能瓶颈,导致后续的事件迟迟得不到处理,影响下一轮
的事件轮询。在实际应用中,在多路复用方法体内一般不建议做复杂
逻辑运算,只做数据的接收和转发,将具体的业务操作转发给后面的
业务线程处理。
1.9.4 信号驱动 I/O 模型 在信号驱动 I/O 模型中,在用户线程发起一个 I/O 请求操作时,系统会
为该请求对应的 Socket 注册一个信号函数,然后用户线程可以继续执
行其他业务逻辑;在内核数据就绪时,系统会发送一个信号到用户线
程,用户线程在接收到该信号后,会在信号函数中调用对应的 I/O 读写
操作完成实际的 I/O 请求操作。
1.9.5 异步 I/O 模型
在异步 I/O 模型中,用户线程会发起一个 asynchronous read 操作到内
核,内核在接收到 synchronous read 请求后会立刻返回一个状态,来说
明请求是否成功发起,在此过程中用户线程不会发生任何阻塞。接
着,内核会等待数据准备完成并将数据复制到用户线程中,在数据复
制完成后内核会发送一个信号到用户线程,通知用户线程
asynchronous 读操作已完成。在异步 I/O 模型中,用户线程不需要关心
整个 I/O 操作是如何进行的,只需发起一个请求,在接收到内核返回的
成功或失败信号时说明 I/O 操作已经完成,直接使用数据即可。
在异步 I/O 模型中, I/O 操作的两个阶段(请求的发起、数据的读取)
都是在内核中自动完成的,最终发送一个信号告知用户线程 I/O 操作已
经完成,用户直接使用内存写好的数据即可,不需要再次调用 I/O 函数
进行具体的读写操作,因此在整个过程中用户线程不会发生阻塞。
在信号驱动模型中,用户线程接收到信号便表示数据已经就绪,需要
用户线程调用 I/O 函数进行实际的 I/O 读写操作,将数据读取到用户线
程;而在异步 I/O 模型中,用户线程接收到信号便表示 I/O 操作已经完
成(数据已经被复制到用户线程),用户可以开始使用该数据了。
异步 I/O 需要操作系统的底层支持,在 Java 7 中提供了 Asynchronous I/O
操作。
1.9.6 Java I/O
在整个 Java.io 包中最重要的是 5 个类和 1 个接口。 5 个类指的是 File
OutputStream InputStream Writer Reader
Version:0.9 StartHTML:0000000105 EndHTML:0000072564 StartFragment:0000000141 EndFragment:0000072524
1.9.7 Java NIO
Java NIO 的实现主要涉及三大核心内容: Selector (选择器)、
Channel (通道)和 Buffer (缓冲区)。 Selector 用于监听多个 Channel
的事件,比如连接打开或数据到达,因此,一个线程可以实现对多个
数据 Channel 的管理。传统 I/O 基于数据流进行 I/O 读写操作;而 Java
NIO 基于 Channel Buffer 进行 I/O 读写操作,并且数据总是被从 Channel
读取到 Buffer 中,或者从 Buffer 写入 Channel 中。
Java NIO 和传统 I/O 的最大区别如下。
1 I/O 是面向流的, NIO 是面向缓冲区的:在面向流的操作中,数
据只能在一个流中连续进行读写,数据没有缓冲,因此字节流无法前
后移动。而在 NIO 中每次都是将数据从一个 Channel 读取到一个 Buffer
中,再从 Buffer 写入 Channel 中,因此可以方便地在缓冲区中进行数据
的前后移动等操作。该功能在应用层主要用于数据的粘包、拆包等操
作,在网络不可靠的环境下尤为重要。
2 )传统 I/O 的流操作是阻塞模式的, NIO 的流操作是非阻塞模式
的。在传统 I/O 下,用户线程在调用 read() write() 进行 I/O 读写操作
时,该线程将一直被阻塞,直到数据被读取或数据完全写入。 NIO
Selector 监听 Channel 上事件的变化,在 Channel 上有数据发生变化时
通知该线程进行读写操作。对于读请求而言,在通道上有可用的数据
时,线程将进行 Buffer 的读操作,在没有数据时,线程可以执行其他
业务逻辑操作。对于写操作而言,在使用一个线程执行写操作将一些
数据写入某通道时,只需将 Channel 上的数据异步写入 Buffer 即可,
Buffer 上的数据会被异步写入目标 Channel 上,用户线程不需要等待整
个数据完全被写入目标 Channel 就可以继续执行其他业务逻辑。
非阻塞 I/O 模型中的 Selector 线程通常将 I/O 的空闲时间用于执行其他通
道上的 I/O 操作,所以一个 Selector 线程可以管理多个输入和输出通
道,如图 1-18 所示。 1-18
1. Channel
Channel I/O 中的 Stream (流)类似,只不过 Stream 是单向的(例如
InputStream OutputStream ),而 Channel 是双向的,既可以用来进行
读操作,也可以用来进行写操作。
NIO Channel 的主要实现有: FileChannel DatagramChannel
SocketChannel ServerSocketChannel ,分别对应文件的 I/O UDP
TCP I/O Socket Client Socker Server 操作。
2. Buffer
Buffer 实际上是一个容器,其内部通过一个连续的字节数组存储 I/O
的数据。在 NIO 中, Channel 在文件、网络上对数据的读取或写入都必
须经过 Buffer
如图 1-19 所示,客户端在向服务端发送数据时,必须先将数据写入
Buffer 中,然后将 Buffer 中的数据写到服务端对应的 Channel 上。服务
端在接收数据时必须通过 Channel 将数据读入 Buffer 中,然后从 Buffer
中读取数据并处理。
1-19
NIO 中, Buffer 是一个抽象类,对不同的数据类型实现不同的 Buffer
操作。常用的 Buffer 实现类有: ByteBuffer IntBuffer CharBuffer
LongBuffer DoubleBuffer FloatBuffer ShortBuffer
3. Selector
Selector 用于检测在多个注册的 Channel 上是否有 I/O 事件发生,并对检
测到的 I/O 事件进行相应的响应和处理。因此通过一个 Selector 线程就
可以实现对多个 Channel 的管理,不必为每个连接都创建一个线程,避
免线程资源的浪费和多线程之间的上下文切换导致的开销。同时, Selector 只有在 Channel 上有读写事件发生时,才会调用 I/O 函数进行读
写操作,可极大减少系统开销,提高系统的并发量。
4. Java NIO 使用
要实现 Java NIO ,就需要分别实现 Server Client 。具体的 Server 实现
代码如下:
在以上代码中定义了名为 MyServer 的服务端实现类,在该类中定义了
serverSocketChannel 用于 ServerSocketChannel 的建立和端口的绑定;
byteBuffer 用于不同 Channel 之间的数据交互; selector 用于监听服务器
各个 Channel 上数据的变化并做出响应。同时,在类构造函数中调用了
初始化 ServerSocketChannel 的操作,定义了 listener 方法来监听 Channel
上的数据变化,解析客户端的数据并对客户端的请求做出响应。
具体的 Client 实现代码如下:
在以上代码中定义了 MyClient 类来实现客户端的 Channel 逻辑,其中,
connectServer 方法用于和服务端建立连接, receive 方法用于接收服务
端发来的数据, send2Server 用于向服务端发送数据。
1.10 JVM 的类加载机制
1.10.1 JVM 的类加载阶段
JVM 的类加载分为 5 个阶段:加载、验证、准备、解析、初始化。在
类初始化完成后就可以使用该类的信息,在一个类不再被需要时可以
JVM 中卸载,如图 1-20 所示。
1-20
1. 加载 JVM 读取 Class 文件,并且根据 Class 文件描述创建 java.lang.Class 对象
的过程。类加载过程主要包含将 Class 文件读取到运行时区域的方法区
内,在堆中创建 java.lang.Class 对象,并封装类在方法区的数据结构的
过程,在读取 Class 文件时既可以通过文件的形式读取,也可以通过 jar
包、 war 包读取,还可以通过代理自动生成 Class 或其他方式读取。
2. 验证
主要用于确保 Class 文件符合当前虚拟机的要求,保障虚拟机自身的安
全,只有通过验证的 Class 文件才能被 JVM 加载。
3. 准备
主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始
值。初始值指不同数据类型的默认值,这里需要注意 final 类型的变量
和非 final 类型的变量在准备阶段的数据初始化过程不同。比如一个成
员变量的定义如下:
在以上代码中,静态变量 value 在准备阶段的初始值是 0 ,将 value 设置
1000 的动作是在对象初始化时完成的,因为 JVM 在编译阶段会将静
态变量的初始化操作定义在构造器中。但是,如果将变量 value 声明为
final 类型:
JVM 在编译阶段后会为 final 类型的变量 value 生成其对应的
ConstantValue 属性,虚拟机在准备阶段会根据 ConstantValue 属性将
value 赋值为 1000
4. 解析
JVM 会将常量池中的符号引用替换为直接引用。
5. 初始化 主要通过执行类构造器的 <client> 方法为类进行初始化。 <client> 方法
是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组
成的。 JVM 规定,只有在父类的 <client> 方法都执行成功后,子类中的
<client> 方法才可以被执行。在一个类中既没有静态变量赋值操作也没
有静态语句块时,编译器不会为该类生成 <client> 方法。
在发生以下几种情况时, JVM 不会执行类的初始化流程。
常量在编译时会将其常量值存入使用该常量的类的常量池中,该过
程不需要调用常量所在的类,因此不会触发该常量类的初始化。
在子类引用父类的静态字段时,不会触发子类的初始化,只会触发
父类的初始化。
定义对象数组,不会触发该类的初始化。
在使用类名获取 Class 对象时不会触发类的初始化。
在使用 Class.forName 加载指定的类时,可以通过 initialize 参数设置
是否需要对类进行初始化。
在使用 ClassLoader 默认的 loadClass 方法加载类时不会触发该类的初
始化。
1.10.2 类加载器
JVM 提供了 3 种类加载器,分别是启动类加载器、扩展类加载器和应
用程序类加载器,如图 1-21 所示。
1-21
1 )启动类加载器:负责加载 Java_HOME/lib 目录中的类库,或通过 -
Xbootclasspath 参数指定路径中被虚拟机认可的类库。 2 )扩展类加载器:负责加载 Java_HOME/lib/ext 目录中的类库,或
通过 java.ext.dirs 系统变量加载指定路径中的类库。
3 )应用程序类加载器:负责加载用户路径( classpath )上的类库。
除了上述 3 种类加载器,我们也可以通过继承 java.lang.ClassLoader
现自定义的类加载器。
1.10.3 双亲委派机制
JVM 通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到
类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委
派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给
自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动
类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加
载该类(通常原因是该类的 Class 文件在父类的类加载路径中不存
在),则父类会将该信息反馈给子类并向下委派子类加载器加载该
类,直到该类被成功加载,若找不到该类,则 JVM 会抛出
ClassNotFoud 异常。
双亲委派类加载机制的类加载流程如下,如图 1-22 所示。
1 )将自定义加载器挂载到应用程序类加载器。
2 )应用程序类加载器将类加载请求委托给扩展类加载器。
3 )扩展类加载器将类加载请求委托给启动类加载器。
4 )启动类加载器在加载路径下查找并加载 Class 文件,如果未找到
目标 Class 文件,则交由扩展类加载器加载。
5 )扩展类加载器在加载路径下查找并加载 Class 文件,如果未找到
目标 Class 文件,则交由应用程序类加载器加载。 6 )应用程序类加载器在加载路径下查找并加载 Class 文件,如果未
找到目标 Class 文件,则交由自定义加载器加载。
7 )在自定义加载器下查找并加载用户指定目录下的 Class 文件,如
果在自定义加载路径下未找到目标 Class 文件,则抛出 ClassNotFoud
常。
1-22
双亲委派机制的核心是保障类的唯一性和安全性。例如在加载 rt.jar
中的 java.lang.Object 类时,无论是哪个类加载器加载这个类,最终都
将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一
性。如果在 JVM 中存在包名和类名相同的两个类,则该类将无法被加
载, JVM 也无法完成类加载流程。
1.10.4 OSGI
OSGI Open Service Gateway Initiative )是 Java 动态化模块化系统的一
系列规范,旨在为实现 Java 程序的模块化编程提供基础条件。基于
OSGI 的程序可以实现模块级的热插拔功能,在程序升级更新时,可以
只针对需要更新的程序进行停用和重新安装,极大提高了系统升级的
安全性和便捷性。
OSGI 提供了一种面向服务的架构,该架构为组件提供了动态发现其他
组件的功能,这样无论是加入组件还是卸载组件,都能被系统的其他
组件感知,以便各个组件之间能更好地协调工作。
OSGI 不但定义了模块化开发的规范,还定义了实现这些规范所依赖的
服务与架构,市场上也有成熟的框架对其进行实现和应用,但只有部
分应用适合采用 OSGI 方式,因为它为了实现动态模块,不再遵循 JVM
类加载双亲委派机制和其他 JVM 规范,在安全性上有所牺牲。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值