JVM原理学习笔记总结

1.1 JVM的运行机制

JVMJava Virtual Machine)是用于运行Java字节码的虚拟机,包括一
套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、
一个方法区和一个垃圾回收器。JVM运行在操作系统之上,不与硬件
设备直接交互。
Java源文件在通过编译器之后被编译成相应的.Class文件(字节码文
件),.Class文件又被JVM中的解释器编译成机器码在不同的操作系
统(WindowsLinuxMac)上运行。每种操作系统的解释器都是不
同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台
的原因。在一个Java进程开始运行后,虚拟机就开始实例化了,有多
个进程启动就会实例化多个虚拟机实例。进程退出或者关闭,则虚拟
机实例消亡,在多个虚拟机实例之间不能共享数据。
Java程序的具体运行过程如下。
1Java源文件被编译器编译成字节码文件。
2JVM将字节码文件编译成相应操作系统的机器码。
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运行时数据区的一部分,但
在并发编程中被频繁使用。JDKNIO模块提供的基于Channel
BufferI/O操作方式就是基于堆外内存实现的,NIO模块通过调用
Native函数库直接在操作系统上分配堆外内存,然后使用
DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程
可以通过堆外内存技术避免在Java堆和Native堆中来回复制数据带来
的资源占用和性能消耗,因此堆外内存在高并发应用场景下被广泛使
用(NettyFlinkHBaseHadoop都有用到堆外内存)。
1.3.1 程序计数器:线程私有,无内存溢出问题
程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行
的字节码的行号指示器。每个运行中的线程都有一个独立的程序计数
器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的
值为空(Undefined)。
程序计数器属于线程私有的内存区域,它是唯一没有Out Of
Memory(内存溢出)的区域。
1.3.2 虚拟机栈:线程私有,描述Java方法的执行过程
虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧
Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出
口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理
动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch
Exception)。
栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一
个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈
和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未
被捕获的异常),都视为方法运行结束。线程 1CPU1上运行,线程 2CPU2上运行,在CPU
资源不够时其他线程将处于等待状态
等待获取CPU时间片。而在线程内部,每个方法的执行和返回都对应
一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活
动状态。
1.3.3 本地方法区:线程私有
本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法
服务,本地方法栈为Native方法服务。
1.3.4 堆:也叫作运行时数据区,线程共享JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被
线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存
区域。由于现代JVM采用分代收集算法 ,因此Java堆从GCGarbage
Collection,垃圾回收)的角度还可以细分为:新生代、老年代和永久
代。
1.3.5 方法区:线程共享
方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编
译器编译后的机器码、运行时常量池等数据
JVMGC分代收集扩展至方法区,即使用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区,如下所述。
1Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建
的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体
JVM版本、堆大小和垃圾回收策略有关,一般为 2KB128KB,可
通过XX:PretenureSizeThreshold设置其大小。在Eden区的内存空间不足
时会触发MinorGC,对新生代进行一次垃圾回收。
2ServivorTo区:保留上一次MinorGC时的幸存者。
3ServivorFrom区:将上一次MinorGC时的幸存者作为这一次
MinorGC的被扫描者。
新生代的GC过程叫作MinorGC,采用复制算法实现,具体过程如下。
1)把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo
区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由
XX:MaxTenuringThreshold设置,默认为 15),则将其复制到老年
代,同时把这些对象的年龄加 1;如果ServivorTo区的内存空间不够,
则也直接将其复制到老年代;如果对象属于大对象(大小为 2KB
128KB的对象属于大对象,例如通过
XX:PretenureSizeThreshold=2097152设置大对象为 2MB1024×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 永久代
永久代指内存的永久保存区域,主要存放ClassMeta(元数据)的信
息。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针对新生代
和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾
收集器有SerialParNewParallel Scavenge,针对老年代提供的垃圾
收集器有Serial OldParallel OldCMS,还有针对不同区域的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垃圾收集器CMSConcurrent Mark Sweep)垃圾收集器是为老年代设计的垃圾收
集器,其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记
清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间
提高系统的稳定性。
CMS的工作机制相对复杂,垃圾回收过程包含如下4个步骤。
1)初始标记:只标记和GC Roots直接关联的对象,速度很快,需
要暂停所有工作线程。
2)并发标记:和用户线程一起工作,执行GC Roots跟踪标记过
程,不需要暂停工作线程。
3)重新标记:在并发标记过程中用户线程继续运行,导致在垃圾
回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正
确性,需要对其重新标记并暂停工作线程。
4)并发清除:和用户线程一起工作,执行清除GC Roots不可达对
象的任务,不需要暂停工作线程。
CMS垃圾收集器在和用户线程一起工作时(并发标记和并发清除)不
需要暂停用户线程,有效缩短了垃圾回收时系统的停顿时间,同时由
CMS垃圾收集器和用户线程一起工作,因此其并行度和效率也有很
大提升
1.8.7 G1垃圾收集器
G1Garbage 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模型在连接数众
多且消息体不大的情况下有很大的优势。尤其在物联网领域比如车载
设备实时位置、智能家电状态等定时上报状态且字节数较少的情况下
优势更加明显,一般一个经过优化后的1632GB服务器能承载约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
OutputStreamInputStreamWriterReader
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基于ChannelBuffer进行I/O读写操作,并且数据总是被从Channel
读取到Buffer中,或者从Buffer写入Channel中。
Java NIO和传统I/O的最大区别如下。
1I/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
ChannelI/O中的Stream(流)类似,只不过Stream是单向的(例如
InputStreamOutputStream),而Channel是双向的,既可以用来进行
读操作,也可以用来进行写操作。
NIOChannel的主要实现有:FileChannelDatagramChannel
SocketChannelServerSocketChannel,分别对应文件的I/OUDP
TCP I/OSocket ClientSocker Server操作。
2. Buffer
Buffer实际上是一个容器,其内部通过一个连续的字节数组存储I/O
的数据。在NIO中,Channel在文件、网络上对数据的读取或写入都必
须经过Buffer
如图 1-19所示,客户端在向服务端发送数据时,必须先将数据写入
Buffer中,然后将Buffer中的数据写到服务端对应的Channel上。服务
端在接收数据时必须通过Channel将数据读入Buffer中,然后从Buffer
中读取数据并处理。
1-19
NIO中,Buffer是一个抽象类,对不同的数据类型实现不同的Buffer
操作。常用的Buffer实现类有:ByteBufferIntBufferCharBuffer
LongBufferDoubleBufferFloatBufferShortBuffer
3. Selector
Selector用于检测在多个注册的Channel上是否有I/O事件发生,并对检
测到的I/O事件进行相应的响应和处理。因此通过一个Selector线程就
可以实现对多个Channel的管理,不必为每个连接都创建一个线程,避
免线程资源的浪费和多线程之间的上下文切换导致的开销。同时,Selector只有在Channel上有读写事件发生时,才会调用I/O函数进行读
写操作,可极大减少系统开销,提高系统的并发量。
4. Java NIO使用
要实现Java NIO,就需要分别实现ServerClient。具体的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
OSGIOpen Service Gateway Initiative)是Java动态化模块化系统的一
系列规范,旨在为实现Java程序的模块化编程提供基础条件。基于
OSGI的程序可以实现模块级的热插拔功能,在程序升级更新时,可以
只针对需要更新的程序进行停用和重新安装,极大提高了系统升级的
安全性和便捷性。
OSGI提供了一种面向服务的架构,该架构为组件提供了动态发现其他
组件的功能,这样无论是加入组件还是卸载组件,都能被系统的其他
组件感知,以便各个组件之间能更好地协调工作。
OSGI不但定义了模块化开发的规范,还定义了实现这些规范所依赖的
服务与架构,市场上也有成熟的框架对其进行实现和应用,但只有部
分应用适合采用OSGI方式,因为它为了实现动态模块,不再遵循JVM
类加载双亲委派机制和其他JVM规范,在安全性上有所牺牲。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值