JVMjava
(1) 基本概念:
JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、
一个垃圾回收,堆 和 一个存储方法域。 JVM 是运行在操做系统之上的,它与硬件没有直接
的交互算法
Hotspot JVM 后台运行的系统线程主要有下面几个:数据库
虚拟机线程 (VM thread) |
这个线程等待 JVM 到达安全点操做出现。这些操做必需要在独立的线程里执行,由于当 堆修改没法进行时,线程都须要 JVM 位于安全点。这些操做的类型有: stop-the world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。 |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操做的执行。 |
GC 线程 | 这些线程支持 JVM 中不一样的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区
域【JAVA 堆、方法区】、直接内存。编程
程序计数器(线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的
程序计数器,这类内存也称为“线程私有” 的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如
果仍是 Native 方法,则为空。
这个内存区域是惟一一个在虚拟机中没有规定任何 OutOfMemoryError 状况的区域。数组
虚拟机栈(线程私有)
是描述java方法执行的内存模型,每一个方法在执行的同时都会建立一个栈帧(Stack Frame)
用于存储局部变量表、操做数栈、动态连接、方法出口等信息。 每个方法从调用直至执行完成
的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态连接
(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。 栈帧随着方法调用而创
13/04/2018 Page 23 of 283
建,随着方法结束而销毁——不管方法是正常完成仍是异常完成(抛出了在方法内未被捕获的异
常)都算做方法结束
缓存
本地方法区(线程私有)
本地方法区和 Java Stack 做用相似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为
Native 方法服务, 若是一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个
C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
安全
堆(Heap-线程共享) -运行时数据区
是被线程共享的一块内存区域, 建立的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行
垃圾收集的最重要的内存区域。 因为现代 VM 采用分代收集算法, 所以 Java 堆从 GC 的角度还能够
细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。
网络
方法区/永久代(线程共享)
即咱们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、 常量、 静
态变量、 即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即便用Java
堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就能够像管理 Java 堆同样管理这部份内存,
而没必要为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型
的卸载, 所以收益通常很小)。
数据结构
运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class 文件中除了有类的版
本、字段、方法、接口等描述等信息外,还有一项信息是常量池
多线程
JVM 运行时内存
Java 堆从 GC 的角度还能够细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年
代。
新生代
是用来存放新生的对象。通常占据堆的 1/3 空间。因为频繁建立对象,因此新生代会频繁触发
MinorGC 进行垃圾回收。新生代又分为 Eden 区、 ServivorFrom、 ServivorTo 三个区
Eden 区
Java 新对象的出生地(若是新建立的对象占用内存很大,则直接分配到老
年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行
一次垃圾回收。
ServivorFrom
上一次 GC 的幸存者,做为这一次 GC 的被扫描者。
ServivorTo
保留了一次 MinorGC 过程当中的幸存者。
MinorGC 的过程(复制->清空->互换)
MinorGC 采用复制算法。
eden、 servicorFrom 复制到 ServicorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(若是有对象的年
龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(若是 ServicorTo 不
够位置了就放到老年区);
2: 清空 eden、 servicorFrom
而后,清空 Eden 和 ServicorFrom 中的对象;
3: ServicorTo 和 ServicorFrom 互换
最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom
区。
老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,因此 MajorGC 不会频繁执行。在进行 MajorGC 前通常都先进行
了一次 MinorGC,使得有新生代的对象晋身入老年代,致使空间不够用时才触发。当没法找到足
够大的连续空间分配给新建立的较大对象时也会提早触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次全部老年代,标记出存活的对象,而后回收没
有标记的对象。 MajorGC 的耗时比较长,由于要扫描再回收。 MajorGC 会产生内存碎片,为了减
少内存损耗,咱们通常须要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的
时候,就会抛出 OOM(Out of Memory)
永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被
放入永久区域, 它和和存放实例的区域不一样,GC 不会在主程序运行期对永久区域进行清理。因此这
也致使了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
JAVA8 与元数据
在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
的本质和永久代相似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用
本地内存。所以,默认状况下,元空间的大小仅受本地内存限制。 类的元数据放入 native
memory, 字符串池和类的静态变量放入 java 堆中, 这样能够加载多少类的元数据就再也不由
MaxPermSize 控制, 而由系统的实际可用空间来控制。
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 建立/销毁(在 Hotspot
VM 内, 每一个线程都与操做系统的本地线程直接映射, 所以这部份内存区域的存/否跟随本地线程的
生/死对应)。
线程共享区域随虚拟机的启动/关闭而建立/销毁。
直接内存并非 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提
供了基于 Channel 与 Buffer 的 IO 方式, 它可使用 Native 函数库直接分配堆外内存, 而后使用
DirectByteBuffer 对象做为这块内存的引用进行操做(详见: Java I/O 扩展), 这样就避免了在 Java
堆和 Native 堆中来回复制数据, 所以在一些场景中能够显著提升性能
2.4.垃圾回收与算法
2.4.1. 如何肯定垃圾
2.4.1.1. 引用计数法
在 Java 中,引用和对象是有关联的。若是要操做对象则必须用引用进行。所以,很显然一个简单
的办法是经过引用计数来判断一个对象是否能够回收。简单说,即一个对象若是没有任何与之关
联的引用, 即他们的引用计数都不为 0, 则说明对象不太可能再被用到,那么这个对象就是可回收
对象。
可达性分析
为了解决引用计数法的循环引用问题, Java 使用了可达性分析的方法。经过一系列的“GC roots”
对象做为起点搜索。若是在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
13/04/2018 Page 27 of 283
要注意的是,不可达对象不等价于可回收对象, 不可达对象变为可回收对象至少要通过两次标记
过程。两次标记后仍然是可回收对象,则将面临回收
标记清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出全部须要回收的对象,清
除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可
利用空间的问题。
2.4.3. 复制算法(copying)
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小
的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另外一块上去,把已使用
的内存清掉,如图
这种算法虽然实现简单,内存效率高,不易产生碎片,可是最大的问题是可用内存被压缩到了原
本的一半。且存活对象增多的话, Copying 算法的效率会大大下降
2.4.4. 标记整理算法(Mark-Compact)
结合了以上两个算法,为了不缺陷而提出。标记阶段和 Mark-Sweep 算法相同, 标记后不是清
理对象,而是将存活对象移向内存的一端。而后清除端边界外的对象。
2.4.5. 分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不一样生命周期将内存
划分为不一样的域,通常状况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young
Generation)。老生代的特色是每次垃圾回收时只有少许对象须要被回收,新生代的特色是每次垃
圾回收时都有大量垃圾须要被回收,所以能够根据不一样区域选择不一样的算法
2.4.5.1. 新生代与复制算法
目前大部分 JVM 的 GC 对于新生代都采起 Copying 算法,由于新生代中每次垃圾回收都要
回收大部分对象,即要复制的操做比较少,但一般并非按照 1: 1 来划分新生代。通常将新生代
划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用
Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另
一块 Survivor 空间中。
2.4.5.2. 老年代与标记复制算法
而老年代由于每次只回收少许对象,于是采用 Mark-Compact 算法。
1. JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation), 它用来存储 class 类,
常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目
前存放对象的那一块),少数状况会直接分配到老生代。
3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后, Eden
Space 和 From Space 区的存活对象会被挪到 To Space,而后将 Eden Space 和 From
Space 进行清理。
4. 若是 To Space 没法足够存储某个对象,则将这个对象存储到老生代。
5. 在进行 GC 后,使用的即是 Eden Space 和 To Space 了,如此反复循环。
6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。 默认状况下年龄到达 15 的对象会被
移到老生代中。
2.5.JAVA 四中引用类型
2.5.1. 强引用
在 Java 中最多见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引
用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即
使该对象之后永远都不会被用到 JVM 也不会回收。所以强引用是形成 Java 内存泄漏的主要缘由之
一。Object o=new Object();
2.5.2. 软引用
软引用须要用 SoftReference 类来实现,对于只有软引用的对象来讲,当系统内存足够时它
不会被回收,当系统内存空间不足时它会被回收。软引用一般用在对内存敏感的程序中。
2.5.3. 弱引用
弱引用须要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象
来讲,只要垃圾回收机制一运行,无论 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用须要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚
引用的主要做用是跟踪对象被垃圾回收的状态。
2.6.GC 分代收集算法 VS 分区收集算法
分代收集算法
当前主流 VM 垃圾收集都采用”分代收集” (Generational Collection)算法, 这种算法会根据
对象存活周期的不一样将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代, 这样就能够根据
各年代特色分别采用最适当的 GC 算法
2.6.1.1. 在新生代-复制算法
每次垃圾收集都能发现大批对象已死, 只有少许存活. 所以选用复制算法, 只须要付出少许
存活对象的复制成本就能够完成收集
2.6.1.2. 在老年代-标记整理算法
由于对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标
记—整理” 算法来进行回收, 没必要进行内存复制, 且直接腾出空闲内存
2.6.2. 分区收集算法
分区算法则将整个堆空间划分为连续的不一样小区间, 每一个小区间独立使用, 独立回收. 这样作的
好处是能够控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是
整个堆), 从而减小一次 GC 所产生的停顿。
2.7.GC 垃圾收集器
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;
年老代主要使用标记-整理垃圾回收算法,所以 java 虚拟中针对新生代和年老代分别提供了多种不
同的垃圾收集器, JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器以下:
2.7.1. Serial 垃圾收集器(单线程、 复制算法)
Serial(英文连续) 是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 以前新生代惟一的垃圾
收集器。 Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工
做,而且在进行垃圾收集的同时,必须暂停其余全部的工做线程,直到垃圾收集结束。
Serial 垃圾收集器虽然在收集垃圾过程当中须要暂停全部其余的工做线程,可是它简单高效,对于限
定单个 CPU 环境来讲,没有线程交互的开销,能够得到最高的单线程垃圾收集效率,所以 Serial
垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器
2.7.2. ParNew 垃圾收集器(Serial+多线程)
ParNew 垃圾收集器实际上是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃
圾收集以外,其他的行为和 Serial 收集器彻底同样, ParNew 垃圾收集器在垃圾收集过程当中一样也
要暂停全部其余的工做线程
ParNew 收集器默认开启和 CPU 数目相同的线程数,能够经过-XX:ParallelGCThreads 参数来限
制垃圾收集器的线程数。 【Parallel:平行的】
ParNew 虽然是除了多线程外和Serial 收集器几乎彻底同样,可是ParNew垃圾收集器是不少 java
虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
2.7.3. Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,一样使用复制算法,也是一个多线程的垃
圾收集器, 它重点关注的是程序达到一个可控制的吞吐量(Thoughput, CPU 用于运行用户代码
的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),
高吞吐量能够最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而
不须要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个
重要区别
2.7.4. Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它一样是个单线程的收集器,使用标记-整理算法,
这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:
1. 在 JDK1.5 以前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
2. 做为年老代中使用 CMS 收集器的后备垃圾收集方案。
2.7.5. Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6
才开始提供。
在 JDK1.6 以前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只
能保证新生代的吞吐量优先,没法保证总体的吞吐量, Parallel Old 正是为了在年老代一样提供吞
吐量优先的垃圾收集器, 若是系统对吞吐量要求比较高,能够优先考虑新生代 Parallel Scavenge
和年老代 Parallel Old 收集器的搭配策略。
2.7.6. CMS 收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾
回收停顿时间, 和其余年老代使用标记-整理算法不一样,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间能够为交互比较高的程序提升用户体验。
CMS 工做机制相比其余的垃圾收集器来讲更复杂,整个过程分为如下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然须要暂停全部的工做线程。
并发标记
进行 GC Roots 跟踪的过程,和用户线程一块儿工做,不须要暂停工做线程
从新标记
为了修正在并发标记期间,因用户程序继续运行而致使标记产生变更的那一部分对象的标记
记录,仍然须要暂停全部的工做线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一块儿工做,不须要暂停工做线程。因为耗时最长的并
发标记和并发清除过程当中,垃圾收集线程能够和用户如今一块儿并发工做, 因此整体上来看
CMS 收集器的内存回收和用户线程是一块儿并发地执行。
2.7.7. G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器, G1 收
集器两个最突出的改进是:
1. 基于标记-整理算法,不产生内存碎片。
2. 能够很是精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,而且跟踪这些区域
的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所容许的收集时间, 优先回收垃圾
最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器能够在有限时间得到最高的垃圾收
集效率。
2.8. JAVA IO/NIO
2.8.1. 阻塞 IO 模型
最传统的一种 IO 模型,即在读写数据过程当中会发生阻塞现象。当用户线程发出 IO 请求以后,内
核会去查看数据是否就绪,若是没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用
户线程交出 CPU。当数据就绪以后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用
13/04/2018 Page 35 of 283
户线程才解除 block 状态。典型的阻塞 IO 模型的例子为: data = socket.read();若是数据没有就
绪,就会一直阻塞在 read 方法。
2.8.2. 非阻塞 IO 模型
当用户线程发起一个 read 操做后,并不须要等待,而是立刻就获得了一个结果。 若是结果是一个
error 时,它就知道数据尚未准备好,因而它能够再次发送 read 操做。一旦内核中的数据准备
好了,而且又再次收到了用户线程的请求,那么它立刻就将数据拷贝到了用户线程,而后返回。
因此事实上,在非阻塞 IO 模型中,用户线程须要不断地询问内核数据是否就绪,也就说非阻塞 IO
不会交出 CPU,而会一直占用 CPU。 典型的非阻塞 IO 模型通常以下
可是对于非阻塞 IO 就有一个很是严重的问题, 在 while 循环中须要不断地去询问内核数据是否就
绪,这样会致使 CPU 占用率很是高,所以通常状况下不多使用 while 循环这种方式来读取数据
2.8.3. 多路复用 IO 模型
多路复用 IO 模型是目前使用得比较多的模型。 Java NIO 实际上就是多路复用 IO。在多路复用 IO
模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真
正调用实际的 IO 读写操做。由于在多路复用 IO 模型中,只须要使用一个线程就能够管理多个
socket,系统不须要创建新的进程或者线程,也没必要维护这些线程和进程,而且只有在真正有
socket 读写事件进行时,才会使用 IO 资源,因此它大大减小了资源占用。在 Java NIO 中,是通
过 selector.select()去查询每一个通道是否有到达事件,若是没有事件,则一直阻塞在那里,所以这
种方式会致使用户线程的阻塞。多路复用 IO 模式,经过一个线程就能够管理多个 socket,只有当
socket 真正有读写事件发生才会占用资源来进行实际的读写操做。所以,多路复用 IO 比较适合连
接数比较多的状况。
另外多路复用 IO 为什么比非阻塞 IO 模型的效率高是由于在非阻塞 IO 中,不断地询问 socket 状态
时经过用户线程去进行的,而在多路复用 IO 中,轮询每一个 socket 状态是内核在进行的,这个效
率要比用户线程要高的多。
不过要注意的是,多路复用 IO 模型是经过轮询的方式来检测是否有事件到达,而且对到达的事件
逐一进行响应。所以对于多路复用 IO 模型来讲, 一旦事件响应体很大,那么就会致使后续的事件
迟迟得不处处理,而且会影响新的事件轮询
不过要注意的是,多路复用 IO 模型是经过轮询的方式来检测是否有事件到达,而且对到达的事件
逐一进行响应。所以对于多路复用 IO 模型来讲, 一旦事件响应体很大,那么就会致使后续的事件
迟迟得不处处理,而且会影响新的事件轮询
2.8.4. 信号驱动 IO 模型
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操做,会给对应的 socket 注册一个信号函
数,而后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到
信号以后,便在信号函数中调用 IO 读写操做来进行实际的 IO 请求操做。
2.8.5. 异步 IO 模型
异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操做以后,马上就
能够开始去作其它的事。而另外一方面,从内核的角度,当它受到一个 asynchronous read 以后,
它会马上返回,说明 read 请求已经成功发起了,所以不会对用户线程产生任何 block。而后,内
核会等待数据准备完成,而后将数据拷贝到用户线程,当这一切都完成以后,内核会给用户线程
发送一个信号,告诉它 read 操做完成了。也就说用户线程彻底不须要实际的整个 IO 操做是如何
进行的, 只须要先发起一个请求,当接收内核返回的成功信号时表示 IO 操做已经完成,能够直接
去使用数据了。
也就说在异步 IO 模型中, IO 操做的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完
成,而后发送一个信号告知用户线程操做已完成。用户线程中不须要再次调用 IO 函数进行具体的
读写。这点是和信号驱动模型有所不一样的,在信号驱动模型中,当用户线程接收到信号表示数据
已经就绪,而后须要用户线程调用 IO 函数进行实际的读写操做;而在异步 IO 模型中,收到信号
表示 IO 操做已经完成,不须要再在用户线程中调用 IO 函数进行实际的读写操做。
2.8.1. JAVA IO 包
字节流与字符流
我本身网上看到的:
https://www.jianshu.com/p/828051b6a50f
问:字节流与字符流有什么区别?
答:计算机中的一切最终都是以二进制字节形式存在的,对于咱们常常操做的字符串,在写入时其实都是先获得了其对应的字节,而后将字节写入到输出流,在读取时其实都是先读到的是字节,而后将字节直接使用或者转换为字符给咱们使用。因为字节和字符两种操做的需求比较普遍,因此 Java 专门提供了字符流与字节流相关 IO 类。
对于程序运行的底层设备来讲永远都只接受字节数据,因此当咱们往设备写数据时不管是字节仍是字符最终都是写的字节流。字符流是字节流的包装类,因此当咱们将字符流向字节流转换时要注意编码问题(由于字符串转成字节数组的实质是转成该字符串的某种字节编码)。
字符流和字节流的使用很是类似,可是实际上字节流的操做不会通过缓冲区(内存)而是直接操做文本自己的,而字符流的操做会先通过缓冲区(内存)而后经过缓冲区再操做文件。
问:什么是缓冲区?有什么做用?
答:缓冲区就是一段特殊的内存区域,不少状况下当程序须要频繁地操做一个资源(如文件或数据库)则性能会很低,因此为了提高性能就能够将一部分数据暂时读写到缓存区,之后直接今后区域中读写数据便可,这样就显著提高了性能。
对于 Java 字符流的操做都是在缓冲区操做的,因此若是咱们想在字符流操做中主动将缓冲区刷新到文件则可使用 flush() 方法操做。
问:字节流和字符流哪一个好?怎么选择?
答:大多数状况下使用字节流会更好,由于字符流是字节流的包装,而大多数时候 IO 操做都是直接操做磁盘文件,因此这些流在传输时都是以字节的方式进行的(图片等都是按字节存储的)。
而若是对于操做须要经过 IO 在内存中频繁处理字符串的状况使用字符流会好些,由于字符流具有缓冲区,提升了性能。
2.8.2. JAVA NIO
NIO 主要有三大核心部分: Channel(通道), Buffer(缓冲区), Selector。传统 IO 基于字节流和字
符流进行操做, 而 NIO 基于 Channel 和 Buffer(缓冲区)进行操做,数据老是从通道读取到缓冲区
中,或者从缓冲区写入到通道中。 Selector(选择区)用于监听多个通道的事件(好比:链接打开,
数据到达)。所以,单个线程能够监听多个数据通道。
NIO 的缓冲区
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取全部字节,它们没有被缓存在任何
地方。此外,它不能先后移动流中的数据。若是须要先后移动从流中读取的数据, 须要先将它缓
存到一个缓冲区。 NIO 的缓冲导向方法不一样。数据读取到一个它稍后处理的缓冲区,须要时可在
缓冲区中先后移动。这就增长了处理过程当中的灵活性。可是,还须要检查是否该缓冲区中包含所
有您须要处理的数据。并且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里还没有处理的
数据
NIO 的非阻塞
IO 的各类流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有
一些数据被读取,或数据彻底写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式,
使一个线程从某通道发送请求读取数据,可是它仅能获得目前可用的数据,若是目前没有数据可
用时,就什么都不会获取。而不是保持线程阻塞,因此直至数据变的能够读取以前,该线程能够
继续作其余的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不须要等待它
彻底写入,这个线程同时能够去作别的事情。 线程一般将非阻塞 IO 的空闲时间用于在其它通道上
执行 IO 操做,因此一个单独的线程如今能够管理多个输入和输出通道(channel)。
2.8.3. Channel
首先说一下 Channel,国内大多翻译成“通道”。 Channel 和 IO 中的 Stream(流)是差很少一个
等级的。 只不过 Stream 是单向的,譬如: InputStream, OutputStream, 而 Channel 是双向
的,既能够用来进行读操做,又能够用来进行写操做。
NIO 中的 Channel 的主要实现有:
1. FileChannel
2. DatagramChannel
3. SocketChannel
4. ServerSocketChannel
这里看名字就能够猜出个因此然来:分别能够对应文件 IO、 UDP 和 TCP(Server 和 Client)。
下面演示的案例基本上就是围绕这 4 个类型的 Channel 进行陈述的。
2.8.4. Buffer
Buffer,故名思意, 缓冲区,其实是一个容器,是一个连续数组。 Channel 提供从文件、
网络读取数据的渠道,可是读取或写入的数据都必须经由 Buffer。
上面的图描述了从一个客户端向服务端发送数据,而后服务端接收数据的过程。客户端发送
数据时,必须先将数据存入 Buffer 中,而后将 Buffer 中的内容写入通道。服务端这边接收数据必
须经过 Channel 将数据读入到 Buffer 中,而后再从 Buffer 中取出数据来处理。
在 NIO 中, Buffer 是一个顶层父类,它是一个抽象类,经常使用的 Buffer 的子类有:
ByteBuffer、 IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、 FloatBuffer、
ShortBuffer
2.8.5. Selector
Selector 类是 NIO 的核心类, Selector 可以检测多个注册的通道上是否有事件发生,若是有事
件发生,便获取事件而后针对每一个事件进行相应的响应处理。这样一来,只是用一个单线程就可
以管理多个通道,也就是管理多个链接。这样使得只有在链接真正有读写事件发生时,才会调用
函数来进行读写,就大大地减小了系统开销,而且没必要为每一个链接都建立一个线程,不用去维护
多个线程,而且避免了多线程之间的上下文切换致使的开销。
2.9.JVM 类加载机制
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面咱们就分别来看一下这
五个过程。
加载是类加载过程当中的一个阶段, 这个阶段会在内存中生成一个表明这个类的 java.lang.Class 对
象, 做为方法区这个类的各类数据的入口。注意这里不必定非得要从一个 Class 文件获取,这里既
能够从 ZIP 包中读取(好比从 jar 包和 war 包中读取),也能够在运行时计算生成(动态代理),
也能够由其它文件生成(好比将 JSP 文件转换成对应的 Class 类)
2.9.1.2. 验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并
且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使
用的内存空间。注意这里所说的初始值概念,好比一个类变量定义为:
public static int v = 8080; |
实际上变量 v 在准备阶段事后的初始值为 0 而不是 8080, 将 v 赋值为 8080 的 put static 指令是
程序被编译后, 存放于类构造器<client>方法之中。
可是注意若是声明为:
public static final int v = 8080; |
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v
赋值为 8080。
2.9.1.4. 解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中
的:
CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info
等类型的常量。
2.9.1.5. 符号引用
符号引用与虚拟机实现的布局无关, 引用的目标并不必定要已经加载到内存中。 各类虚拟
机实现的内存布局能够各不相同,可是它们能接受的符号引用必须是一致的,由于符号引
用的字面量形式明肯定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用
直接引用能够是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。若是有
了直接引用,那引用的目标一定已经在内存中存在。
2.9.1.7. 初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段以后,除了在加载阶段能够自定义类加载
器之外,其它操做都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
2.9.1.8. 类构造器<client>
初始化阶段是执行类构造器<client>方法的过程。 <client>方法是由编译器自动收集类中的类变
量的赋值操做和静态语句块中的语句合并而成的。虚拟机会保证子<client>方法执行以前,父类
的<client>方法已经执行完毕, 若是一个类中没有对静态变量赋值也没有静态语句块,那么编译
器能够不为这个类生成<client>()方法。
注意如下几种状况不会执行类初始化:
1. 经过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并无直接引用定义常量的类,不会触
发定义常量所在的类。
4. 经过类名获取 Class 对象,不会触发类的初始化。
5. 经过 Class.forName 加载指定类时,若是指定参数 initialize 为 false 时,也不会触发类初
始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6. 经过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动做。
2.9.2. 类加载器
虚拟机设计团队把加载动做放到 JVM 外部实现,以便让应用程序决定如何获取所需的类, JVM 提
供了 3 种类加载器:
2.9.2.1. 启动类加载器(Bootstrap ClassLoader)
1. 负责加载 JAVA_HOME\lib 目录中的, 或经过-Xbootclasspath 参数指定路径中的, 且被
虚拟机承认(按文件名识别, 如 rt.jar) 的类。
2.9.2.2. 扩展类加载器(Extension ClassLoader)
2. 负责加载 JAVA_HOME\lib\ext 目录中的,或经过 java.ext.dirs 系统变量指定路径中的类
库。
2.9.2.3. 应用程序类加载器(Application ClassLoader):
3. 负责加载用户路径(classpath)上的类库。
JVM 经过双亲委派模型进行类的加载, 固然咱们也能够经过继承 java.lang.ClassLoader
实现自定义的类加载器。
2.9.3. 双亲委派
当一个类收到了类加载请求,他首先不会尝试本身去加载这个类,而是把这个请求委派给父
类去完成,每个层次类加载器都是如此,所以全部的加载请求都应该传送到启动类加载其中,
只有当父类加载器反馈本身没法完成这个请求的时候(在它的加载路径下没有找到所需加载的
Class), 子类加载器才会尝试本身去加载。
采用双亲委派的一个好处是好比加载位于 rt.jar 包中的类 java.lang.Object,无论是哪一个加载
器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不一样的类加载
器最终获得的都是一样一个 Object 对象。
2.9.4. OSGI( 动态模型系统)
OSGi(Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系
统的一系列规范。
OSGi(Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系
统的一系列规范。
2.9.4.1. 动态改变构造
OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使
这些耦合度可管理, OSGi 技术提供一种面向服务的架构,它能使这些组件动态地发现对方。
2.9.4.2. 模块化编程与热插拔 OSGi 旨在为实现 Java 程序的模块化编程提供基础条件,基于 OSGi 的程序极可能能够实现模块级 的热插拔功能,当程序升级更新时,能够只停用、从新安装而后启动程序的其中一部分,这对企 业级程序开发来讲是很是具备诱惑力的特性。 OSGi 描绘了一个很美好的模块化开发目标,并且定义了实现这个目标的所须要服务与架构,同时 也有成熟的框架进行实现支持。但并不是全部的应用都适合采用 OSGi 做为基础架构,它在提供强大 功能同时,也引入了额外的复杂度,由于它不遵照了类加载的双亲委托模型。