This commit is contained in:
yinkanglong
2023-10-21 09:48:50 +08:00
parent 5ef2c996cf
commit b2f069ee8d
555 changed files with 1886 additions and 506 deletions

View File

@@ -0,0 +1,95 @@
> https://www.cnblogs.com/qingshanli/p/9281760.html
## 1 Java编译流程
### 基本流程
* 前端编译主要指与源语言有关但与目标机无关的部分,包括词法分析、语法分析、语义分析与中间代码生成。把*.java文件转变成*.class文件
* 后端编译主要指与目标机有关的部分,包括代码优化和目标代码生成等。指把字节码转变成机器码
![](image/2022-08-15-21-55-14.png)
关键工具:词法分析器、语法分析器、语义分析器和代码生成器
1. 读取源码进行词法分析。也就是找出源码字节中的关键字识别出合法的关键字最后得出一些规范化的Token(中文意思是“标记“、”象征”等)流。
2. 对Token流进行语法分析检查关键词的组合是否符合语法最后得到抽象的语法树语法树是吧语言的主要此法用一个结构化的形式组合在一起
3. 进行语法分析,把难懂的,复杂的语法转化成更加简单的的语法(对计算机来说),最后得到一个注解过后的抽象语法树
4. 通过字节码生成器将经过注解的抽象语法树生成字节码
## 2 Java前端编译
java的前端编译(即javac编译)可分为解析与填充符号表、插入式注解处理器的注解处理、分析与字节码生成等三个过程。
![](image/2022-08-15-22-21-05.png)
1. 解析与填充符号表。解析步骤包括词法分析和语法分析两个阶段。词法分析是将源代码的字符流转变为标记(Token)集合, 单个字符是程序编写过程的最小单位, 而标记则是编译过程的最小单位, 关键字、变量名、字面量、运算符都可以成为标记。语法分析是根据Token序列构造抽象语法树的过程, 抽象语法树(AST)是一种用来描述程序代码语法结构的树形表示方式, 语法树的每一个节点都代表着程序代码中的一个语法结构, 如包、类型、修饰符、运算符、接口、返回值都可以是一个语法结构。 符号表是由一组符号地址和符号信息构成的表格。在语法分析中, 符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段, 符号表是当对符号名进行地址分配时的依据。
2. 插入式注解处理器。插入式注解处理器可以看做是一组编译器的插件, 在这些插件里面, 可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法数进行了修改, 编译器将回到解析与填充符号表的过程重新处理, 直到所有插入式注解处理器都没有再对语法数进行修改为止, 每一次循环称为一个Round。
3. 语义分析与字节码生成。语法分析后, 编译器获得了程序代码的抽象语法树表示, 语法数能表示一个结构正确的源程序的抽象, 但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构正确的源程序进行上下文有关性质的审查。Javac的编译过程中, 语义分析过程分为标注检查、数据及控制流分析两个步骤。
1. 标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。另外在标注检查步骤中, 还有一个重要的动作称为常量折叠。
2. 数据及控制流分析是对程序上下文逻辑更进一步的验证, 他可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理等问题。
Java中常用的语法糖有泛型、变长参数、自动装箱/拆箱、遍历循环、条件编译等等。虚拟机运行时并不支持这些语法, 它们在编译阶段还原回简单的基础语法结构, 这个过程称为解语法糖。
字节码生成是Javac编译过程的最后一个阶段, 它将前面各个步骤所生成的信息(语法数、符号表)转化成字节码写到磁盘中, 另外还进行少量的代码添加(如实例构造器)和转换工作。
## 3 Java中的后端编译
在部分商用虚拟机中, Java程序最初是通过解释器进行解释执行的, 当虚拟机发现某个方法或代码块的运行特别频繁时, 就会把这些代码认定为"热点代码"。为了提高热点代码的执行效率, 在运行时, 虚拟机将会把这些代码编译成与本地平台相关的机器码, 并进行各种层析的优化, 完成这个任务的编译器称为即时编译器(JIT编译器)。
### 编译器与解释器
HotSpot虚拟机中内置了两个即时编译器, 分别称为Client Compiler(C1编译器)和Server Compiler(C2编译器)。在HotSpot虚拟机中, 默认采用解释器与其中一个编译器直接配合的方式工作, 程序使用哪个编译器, 取决于虚拟机运行的模式, HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式, 这种解释器与编译器搭配使用的方式在虚拟机中称为"混合模式"(Mixed Mode)。在个人机器上, 通过java -version命令可查看自己安装的JDK中是哪种模式。
```sh
➜ ~ java -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)
```
### 分层编译
在JDK 1.7的Server模式虚拟机中, 默认开启分层编译的策略。分层编译根据编译器编译、优化的规模与耗时, 划分出不同的编译层次:
1. 第0层, 程序解释执行, 解释器不开启性能监控功能, 可触发第1层编译。
2. 第1层, 也称为C1编译, 将字节码编译为本地代码, 进行简单可靠的优化, 如有必要将加入性能性能监控的逻辑。
3. 第2层(或2层以上), 也称为C2编译, 也是将字节码编译为本地代码, 但是会启用一些编译耗时较长的优化, 甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后, C1编译器和C2编译器将会同时工作, 用C1编译器获取更高的编译速度, 用C2编译器获取更好的编译质量。
### 编译对象与触发条件
在运行过程中会被即时编译器编译的"热点代码"有如下两类:
* 被多次调用的方法。
* 被多次执行的循环体。
对于第一种情况, 编译器会以整个方法作为编译对象, 这种编译也是虚拟机中标准的JIT编译方式。而对于第二种, 尽管编译动作是由循环体所触发的, 但编译器依然会以整个方法(而不是单独的循环体)作为编译对象, 这种编译方式因为编译发生在方法执行过程之中, 因此形象的称之为栈上替换(即OSR编译)。
### 热点探测
判断是否需要触发即时编译, 需要先识别出热点代码, 这个行为称之为热点探测。目前主要的热点探测判定方式有以下两种:
基于采样的热点探测: 虚拟机周期性地检查各个线程的栈顶, 如发现某个方法经常出现在栈顶, 它就是"热点方法"。好处是简单高效, 还可以获取方法调用关系; 缺点是很难精确的确认一个方法的热点, 容易受到线程阻塞或别的外界因素干扰。
基于计数器的热点探测: 虚拟机会为每个方法(甚至是代码块)建立计数器, 统计方法的执行次数, 如果执行次数超过一定的阈值就认为是"热点方法"。
在HotSpot虚拟机中使用的是第二种————基于计数器的热点探测, 它为每个方法准备了两类计数器: 方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下, 这两个计数器都有一个的确定的阈值, 当计数器超过阈值溢出, 就会触发JIT编译。
方法调用计数器用于统计方法被调用的次数; 回边计数器用于统计一个方法中循环体代码执行的次数, 在字节码中遇到控制流向后跳转的指令称为"回边"。关于这两种计数器, 读者可参阅<<深入理解Java虚拟机>>, 这里不多做深入分析。
### 编译过程
在默认设置下, 无论是方法调用产生的标准JIT编译请求, 还是OSR编译请求, 虚拟机在代码编译器还未完成之前, 都仍然将按照解释方式继续执行, 而编译动作则在后台的编译线程中进行。
## 4 Java的后端编译优化技术
### 公共子表达式消除
如果一个表达式E已经计算过了并且从先前的计算到现在E中所有变量的值都没有发生变化那E的这次出现就成为了公共子表达式。对于这种表达式, 没必要花时间再对它进行计算, 只需要直接用前面计算过的表达式结果替代E就可以了。
### 数组边界检查消除
顾名思义就是如果编译器根据数据流分析, 访问数组的下标没有越界, 那么就可以消除数组的边界检查, 这样能节省很多的条件判断操作, 提升程序性能。
### 方法内联
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。
### 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
如果能证明一个对象不会逃逸到方法或线程外,则可能为这个变量进行一些高效的优化, 如栈上替换、同步消除、标量替换。

View File

@@ -0,0 +1,89 @@
# Java类加载机制
> https://blog.csdn.net/qq_29167297/article/details/124800850
## 0 JVM简介
### JVM空间
JVM内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
![](image/2022-12-04-13-14-56.png)
### ClassLoader
![](image/2022-12-04-13-15-46.png)
## 1 加载
加载阶段是将class文件从磁盘或者jar等读到JVM内存中并为其创建一个Class对象。任何一个类被使用时候系统都会为其创建一个Class对象的。加载的同时将加载的这些数据转换成方法区中运行时数据(运行时候数据区:静态变量、静态代码块、常量池等),作为方法区数据的访问入口。
加载是类加载的第一个过程,在这个阶段,将完成以下三件事情:
1. 通过一个类的全限定名获取该类的二进制流。
2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
3. 在内存中生成该类的Class对象作为该类的数据访问入口。
事实上,这三条限定都不是很严格,比如第一条,并没有明确指出通过全限定名从哪里得到二进制流,由此就有很多不同的实现:
* 在ZIP包中读取JAR,EAR,WAR
* 从网络中获取APPLET
* 运行时计算生成这种场景使用的最多的就是动态代理技术在java.lang.reflect.Proxy中就是用了ProxyGenerator.generateProxyClass来为特定接口申城$Proxy的代理类的二进制流
* 由其它文件生成jsp
* 从数据库中读取有些中间件服务器SAP NETWEAVER)
加载阶段完成后虚拟机外部的二进制流就按照虚拟机所需的格式存储在方法区中方法区中的数据存储格式由虚拟机实现自行定义。然后在JAVA堆中实例化一个java.lang.Class类对象比如我们new A()对象在加载过程中会在堆区生成一个代表A类的java.lang.Class类的对象而new所产生的对象是依靠A类的java.lang.class对象为模板产生的新的对象到堆区这个对象将作为程序访问方法区中的这些类型数据的外部接口。加载阶段与连接阶段的部分内容是交叉进行的加载阶段尚未完成连接阶段可能已经开始。
## 2 验证
验证目的是为了确保Class文件的字节流中的信息不会危害到虚拟机在该阶段主要完成以下四种验证
1. 文件格式验证验证字节流是否符合Class文件的规范如主次版本号是否在当前虚拟机范围内常量池中的常量是否有不被支持的类型。
2. 元数据验证:对字节码描述的信息进行语义分析,如这个类中是否有父类,是否集成了不被继承的类等。
3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
## 3 准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存实例变量将会在对象实例化时随着对象一起分配在Java堆中。如果该变量被final修饰将在编译时生成ConstantValue这样在准备阶段将直接设置成该初值。
```java
public static int value=123;//在准备阶段value初始值为0初始化阶段才变为123。
```
### 4 解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
* 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。其实就是将堆内存空间里的静态变量符号修改为已经申请了空间的静态变量地址的过程
* 符号引用Symbolic References符号引用以一组符号来描述所引用的目标可以是任何形式的字面量引用的目标并不一定已经加载到内存中与虚拟机内存布局无关。
* 直接引用Direct References直接引用可以是直接指向目标的指针相对偏移量或是一个能间接定位到目标的句柄。与虚拟机内存布局相关。
### 5 初始化
初始化时类加载的最后一步前面的类加载过程除了在加载阶段用户应用程序可以通过自定义类加载器参与之外其余动作完全由虚拟机主导和控制。到了初始化阶段才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程
1. <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成。编译器收集的顺序和语句在源文件中出现的顺序一致,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,只能赋值,不能访问
2. <clinit>()方法与类的构造函数<init>()不同,不需要显式的调用父类构造器,虚拟机会保证父类的<clinit>()在子类的之前完成。因此,虚拟机执行的第一个<clinit>()方法肯定是java.lang.Object.
3. 由于父类<clinit>()方法先执行,也就意味着父类中定义的静态语句要优先于子类的变量赋值操作。
4. <clinit>()方法并不是必须的,如果一个类没有静态语句块也没有对变量赋值操作,就不会生成
5. 接口中不能使用静态语句块,但仍有变量初始化赋值的操作,因此也会生成<clinit>()方法,但与类不同的是,接口的<clinit>()方法不需要执行父接口的<clinit>()方法。只有当父几口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的<clinit>()方法。
6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞,直到该方法执行完,如果在一个类的<clinit>()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽。
### 触发初始化
虚拟机规范严格规定了有且只有四种情况必须对类进行初始化(加载,验证,准备自动在之前开始)
1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时如果类没有进行初始化则先初始化。这4个字节码常见的出现场景是使用new关键字实例化对象的时候读取或设置静态字段被final修饰已在编译期把结果放入常量池的静态字段除外的时候以及调用一个类的静态方法的时候。
2. 反射调用时
3. 初始化一个类时,如果其父类还未初始化,则先出发父类初始化。
4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
这4种情况称为对类的主动引用其他情况称为被动引用。一下四种情况不会触发初始化
1. 对于访问静态字段只有直接定义这个字段的类才被初始化因此通过子类来引用父类中定义的静态字段只会触发父类的初始化而不会触发子类的初始化。但是对于HOTSPOT,会触发子类的加载。
2. 通过数组定义引用类,不会触发此类的初始化。
3. 常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
4. 接口的加载和类加载过程稍有不同接口不能有static代码段但接口中还是会生成<clinit>()类构造器,用于初始化接口中所定义的成员变量。 一个接口在初始化时,并不要求其父类也初始化了。
### 补充说明
* 只有当某个类初始化之后,才会调用类的静态代码块。
* 初始化过程一方面是唯一的,另一方面是线程安全的。所以通过静态语句块的单例模式非常合理。

View File

@@ -0,0 +1,177 @@
## 1 JVM Memory Model
### JVM内存模型和结构
Java虚拟机定义了在程序执行期间使用的各种run-time data areas 。 其中一些数据区域是在Java虚拟机启动时创建的仅在Java虚拟机退出时才被销毁。 其他数据区域是每个线程的。 在创建线程时创建每个线程的数据区域,并在线程退出时销毁每个数据区域。
![](image/2022-12-19-17-08-55.png)
### 堆区Heap Area
堆区代表运行时数据区,从中为所有类实例和数组分配内存,并在虚拟机启动期间创建。
自动存储管理系统回收对象的堆存储。 堆的大小可以是固定的或动态的(基于系统的配置),并且分配给堆区域的内存不必是连续的。
### 方法区Method Area
方法区域存储每个类的结构,例如运行时常量池; 领域和方法数据; 方法和构造函数的代码,包括用于类,实例和接口初始化的特殊方法。
方法区域是在虚拟机启动时创建的。 尽管从逻辑上讲它是堆的一部分,但是可以或不能将其进行垃圾收集,而我们已经读到堆中的垃圾收集不是可选的; 这是强制性的。 方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。 方法区域的内存不必是连续的。
### 堆栈区JVM Stacks
每个JVM线程都有一个与该线程同时创建的私有堆栈。 堆栈存储帧。 框架用于存储数据和部分结果,并执行动态链接,方法的返回值和调度异常。
它保存局部变量和部分结果,并在方法调用和返回中起作用。 因为除了压入和弹出帧外,从不直接操纵此堆栈,所以可以对帧进行堆分配。 与堆类似,此堆栈的内存不必是连续的。
该规范允许堆栈的大小可以是固定的,也可以是动态的。 如果具有固定大小,则在创建该堆栈时可以独立选择每个堆栈的大小。
### 原生堆栈Native method stacks
本机方法堆栈称为C堆栈 它支持本机方法用Java编程语言以外的其他语言编写的方法通常在创建每个线程时为每个线程分配。 无法加载本机方法并且自身不依赖于常规堆栈的Java虚拟机实现不需要提供本机方法堆栈。
本机方法堆栈的大小可以是固定的,也可以是动态的。
### PC registers
每个JVM线程都有其自己的程序计数器pc寄存器。 在任何时候每个JVM线程都在执行单个方法的代码即该线程的当前方法。
由于Java应用程序可以包含一些本机代码例如使用本机库因此本机和非本机方法有两种不同的方式。 如果该方法不是本机的即Java代码则PC寄存器包含当前正在执行的JVM指令的地址。 如果该方法是本地方法则未定义JVM的PC寄存器的值。
Java虚拟机的pc寄存器足够宽可以在特定平台上保存返回地址或本机指针。
## 2 Java 内存管理Memory management
### GC概念
Java中的Memory management是垃圾收集器的职责。
* allocating memory
* 确保所有引用的对象都保留在内存中,并且
* 恢复由执行代码中的引用无法访问的对象使用的内存。
在应用程序运行时,应用程序会创建许多对象,每个对象都有其生命周期。 在内存中被其他对象引用的对象被称为live objects 。 不再由任何活动对象引用的对象被视为dead objects 并称为garbage 。 查找和释放也称为回收这些对象使用的空间的过程称为garbage collection 。
垃圾回收解决了许多但不是全部的内存分配问题。 例如,我们可以无限期地创建对象并继续引用它们,直到没有更多可用内存为止( Out of memory error )。 垃圾收集是一项复杂的任务,需要花费时间和资源。 它在通常由称为堆的大型内存池分配的空间上运行。
垃圾收集的时间取决于垃圾收集器。 通常,整个堆或堆的一部分会在堆满或达到占用率的百分比时收集。
### 引用计数机制Reference counting mechanism
非常古老的GC机制。 在引用计数技术中,每个对象都有从其他对象和堆栈指向该对象的指针数。 每次引用新对象时,计数器都会增加一。 同样,当任何对象丢失其引用时,计数器将减一。 当count达到'0'时,垃圾回收器可以取消分配对象。
引用计数算法的主要advantage是分配给新对象时每次内存写入工作量少。 但是它在data cycles方面存在非常critical problem 。 这意味着当第一个对象被第二个对象引用,第二个对象被第一个对象( cyclic references cyclic references ,计数永远不会为零,因此它们永远不会被垃圾回收。
### 标记清除机制Mark and sweep mechanism
![](image/2022-12-19-17-19-40.png)
标记清除算法是第一个开发的able to reclaim cyclic data structures垃圾收集算法。 在这种算法中GC将首先将某些对象标识为默认可达对象这些对象通常是堆栈中的全局变量和局部变量。 有所谓的活动对象。
在下一步中,算法开始从这些活动对象中跟踪对象,并将它们也标记为活动对象。 继续执行此过程,直到检查所有对象并将其标记为活动。 完全跟踪后未标记为活动的对象被视为死对象。
使用标记扫描时,未引用的对象不会立即被回收。 取而代之的是,允许垃圾收集累积,直到所有可用内存都用完为止。 发生这种情况时该程序的执行会暂时暂停这称为stop the world ),而标记清除算法会收集所有垃圾。 一旦回收了所有未引用的对象,就可以恢复程序的正常执行。
除了暂停应用程序一段时间外该技术还需要经常对内存地址空间de-fragmentation整理这是另一项开销。
### 停止copy机制Stop and copy GC
像“标记和清除”一样,该算法还取决于识别活动对象并对其进行标记。 区别在于它处​​理活动对象的方式。
停止和复制技术将整个堆设计为两个semi-spaces 。 一次只有一个半空间处于活动状态,而为新创建的对象分配的内存仅发生在单个半空间中,而另一个保持平静。
GC运行时它将开始标记当前半空间中的活动对象完成后它将所有活动对象复制到其他半空间中。 当前半空间中的所有其余对象都被视为已死,并已被垃圾回收。
与以前的方法一样它具有一些advantages例如仅接触活动对象。 另外不需要分段因为在切换半memory contraction会完成memory contraction 。
这种方法的主要disadvantages是需要将所需的内存大小增加一倍因为在给定的时间点仅使用一半的内存。 除此之外,它还需要在切换半空间时停止世界。
### 世代停止复制机制Generational stop and copy
将内存划分为三个半空间。 这些半空间在这里称为世代。 因此,此技术中的内存分为三代: young generation old generation和permanent generation 。
大多数对象最初是在年轻一代中分配的。 老一代包含的对象在许多年轻一代集合中幸存下来,还有一些大型对象可以直接在老一代中分配。 永久生成包含JVM认为便于垃圾回收器管理的对象例如描述类和方法的对象以及类和方法本身。
当年轻一代填满时将执行该一代的年轻一代垃圾收集有时称为minor collection垃圾minor collection )。 当旧的或永久的一代填满时通常会完成所谓的完整垃圾回收有时称为major collection垃圾major collection )。 即,收集了所有的世代。
通常,首先使用专门为该代设计的垃圾收集算法来收集年轻代,因为它通常是识别年轻代中最有效的垃圾算法。 幸存于GC跟踪中的对象被推入更早的年代。 出于明显的原因,较老的一代被收集的频率较低,即它们在那里是因为时间更长。 除上述情况外,如果发生碎片/压缩,则每一代都将单独压缩。
该技术的主要advantages是可以在较年轻的一代中早期回收死对象而无需每次都扫描整个内存以识别死对象。 较早的对象已经经历了一些GC周期因此假定它们在系统中的存在时间更长因此无需频繁扫描它们不是每次都完美的情况但大多数情况下应该如此
Disadvantages还是一样即需要对存储区进行碎片整理并且需要在GC运行全扫描时停止整个环境应用程序
## 3 垃圾回收的策略
### mark and sweep标记清除算法
它是初始且非常基本的算法,分为两个阶段运行:
* Marking live objects –找出所有仍然存在的对象。
* Removing unreachable objects -摆脱所有其他东西-所谓的已死和未使用的对象。
第一阶段介绍:
1. mark live objects。首先GC将某些特定对象定义为“ Garbage Collection Roots 。 例如当前执行方法的局部变量和输入参数活动线程已加载类的静态字段和JNI引用。 现在GC遍历了内存中的整个对象图从这些根开始然后是从根到其他对象的引用。 GC访问的每个对象都被标记为活动对象。
第二阶段介绍
1. mark-sweep。Normal deletion 普通删除将未引用的对象删除以释放空间并保留引用的对象和指针。 内存分配器某种哈希表保存对可分配新对象的可用空间块的引用。它通常被称为mark-sweep算法。
![](image/2022-12-19-17-29-01.png)
1. mark-sweep-compact 。Deletion with compacting仅删除未使用的对象效率不高因为可用内存块分散在整个存储区域中并且如果创建的对象足够大且找不到足够大的内存块则会导致OutOfMemoryError 。为了解决此问题,删除未引用的对象后,将对其余的引用对象进行压缩。 这里的压缩指的是将参考对象一起移动的过程。 这使得新的内存分配变得更加容易和快捷。它通常被称为mark-sweep-compact算法。
![](image/2022-12-19-17-29-28.png)
4. mark-copy 。Deletion with copying –与标记和补偿方法非常相似,因为它们也会重新放置所有活动对象。 重要的区别是重定位的目标是不同的存储区域。它通常被称为mark-copy算法。
![](image/2022-12-19-17-29-53.png)
### Concurrent mark sweep (CMS)
CMS垃圾回收实质上是一种升级的标记和清除方法。 它using multiple threads扫描堆内存。 对其进行了修改以利用更快的系统并增强了性能。它尝试通过与应用程序线程concurrently执行大多数垃圾回收工作来最大程度地减少由于垃圾回收导致的暂停。 它在年轻一代中使用并行的世界停止mark-copy算法而在老一代中使用大多数并发的mark-sweep算法。
```java
-XX:+UseConcMarkSweepGC
```
### Serial garbage collection
该算法对年轻一代使用mark-copy 对老一代使用mark-copy mark-sweep-compact 。 它在单个线程上工作。 执行时它将冻结所有其他线程直到垃圾回收操作结束。由于串行垃圾回收具有线程冻结特性因此仅适用于非常小的程序。要使用串行GC请使用以下JVM参数
```java
-XX:+UseSerialGC
```
### Parallel garbage collection
与串行GC相似它在年轻一代中使用mark-copy 在旧一代中使用mark-sweep-compact 。 多个并发线程用于标记和复制/压缩阶段。 您可以使用-XX:ParallelGCThreads=N选项配置线程数。如果您的主要目标是通过有效利用现有系统资源来提高吞吐量那么Parallel Garbage Collector将适用于多核计算机。 使用这种方法可以大大减少GC循环时间。
```java
-XX:+UseParallelGC
```
### G1 garbage collection
G1垃圾优先垃圾收集器已在Java 7中提供旨在长期替代CMS收集器。 G1收集器是并行的并发的渐进压缩的低暂停垃圾收集器。此方法涉及将内存堆分段为多个小区域通常为2048。 每个区域都被标记为年轻一代(进一步划分为伊甸园地区或幸存者地区)或老一代。 这样GC可以避免立即收集整个堆而可以逐步解决问题。 这意味着一次只考虑区域的一个子集。
1. G1跟踪每个区域包含的实时数据量。 此信息用于确定包含最多垃圾的区域。 因此它们是首先收集的。
2. 这就是为什么它是名称garbage-first集合。与其他算法一样不幸的是压缩操作是使用Stop the World方法进行的。 但是根据其设计目标,您可以为其设置特定的性能目标。 您可以配置暂停持续时间例如在任何给定的秒内不超过10毫秒。 垃圾优先GC将尽最大可能但不能确定由于OS级线程管理这很难实时实现来尽力实现该目标。
![](image/2022-12-19-17-34-47.png)
```java
-XX:+UseG1GC
-XX:G1HeapRegionSize=16m based on the minimum Java heap size.
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=5
-XX:GCPauseIntervalMillis=200
```
### 总结
* 对象生命周期分为三个阶段,即对象创建,对象使用和对象销毁。
* mark-sweep mark-sweep-compact和mark-copy机制如何工作。
* 不同的单线程和并发GC算法。
* 直到Java 8并行GC才是默认算法。
* 从Java 9开始将G1设置为默认GC算法。

View File

@@ -0,0 +1,758 @@
# Java 虚拟机
<!-- GFM-TOC -->
* [Java 虚拟机](#java-虚拟机)
* [一、运行时数据区域](#一运行时数据区域)
* [程序计数器](#程序计数器)
* [Java 虚拟机栈](#java-虚拟机栈)
* [本地方法栈](#本地方法栈)
* [](#堆)
* [方法区](#方法区)
* [运行时常量池](#运行时常量池)
* [直接内存](#直接内存)
* [二、垃圾收集](#二垃圾收集)
* [判断一个对象是否可被回收](#判断一个对象是否可被回收)
* [引用类型](#引用类型)
* [垃圾收集算法](#垃圾收集算法)
* [垃圾收集器](#垃圾收集器)
* [三、内存分配与回收策略](#三内存分配与回收策略)
* [Minor GC 和 Full GC](#minor-gc-和-full-gc)
* [内存分配策略](#内存分配策略)
* [Full GC 的触发条件](#full-gc-的触发条件)
* [四、类加载机制](#四类加载机制)
* [类的生命周期](#类的生命周期)
* [类加载过程](#类加载过程)
* [类初始化时机](#类初始化时机)
* [类与类加载器](#类与类加载器)
* [类加载器分类](#类加载器分类)
* [双亲委派模型](#双亲委派模型)
* [自定义类加载器实现](#自定义类加载器实现)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
十四、Java 虚拟机Java 内存结构堆栈垃圾回收JVM 内存区域Java 虚拟机栈class 文件字节码指令JVM 参数调优Java 对象模型HotSpot类加载机制编译和反编译反编译工具javapJIT虚拟机性能监控和故障处理工具jps、jstack、jmap、jstat、jconsole、javap
https://link.zhihu.com/?target=https%3A//www.bilibili.com/video/BV1DJ411B7cG%3Ffrom%3Dsearch%26seid%3D1705641531822657605
本文大部分内容参考 **周志明《深入理解 Java 虚拟机》** ,想要深入学习的话请看原书。
## 一、运行时数据区域
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5778d113-8e13-4c53-b5bf-801e58080b97.png" width="400px"> </div><br>
### 程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
### Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8442519f-0b4d-48f4-8229-56f984363c69.png" width="400px"> </div><br>
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K而在 JDK 1.5+ 默认为 1M
```java
java -Xss2M HackTheJava
```
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
### 本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66a6899d-c6b0-4a47-8569-9d08f0baf86c.png" width="300px"> </div><br>
### 堆
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
- 新生代Young Generation
- 老年代Old Generation
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
```java
java -Xms1M -Xmx2M HackTheJava
```
### 方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
### 运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
### 直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
## 二、垃圾收集
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
### 判断一个对象是否可被回收
#### 1. 引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0导致无法对它们进行回收。正是因为循环引用的存在因此 Java 虚拟机不使用引用计数算法。
```java
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
```
在上述代码中a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
#### 2. 可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收GC Roots 一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/83d909d2-3858-4fe1-8ff4-16471db0b180.png" width="350px"> </div><br>
#### 3. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
#### 4. finalize()
类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
### 引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 提供了四种强度不同的引用类型。
#### 1. 强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
```java
Object obj = new Object();
```
#### 2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
```java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
```
#### 3. 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用 WeakReference 类来创建弱引用。
```java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
```
#### 4. 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来创建虚引用。
```java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
```
### 垃圾收集算法
#### 1. 标记 - 清除
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/005b481b-502b-4e3f-985d-d043c2b330aa.png" width="400px"> </div><br>
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size会直接返回这个分块如果找到的块大于 size会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
#### 2. 标记 - 整理
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ccd773a5-ad38-4022-895c-7ac318f31437.png" width="400px"> </div><br>
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:
- 不会产生内存碎片
不足:
- 需要移动大量对象,处理效率比较低。
#### 3. 复制
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b2b77b9e-958c-4016-8ae5-9c6edd83871e.png" width="400px"> </div><br>
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
#### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
### 垃圾收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c625baa0-dde6-449e-93df-c3a67f2f430f.jpg" width=""/> </div><br>
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
#### 1. Serial 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
#### 2. ParNew 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg" width=""/> </div><br>
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
#### 3. Parallel Scavenge 收集器
与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略GC Ergonomics就不需要手工指定新生代的大小-Xmn、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
#### 4. Serial Old 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg" width=""/> </div><br>
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
- 在 JDK 1.5 以及之前版本Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
#### 5. Parallel Old 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/278fe431-af88-4a95-a895-9c3b80117de3.jpg" width=""/> </div><br>
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
#### 6. CMS 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/62e77997-6957-4b68-8d12-bfd609bb2c68.jpg" width=""/> </div><br>
CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
#### 7. G1 收集器
G1Garbage-First它是一款面向服务端应用的垃圾收集器在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
G1 把堆划分成多个大小相等的独立区域Region新生代和老年代不再物理隔离。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br>
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set在做可达性分析的时候就可以避免全堆扫描。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f99ee771-c56f-47fb-9148-c0036695b5fe.jpg" width=""/> </div><br>
如果不计算维护 Remembered Set 的操作G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region时间是用户可控制的而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
## 三、内存分配与回收策略
### Minor GC 和 Full GC
- Minor GC回收新生代因为新生代对象存活时间很短因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC回收老年代和新生代老年代对象其存活时间长因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
### 内存分配策略
#### 1. 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
#### 2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold大于此值的对象直接在老年代分配避免在 Eden 和 Survivor 之间的大量内存复制。
#### 3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
#### 4. 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
#### 5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
### Full GC 的触发条件
对于 Minor GC其触发条件非常简单当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
#### 1. 调用 System.gc()
只是建议虚拟机执行 Full GC但是虚拟机不一定真正去执行。不建议使用这种方式而是让虚拟机管理内存。
#### 2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC应当尽量不要创建过大的对象以及数组。除此之外可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
#### 3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
#### 4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC。
#### 5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
## 四、类加载机制
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
### 类的生命周期
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/335fe19c-4a76-45ab-9320-88c90d6a0d7e.png" width="600px"> </div><br>
包括以下 7 个阶段:
- **加载Loading**
- **验证Verification**
- **准备Preparation**
- **解析Resolution**
- **初始化Initialization**
- 使用Using
- 卸载Unloading
### 类加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。
#### 1. 加载
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
#### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
#### 3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
```java
public static int value = 123;
```
如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。
```java
public static final int value = 123;
```
#### 4. 解析
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
#### 5. 初始化
<div data="modify -->"></div>
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 &lt;clinit\>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
&lt;clinit\>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
```java
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
```
由于父类的 &lt;clinit\>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
```java
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
```
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 &lt;clinit\>() 方法。但接口与类不同的是,执行接口的 &lt;clinit\>() 方法不需要先执行父接口的 &lt;clinit\>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 &lt;clinit\>() 方法。
虚拟机会保证一个类的 &lt;clinit\>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit\>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit\>() 方法完毕。如果在一个类的 &lt;clinit\>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
### 类初始化时机
#### 1. 主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
#### 2. 被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
SuperClass[] sca = new SuperClass[10];
```
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
```
### 类与类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
### 类加载器分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器Bootstrap ClassLoader使用 C++ 实现,是虚拟机自身的一部分;
- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器Bootstrap ClassLoader此类加载器负责将存放在 &lt;JRE_HOME\>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器Extension ClassLoader这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将 &lt;JAVA_HOME\>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器Application ClassLoader这个类加载器是由 AppClassLoadersun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值因此一般称为系统类加载器。它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器。
### 双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
下图展示了类加载器之间的层次关系称为双亲委派模型Parents Delegation Model。该模型要求除了顶层的启动类加载器外其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系Composition来实现而不是继承关系Inheritance
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0dd2d40a-5b2b-4d45-b176-e75a4cd4bdbf.png" width="500px"> </div><br>
#### 1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
#### 2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
#### 3. 实现
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException此时尝试自己去加载。
```java
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
```
### 自定义类加载器实现
以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
```java
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
```
## 参考资料
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,407 @@
# Java 标准IO
<!-- GFM-TOC -->
- [Java 标准IO](#java-标准io)
- [1 概览](#1-概览)
- [IO定义](#io定义)
- [虚拟内存](#虚拟内存)
- [IO的分类](#io的分类)
- [装饰者模式](#装饰者模式)
- [2 字节操作](#2-字节操作)
- [InputStream](#inputstream)
- [OutputStream](#outputstream)
- [3 字符操作](#3-字符操作)
- [编码与解码](#编码与解码)
- [String 的编码方式](#string-的编码方式)
- [字节字符流转换Reader 与 Writer](#字节字符流转换reader-与-writer)
- [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
- [Reader](#reader)
- [Writer](#writer)
- [参考文献](#参考文献)
<!-- GFM-TOC -->
## 1 概览
![](image/2022-07-12-11-35-43.png)
本篇文章的范围应该是涵盖java.io包中的所有类。
### IO定义
> 该理解方式非常有价值。为什么IO的同步和异步如此重要因为IO操作是整个调用链路上的性能瓶颈。普通的A函数调用B函数为什么不采用异步操作呢因为函数都是计算任务都在内存完成。所以所有的操作都可以分为两种计算操作非IO操作内存中即可完成和IO操作从其他设备中读取、写入数据。计算操作是使用CPU的IO操作过程中CPU线程是挂起的等待中。函数、调用可以分为两种正常调用和IO调用。
缓冲区以及如何处理缓冲区是所有I / O的基础。 术语“输入/输出”仅意味着将数据移入和移出缓冲区。 只要时刻牢记这一点即可。 通常,进程通过请求操作系统从缓冲区中清空数据( write operation )或向缓冲区中填充数据( read operation 来执行I / O。 以上是I / O概念的全部摘要。
![](image/2022-11-26-17-31-29.png)
上图显示了块数据如何从外部源例如硬盘移动到正在运行的进程例如RAM内部的存储区的简化“逻辑”图。
1. 首先该进程通过进行read()系统调用来请求填充其缓冲区。
2. 此调用导致内核向磁盘控制器硬件发出命令以从磁盘获取数据。 磁盘控制器通过DMA将数据直接写入内核内存缓冲区而无需主CPU的进一步协助。
3. 磁盘控制器完成缓冲区填充后当它请求read()操作时。内核将数据从内核空间中的临时缓冲区复制到进程指定的缓冲区中;
4. 需要注意的一件事是内核尝试缓存和/或预取数据,因此进程请求的数据可能已经在内核空间中可用。 如果是这样,则将过程所请求的数据复制出来。 如果数据不可用,则该过程将在内核将数据带入内存时挂起。
### 虚拟内存
虚拟内存具有两个重要优点:
1多个虚拟地址可以引用相同的物理内存位置。
2虚拟内存空间可以大于可用的实际硬件内存。
对于第一个特性通过地址映射mmap将内核空间逻辑地址与用户空间中虚拟地址映射到相同物理空间。DMA硬件只能访问物理内存地址可以填充一个缓冲区该缓冲区同时对内核和用户空间进程可见。消除了内核空间和用户空间之间的副本
![](image/2022-11-26-18-03-55.png)
对于第二个特性,进行虚拟内存分页(通常称为交换)。将虚拟内存空间的页面持久保存到外部磁盘存储中,从而在物理内存中为其他虚拟页面腾出空间。物理内存充当页面调度区域的缓存,当虚拟内存的不在物理内存中时,由物理内存从磁盘空间交换。
### IO的分类
Java 的 I/O可以根据如下方式进行分类。
![](image/2022-11-26-19-31-24.png)
![](image/2022-11-26-19-20-23.png)
![](image/2022-11-27-00-08-19.png)
根据内容的不同可以分为:
- 字节操作InputStream 和 OutputStream
- 字符操作Reader 和 Writer
- 输入流 InputStream、Reader
- 输出流 OutputStream、Writer
- 字节转字符InputStreamReader/OutputStreamWriter
根据对象的不同可以分为:
- 文件操作File
- 管道操作Piped
- 数组操作ByteArray&CharArray
- 对象操作Object
- 过滤操作Filter添加额外特性
- Bufferd
- Data
- PushBack
- LineNumber
- 网络操作Socket
根据IO模型的不同可以分为
- 同步阻塞IOBIO
- 异步IONIO
- 异步IOAIO
根据IO的原理可以分为
* Block IO 块IO
* Stream IO 流IO
### 装饰者模式
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9709694b-db05-4cce-8d2f-1c8b09f4d921.png" width="650px"> </div><br>
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
## 2 字节操作
![](image/2022-11-26-19-40-09.png)
### InputStream
* 基本的InputStream
```
int read()
int read(byte[] b)
int read(byte[] b, int off, int len)
void close()
long skip(long n)
```
* FileInputStream
```java
FileInputStream(File file)
FileInputStream(String name)
```
* PipedInputStream
```java
PipedInputStream(int pipeSize)
PipedInputStream(PipedOutputStream src)
PipedInputStream(PipedOutputStream src, int pipeSize)
connect(PipedOutputStream src)
receive(int b)
```
* ByteArrayInputStream
```java
ByteArrayInputStream(byte[] buf)
ByteArrayInputStream(byte[] buf, int offset, int length)
```
* ObjectInputStream 持久化对象。用ObjectOutputStream写出就用这个读入
```java
ObjectInputStream(InputStream in)
readBoolean()ByteCharDoubleFloatIntLong
readFully()
readObject()
```
* FilterInputStream->BufferedInputStream
```java
```
### OutputStream
* 基本的OutputStream
```
close()
flush()
write(byte[] b)
write(byte[] b, int off, int len)
write(int b)
```
* FileOutputStream
```java
FileOutputStream(File file)
FileOutputStream(File file, boolean append)
FileOutputStream(String name)
FileOutputStream(String name, boolean append)
```
* PipedOutputStream
```java
PipedOutputStream(PipedInputStream snk)
connect(PipedInputStream snk)
```
* ByteArrayOutputStream自带一个字节缓冲区
```java
ByteArrayOutputStream(int size)
size()
```
* ObjectOutputStream
```java
ObjectOutputStream(OutputStream out)
writeBoolean(boolean val)
writeByte(int val)
writeBytes(String str)
writeChar(int val)
writeChars(String str)
writeInt(int val)
writeLong(long val)
writeObject(Object obj)
```
* FilterOutputStream->BufferedOutputStream
* PrintStream一个特殊的包装器类
```java
print(基础类型和对象)
printf(String format, Object... args)
println(基础类型和对象)
```
## 3 字符操作
![](image/2022-11-26-20-24-47.png)
### 编码与解码
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
- UTF-16 be编码中中文字符和英文字符都占 2 个字节。
UTF-16be 中的 be 指的是 Big Endian也就是大端。相应地也有 UTF-16lele 指的是 Little Endian也就是小端。
Java 的内存编码使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位也就是两个字节Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
### String 的编码方式
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
```java
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
```java
byte[] bytes = str1.getBytes();
```
### 字节字符流转换Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
- InputStreamReader 实现从字节流解码成字符流;
```java
InputStreamReader(InputStream in)
InputStreamReader(InputStream in, Charset cs)
InputStreamReader(InputStream in, CharsetDecoder dec)
InputStreamReader(InputStream in, String charsetName)
getEncoding()
```
- OutputStreamWriter 实现字符流编码成为字节流。
```java
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, Charset cs)
OutputStreamWriter(OutputStream out, CharsetEncoder enc)
OutputStreamWriter(OutputStream out, String charsetName)
getEncoding()
```
### 实现逐行输出文本文件的内容
```java
public static void readFileContent(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
### Reader
* 基本的Reader
```java
close()
read()
read(char[] cbuf)
read(char[] cbuf, int off, int len)
read(CharBuffer target)
ready()
skip(long n)
```
* FileReader
```java
FileReader(File file)
FileReader(String fileName)
```
* PipedReader
```java
PipedReader(int pipeSize)
PipedReader(PipedWriter src)
PipedReader(PipedWriter src, int pipeSize)
connect(PipedWriter src)
```
* CharArrayReader
```
CharArrayReader(char[] buf)
CharArrayReader(char[] buf, int offset, int length)
```
* BufferedReader
```
String readLine()
Stream<String> lines()
```
### Writer
* 基本的Writer
```java
append(char c)
Writer append(CharSequence csq)
Writer append(CharSequence csq, int start, int end)
close()
flush()
write(char[] cbuf)
write(char[] cbuf, int off, int len)
write(int c)
write(String str)
write(String str, int off, int len)
```
* FileWriter
```java
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, Charset cs)
OutputStreamWriter(OutputStream out, CharsetEncoder enc)
OutputStreamWriter(OutputStream out, String charsetName)
getEncoding()
```
* PipedWriter
```java
PipedWriter(PipedReader snk)
connect(PipedReader snk)
```
* CharArrayWriter
```java
size()
char[] toCharArray()
String toString(
```
* BufferedWriter
```
BufferedWriter(Writer out)
BufferedWriter(Writer out, int sz)
newLine()
```
* PrintWriter一个特殊的装饰器类在write的基础上添加了许多print函数
```java
print(基础类型和对象)
printf(String format, Object... args)
println(基础类型和对象)
```
## 参考文献
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)

View File

@@ -0,0 +1,715 @@
> 参考文献
> * [java nio一篇博客](https://blog.csdn.net/forezp/article/details/88414741)
> * [java nio并发编程网](http://ifeve.com/overview/)
# NIO
## 1 NIO概述
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
IO Stream 和 NIO Block。NIO将最耗时的IO活动即填充和清空缓冲区移回操作系统从而极大地提高了速度。
阻塞 I/O(blocking I/O)是旧的输入/输出(old input/outputOIO)。被称为普通 I/O(plain I/O)
### 标准IO与NIO的区别一流与块
I/O 与 NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
* 面向流的I / O系统一次处理一个或多个字节的数据。 输入流产生一个字节的数据,而输出流消耗一个字节的数据。为流数据创建过滤器非常容易。 将几个过滤器链接在一起也是相对简单的,这样每个过滤器都能发挥自己的作用,相当于一个单一的复杂处理机制。 重要的是字节不会在任何地方缓存。 此外,您不能在流中的数据中来回移动。 如果需要来回移动从流中读取的数据,则必须先将其缓存在缓冲区中。
* 面向块的I / O系统按块处理数据。 每个操作一步就产生或消耗一个数据块。 通过块可以处理数据,比处理(流式传输)字节快得多。 您可以根据需要在缓冲区中来回移动。 这使您在处理过程中更具灵活性。 但是,您还需要检查缓冲区是否包含您需要的所有数据,以便对其进行完全处理。 并且,您需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。 但是面向块的I / O缺少面向流的I / O的一些优雅和简单性。
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
### 标准IO与NIO区别二同步异步
标准IO和NIO第二个主要的区别是同步和异步。同步程序通常不得不诉诸于轮询或创建许多线程来处理大量连接。 使用异步I / O您可以在任意数量的通道上侦听I / O事件而无需轮询且无需额外的线程。异步I / O中的中心对象称为选择器。
* Java IO当线程调用read或write该线程将被阻塞直到有一些数据要读取或数据被完全写入为止。因此引入多线程增加并发性。
* 异步IO中线程可以请求将某些数据写入通道但不等待将其完全写入。 然后线程可以继续运行,同时执行其他操作。 单个线程现在可以管理输入和输出的多个通道。
![](image/2022-11-27-10-06-38.png)
## 2 Path
Path是NIO的入口点。
### 绝对路径
绝对路径始终包含根元素和查找文件所需的完整目录列表。 不再需要更多信息来访问文件或路径。
```java
//Starts with file store root or drive
Path absolutePath1 = Paths.get("C:/Lokesh/Setup/workspace/NIOExamples/src", "sample.txt");
Path absolutePath2 = Paths.get("C:/Lokesh/Setup/workspace", "NIOExamples/src", "sample.txt");
Path absolutePath3 = Paths.get("C:/Lokesh", "Setup/workspace", "NIOExamples/src", "sample.txt");
```
### 相对路径
```java
Path relativePath1 = Paths.get("src", "sample.txt");
```
### 通过Uri
将格式为“ file///src/someFile.txt”的文件路径转换为NIO路径。 让我们来看看如何做。
```java
//URI uri = URI.create("file:///c:/Lokesh/Setup/workspace/NIOExamples/src/sample.txt"); //OR
URI uri = URI.create("file:///Lokesh/Setup/workspace/NIOExamples/src/sample.txt");
String scheme = uri.getScheme();
if (scheme == null)
throw new IllegalArgumentException("Missing scheme");
//Check for default provider to avoid loading of installed providers
if (scheme.equalsIgnoreCase("file"))
{
System.out.println(FileSystems.getDefault().provider().getPath(uri).toAbsolutePath().toString());
}
//If you do not know scheme then use this code. This code check file scheme as well if available.
for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
if (provider.getScheme().equalsIgnoreCase(scheme)) {
System.out.println(provider.getPath(uri).toAbsolutePath().toString());
break;
}
}
```
## 3 Buffer缓冲区
### 缓冲区
Buffer对象可以称为固定数量数据的容器。 它充当存储箱或临时暂存区,可以在其中存储数据并在以后检索。
通道是进行I / O传输的实际门户。 缓冲区是这些数据传输的源或目标。
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区包括以下类型,能够包含所有的类型。
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
### 缓冲区状态变量
所有缓冲区拥有的四个属性可提供有关所包含数据元素的信息。 这些是:
* Capacity :缓冲区可以容纳的最大数据元素数。 容量是在创建缓冲区时设置的,无法更改。
* Limit :不应读取或写入的缓冲区的第一个元素。 换句话说,缓冲区中活动元素的数量。
* Position :下一个要读取或写入的元素的索引。 该位置由相对的get和put方法自动更新。
* Mark :记忆中的位置。 调用mark设置mark =位置。 调用reset设置position =标记。 该标记在设置之前是不确定的。
> 0<= mark <= position <= limit <= capacity
状态变量的改变过程举例:
① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
![](image/2022-11-27-00-33-15.png)
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5limit 保持不变。
![](image/2022-11-27-00-33-28.png)
③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position并将 position 设置为 0。
![](image/2022-11-27-00-33-35.png)
④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
![](image/2022-11-27-00-33-45.png)
⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
![](image/2022-11-27-00-33-59.png)
### 创建缓冲区
缓冲区类都不能直接实例化。 它们都是抽象类,但是每个都包含静态工厂方法来创建相应类的新实例。
```java
CharBuffer charBuffer = CharBuffer.allocate (100);
//调用put()对缓冲区所做的更改将反映在数组中,而直接对数组所做的任何更改将对缓冲区对象可见。
char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
```
### 反转缓冲区:从写状态转换为读状态
```java
public abstract class ByteBuffer extends Buffer implements Comparable
{
// This is a partial API listing
// get到当前元素postion到下一个元素
public abstract byte get();
// get指定元素
public abstract byte get (int index);
public abstract ByteBuffer put (byte b);
public abstract ByteBuffer put (int index, byte b);
}
```
* flip方法将缓冲区从可以附加数据元素的填充状态翻转到耗尽状态以准备读取元素 。
![](image/2022-11-27-00-48-14.png)
```java
buffer.flip();
buffer.limit( buffer.position() ).position(0);
```
* clear()方法将缓冲区重置为空状态。 它不会更改缓冲区的任何数据元素而只是将限制设置为容量并将位置设置回0。这使缓冲区可以再次填充。
```java
import java.nio.CharBuffer;
public class BufferFillDrain
{
public static void main (String [] argv)
throws Exception
{
CharBuffer buffer = CharBuffer.allocate (100);
while (fillBuffer (buffer)) {
buffer.flip( );
drainBuffer (buffer);
buffer.clear();
}
}
private static void drainBuffer (CharBuffer buffer)
{
while (buffer.hasRemaining()) {
System.out.print (buffer.get());
}
System.out.println("");
}
private static boolean fillBuffer (CharBuffer buffer)
{
if (index >= strings.length) {
return (false);
}
String string = strings [index++];
for (int i = 0; i > string.length( ); i++) {
buffer.put (string.charAt (i));
}
return (true);
}
private static int index = 0;
private static String [] strings = {
"Some random string content 1",
"Some random string content 2",
"Some random string content 3",
"Some random string content 4",
"Some random string content 5",
"Some random string content 6",
};
}
```
## 4 Channel通道
### Channel概念
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
- FileChannel从文件中读写数据
- DatagramChannel通过 UDP 读写网络中数据;
- SocketChannel通过 TCP 读写网络中数据;
- ServerSocketChannel可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
### 创建channel
* FileChannel:只能通过在打开的RandomAccessFile FileInputStream或FileOutputStream对象上调用getChannel()方法来获取FileChannel对象。 您不能直接创建FileChannel对象。
```java
RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel();
```
* SocketChannel:套接字通道具有工厂方法来直接创建新的套接字通道。
```java
//How to open SocketChannel
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("somehost", someport));
//How to open ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind (new InetSocketAddress (somelocalport));
//How to open DatagramChannel
DatagramChannel dc = DatagramChannel.open();
```
### 使用Channel
> 可以发现通过Channel转换buffer上的数据而不需要直接操作buffer。
* 它通过实现不同的接口,表示其是双向或者单向的。 连接到只读文件的Channel实例无法写入。
* 快速复制文件
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
public class ChannelCopyExample
{
public static void main(String args[]) throws IOException
{
FileInputStream input = new FileInputStream ("testIn.txt");
ReadableByteChannel source = input.getChannel();
FileOutputStream output = new FileOutputStream ("testOut.txt");
WritableByteChannel dest = output.getChannel();
copyData(source, dest);
source.close();
dest.close();
}
private static void copyData(ReadableByteChannel src, WritableByteChannel dest) throws IOException
{
ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
while (src.read(buffer) != -1)
{
// Prepare the buffer to be drained
buffer.flip();
// Make sure that the buffer was fully drained
while (buffer.hasRemaining())
{
dest.write(buffer);
}
// Make the buffer empty, ready for filling
buffer.clear();
}
}
}
```
### Vectored IO
从通道读取的分散数据是将数据读取到多个缓冲区中的读取操作。 因此,通道将数据从通道“ scatters ”到多个缓冲区中。 收集到通道的写操作是一种写操作,它将来自多个缓冲区的数据写到单个通道中。 因此通道gathers来自多个缓冲区的数据“ gathers ”到一个通道中。 在需要分别处理传输数据的各个部分的情况下,散布/收集可能非常有用。
在此示例中,我创建了两个缓冲区。 一个缓冲区将存储一个随机数,另一个缓冲区将存储一个随机字符串。 我将使用GatheringByteChannel读取写入文件通道中两个缓冲区中存储的数据。 然后我将使用ScatteringByteChannel将文件中的数据读回到两个单独的缓冲区中并在控制台中打印内容以验证存储和检索的数据是否匹配。
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;
public class ScatteringAndGatheringIOExample
{
public static void main(String params[])
{
String data = "Scattering and Gathering example shown in howtodoinjava.com";
gatherBytes(data);
scatterBytes();
}
/*
* gatherBytes() reads bytes from different buffers and writes to file
* channel. Note that it uses a single write for both the buffers.
*/
public static void gatherBytes(String data)
{
//First Buffer holds a random number
ByteBuffer bufferOne = ByteBuffer.allocate(4);
//Second Buffer holds data we want to write
ByteBuffer buffer2 = ByteBuffer.allocate(200);
//Writing Data sets to Buffer
bufferOne.asIntBuffer().put(13);
buffer2.asCharBuffer().put(data);
//Calls FileOutputStream(file).getChannel()
GatheringByteChannel gatherer = createChannelInstance("test.txt", true);
//Write data to file
try
{
gatherer.write(new ByteBuffer[] { bufferOne, buffer2 });
}
catch (Exception e)
{
e.printStackTrace();
}
}
/*
* scatterBytes() read bytes from a file channel into a set of buffers. Note that
* it uses a single read for both the buffers.
*/
public static void scatterBytes()
{
//First Buffer holds a random number
ByteBuffer bufferOne = ByteBuffer.allocate(4);
//Second Buffer holds data we want to write
ByteBuffer bufferTwo = ByteBuffer.allocate(200);
//Calls FileInputStream(file).getChannel()
ScatteringByteChannel scatterer = createChannelInstance("test.txt", false);
try
{
//Reading from the channel
scatterer.read(new ByteBuffer[] { bufferOne, bufferTwo });
}
catch (Exception e)
{
e.printStackTrace();
}
//Read the buffers seperately
bufferOne.rewind();
bufferTwo.rewind();
int bufferOneContent = bufferOne.asIntBuffer().get();
String bufferTwoContent = bufferTwo.asCharBuffer().toString();
//Verify the content
System.out.println(bufferOneContent);
System.out.println(bufferTwoContent);
}
public static FileChannel createChannelInstance(String file, boolean isOutput)
{
FileChannel fc = null;
try
{
if (isOutput) {
fc = new FileOutputStream(file).getChannel();
} else {
fc = new FileInputStream(file).getChannel();
}
}
catch (Exception e) {
e.printStackTrace();
}
return fc;
}
}
```
### 内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
## 5 选择器
NIO 常常被叫做非阻塞 IO主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/093f9e57-429c-413a-83ee-c689ba596cef.png" width="350px"> </div><br>
### 1. 创建选择器
```java
Selector selector = Selector.open();
```
### 2. 将通道注册到选择器上
```java
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
它们在 SelectionKey 的定义如下:
```java
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
```
可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
```java
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
### 3. 监听事件
```java
int num = selector.select();
```
使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
### 4. 获取到达的事件
```java
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
```
### 5. 事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
```java
while (true) {
int num = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}
```
## 6 Socket套接字 NIO 实例
```java
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
```
```java
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}
```
## 7 文件IO
### 使用标准IO的示例代码
```java
import java.io.FileReader;
import java.io.IOException;
public class WithoutNIOExample
{
public static void main(String[] args)
{
BufferedReader br = null;
String sCurrentLine = null;
try
{
br = new BufferedReader(
new FileReader("test.txt"));
while ((sCurrentLine = br.readLine()) != null)
{
System.out.println(sCurrentLine);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
if (br != null)
br.close();
} catch (IOException ex)
{
ex.printStackTrace();
}
}
}
}
```
### NIO 读取文件
```java
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ReadFileWithFixedSizeBuffer
{
public static void main(String[] args) throws IOException
{
RandomAccessFile aFile = new RandomAccessFile
("test.txt", "r");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(inChannel.read(buffer) > 0)
{
buffer.flip();
for (int i = 0; i < buffer.limit(); i++)
{
System.out.print((char) buffer.get());
}
buffer.clear(); // do something with the data and clear/compact it.
}
inChannel.close();
aFile.close();
}
}
```

View File

@@ -0,0 +1,973 @@
<!-- GFM-TOC -->
- [File IO](#file-io)
- [0 File对象](#0-file对象)
- [File](#file)
- [RandomAccessFile](#randomaccessfile)
- [NIO:Files](#niofiles)
- [1 列出list](#1-列出list)
- [BIO](#bio)
- [2 复制copy](#2-复制copy)
- [BIO](#bio-1)
- [NIO](#nio)
- [common-util](#common-util)
- [3 删除delete](#3-删除delete)
- [NIO](#nio-1)
- [apache commons-io](#apache-commons-io)
- [4 创建create](#4-创建create)
- [BIOcreateNewFile](#biocreatenewfile)
- [BIOFileOutputStream](#biofileoutputstream)
- [NIO](#nio-2)
- [5 写入wrirte\&append](#5-写入wrirteappend)
- [BIO:BufferedWriter](#biobufferedwriter)
- [BIO:PrintWriter](#bioprintwriter)
- [BIO:FileOutputStream](#biofileoutputstream-1)
- [BIO:DataOutputStream](#biodataoutputstream)
- [NIO:FileChannel](#niofilechannel)
- [NIO:Files静态方法](#niofiles静态方法)
- [6 读取read](#6-读取read)
- [BIO:BufferedReader按行读](#biobufferedreader按行读)
- [BIO:FileInputStream 读取字节](#biofileinputstream-读取字节)
- [NIO:Files按行读](#niofiles按行读)
- [NIO:读取所有字节](#nio读取所有字节)
- [commons-io](#commons-io)
- [7 Properties](#7-properties)
- [补充:静态内部类与懒汉式单例模式](#补充静态内部类与懒汉式单例模式)
- [8 Resource File](#8-resource-file)
- [在spring中可以这样](#在spring中可以这样)
- [10 读写utf8数据](#10-读写utf8数据)
- [11 从控制台读取输入](#11-从控制台读取输入)
- [console对象](#console对象)
- [System.in封装](#systemin封装)
- [更加复杂的Scanner](#更加复杂的scanner)
- [12 将String转换成输入流](#12-将string转换成输入流)
- [ByteArrayInputStream](#bytearrayinputstream)
- [apach.IOUtils](#apachioutils)
<!-- GFM-TOC -->
# File IO
## 0 File对象
### File
java.io.File
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。有大量相关的方法
```java
File(String pathname)
File(URI uri)
createNewFile()
createTempFile(String prefix, String suffix)
delete()
deleteOnExit()
exists()
getAbsoluteFile()
getAbsolutePath()
getName()
getPath()
isFile()
isDirectory()
list()
listFiles()
mkdir()
mkdirs()
setReadOnly()
setExecutable(boolean executable)
setWritable(boolean writable)
toPath()
toURI()
```
### RandomAccessFile
java.io.RandomAccessFile
RandomAccessFile支持"随机访问"的方式,程序可以直接跳转到文件的任意地方来读写数据。
* RandomAccessFile可以自由访问文件的任意位置。
* RandomAccessFile允许自由定位文件记录指针。
* RandomAccessFile只能读写文件而不是流。
```java
RandomAccessFile(String name, String mode)
RandomAccessFile(File file, String mode)
read*()
write*()
long getFilePointer()返回文件记录指针的当前位置(native方法)
void seek(long pos)将文件记录指针定位到pos位置(调用本地方法seek0)
```
* 使用randomaccessfile插入内容。RandomAccessFile依然不能向文件的指定位置插入内容如果直接将文件记录指针移动到中间某位置后开始输出则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容程序需要先把插入点后面的内容读入缓冲区等把需要插入的数据写入文件后再将缓冲区的内容追加到文件后面。
```java
/**
* 向指定文件的指定位置插入指定的内容
*
* @param fileName 指定文件名
* @param pos 指定文件的指定位置
* @param insertContent 指定文件的指定位置要插入的指定内容
*/
public static void insert(String fileName, long pos,
String insertContent) throws IOException {
RandomAccessFile raf = null;
//创建一个临时文件来保存插入点后的数据
File tmp = File.createTempFile("tmp", null);
FileOutputStream tmpOut = null;
FileInputStream tmpIn = null;
tmp.deleteOnExit();
try {
raf = new RandomAccessFile(fileName, "rw");
tmpOut = new FileOutputStream(tmp);
tmpIn = new FileInputStream(tmp);
raf.seek(pos);
//--------下面代码将插入点后的内容读入临时文件中保存---------
byte[] bbuf = new byte[64];
//用于保存实际读取的字节数
int hasRead = 0;
//使用循环方式读取插入点后的数据
while ((hasRead = raf.read(bbuf)) > 0) {
//将读取的数据写入临时文件
tmpOut.write(bbuf, 0, hasRead);
}
//----------下面代码插入内容----------
//把文件记录指针重新定位到pos位置
raf.seek(pos);
//追加需要插入的内容
raf.write(insertContent.getBytes());
//追加临时文件中的内容
while ((hasRead = tmpIn.read(bbuf)) > 0) {
raf.write(bbuf, 0, hasRead);
}
} finally {
if (raf != null) {
raf.close();
}
}
}
```
### NIO:Files
```java
Files.exists(path); //true
```
## 1 列出list
### BIO
递归地列出一个目录下所有文件:
```java
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
```
从 Java7 开始,可以使用 Paths 和 Files 代替 File。
## 2 复制copy
### BIO
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 利用文件输入流与输出流实现文件的复制操作
*/
public class CopyDemo {
public static void main(String[] args) throws IOException {
//用文件输入流读取待复制的文件
FileInputStream fis = new FileInputStream("01.rmvb");
//用文件输出流向复制文件中写入复制的数据
FileOutputStream fos = new FileOutputStream("01_cp.rmvb");
int d;//先定义一个变量,用于记录每次读取到的数据
long start = System.currentTimeMillis();//获取当前系统时间
while ((d = fis.read()) != -1) {
fos.write(d);
}
long end = System.currentTimeMillis();
System.out.println("复制完毕!耗时:" + (end - start) + "ms");
fis.close();
fos.close();
}
}
//带缓冲区的
```java
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[20 * 1024];
int cnt;
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}
in.close();
out.close();
}
```
### NIO
```java
package com.howtodoinjava.examples.io;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
public class DirectoryCopyExample
{
public static void main(String[] args) throws IOException
{
//Source directory which you want to copy to new location
File sourceFolder = new File("c:\\temp");
//Target directory where files should be copied
File destinationFolder = new File("c:\\tempNew");
//Call Copy function
copyFolder(sourceFolder, destinationFolder);
}
/**
* This function recursively copy all the sub folder and files from sourceFolder to destinationFolder
* */
private static void copyFolder(File sourceFolder, File destinationFolder) throws IOException
{
//Check if sourceFolder is a directory or file
//If sourceFolder is file; then copy the file directly to new location
if (sourceFolder.isDirectory())
{
//Verify if destinationFolder is already present; If not then create it
if (!destinationFolder.exists())
{
destinationFolder.mkdir();
System.out.println("Directory created :: " + destinationFolder);
}
//Get all files from source directory
String files[] = sourceFolder.list();
//Iterate over all files and copy them to destinationFolder one by one
for (String file : files)
{
File srcFile = new File(sourceFolder, file);
File destFile = new File(destinationFolder, file);
//Recursive function call
copyFolder(srcFile, destFile);
}
}
else
{
//Copy the file content from one place to another
Files.copy(sourceFolder.toPath(), destinationFolder.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.out.println("File copied :: " + destinationFolder);
}
}
}
Output:
Directory created :: c:\tempNew
File copied :: c:\tempNew\testcopied.txt
File copied :: c:\tempNew\testoriginal.txt
File copied :: c:\tempNew\testOut.txt
```
### common-util
```java
private static void fileCopyUsingApacheCommons() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
File newFile = new File("c:/temp/testcopied.txt");
FileUtils.copyFile(fileToCopy, newFile);
// OR
IOUtils.copy(new FileInputStream(fileToCopy), new FileOutputStream(newFile));
}
```
## 3 删除delete
### NIO
```java
public class DeleteDirectoryNIOWithStream
{
public static void main(String[] args)
{
Path dir = Paths.get("c:/temp/innerDir");
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
```
### apache commons-io
```
public class DeleteDirectoryNIOWithStream
{
public static void main(String[] args)
{
Path dir = Paths.get("c:/temp/innerDir");
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
```
## 4 创建create
### BIOcreateNewFile
File.createNewFile()方法创建新文件。 此方法返回布尔值–
* 如果文件创建成功则返回true 。
* 如果文件已经存在或操作由于某种原因失败则返回false 。
* 此方法不会像文件中写任何数据
```java
File file = new File("c://temp//testFile1.txt");
//Create the file
if (file.createNewFile())
{
System.out.println("File is created!");
} else {
System.out.println("File already exists.");
}
//Write Content
FileWriter writer = new FileWriter(file);
writer.write("Test data");
writer.close();
```
### BIOFileOutputStream
FileOutputStream.write()方法自动创建一个新文件并向其中写入内容 。
```java
String data = "Test data";
FileOutputStream out = new FileOutputStream("c://temp//testFile2.txt");
out.write(data.getBytes());
out.close();
```
### NIO
Files.write()是创建文件的最佳方法,如果您尚未使用它,则应该是将来的首选方法。
此方法将文本行写入文件 。 每行都是一个char序列并按顺序写入文件每行由平台的line separator终止。
```java
String data = "Test data";
Files.write(Paths.get("c://temp//testFile3.txt"), data.getBytes());
//or
List<String> lines = Arrays.asList("1st line", "2nd line");
Files.write(Paths.get("file6.txt"),
lines,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
```
## 5 写入wrirte&append
* append模式使用BufferedWritter PrintWriter FileOutputStream和Files类将内容追加到java中的 Files 。 在所有示例中在打开要写入的文件时您都传递了第二个参数true 表示该文件以append mode打开
### BIO:BufferedWriter
通过BufferedWriter进行更少的IO操作提高了性能。
```java
public static void usingBufferedWritter() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
BufferedWriter writer = new BufferedWriter(new FileWriter("c:/temp/samplefile1.txt"));
writer.write(fileContent);
writer.close();
}
```
### BIO:PrintWriter
使用PrintWriter将格式化的文本写入文件。
```java
public static void usingPrintWriter() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileWriter fileWriter = new FileWriter("c:/temp/samplefile3.txt");
PrintWriter printWriter = new PrintWriter(fileWriter);
printWriter.print(fileContent);
printWriter.printf("Blog name is %s", "howtodoinjava.com");
printWriter.close();
}
```
### BIO:FileOutputStream
使用FileOutputStream 将二进制数据写入文件 。 FileOutputStream用于写入原始字节流例如图像数据。 要编写字符流请考虑使用FileWriter 。
```java
public static void usingFileOutputStream() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileOutputStream outputStream = new FileOutputStream("c:/temp/samplefile4.txt");
byte[] strToBytes = fileContent.getBytes();
outputStream.write(strToBytes);
outputStream.close();
}
```
### BIO:DataOutputStream
DataOutputStream允许应用程序以可移植的方式将原始Java数据类型写入输出流。 然后,应用程序可以使用数据输入流来读回数据。
```java
public static void usingDataOutputStream() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileOutputStream outputStream = new FileOutputStream("c:/temp/samplefile5.txt");
DataOutputStream dataOutStream = new DataOutputStream(new BufferedOutputStream(outputStream));
dataOutStream.writeUTF(fileContent);
dataOutStream.close();
}
```
### NIO:FileChannel
FileChannel可用于读取写入映射和操作文件。 如果要处理大文件则FileChannel可能比标准IO快。
文件通道可以安全地供多个并发线程使用。
```java
public static void usingFileChannel() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
RandomAccessFile stream = new RandomAccessFile("c:/temp/samplefile6.txt", "rw");
FileChannel channel = stream.getChannel();
byte[] strBytes = fileContent.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(strBytes.length);
buffer.put(strBytes);
buffer.flip();
channel.write(buffer);
stream.close();
channel.close();
}
```
### NIO:Files静态方法
```java
public static void usingPath() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
Path path = Paths.get("c:/temp/samplefile7.txt");
Files.write(path, fileContent.getBytes());
}
public static void usingPath() throws IOException
{
String textToAppend = "\r\n Happy Learning !!"; //new line in content
Path path = Paths.get("c:/temp/samplefile.txt");
Files.write(path, textToAppend.getBytes(), StandardOpenOption.APPEND); //Append mode
}
```
## 6 读取read
### BIO:BufferedReader按行读
```java
//Using BufferedReader and FileReader - Below Java 7
private static String usingBufferedReader(String filePath)
{
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filePath)))
{
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null)
{
contentBuilder.append(sCurrentLine).append("\n");
}
}
catch (IOException e)
{
e.printStackTrace();
}
return contentBuilder.toString();
```
### BIO:FileInputStream 读取字节
```java
import java.io.File;
import java.io.FileInputStream;
public class ContentToByteArrayExample
{
public static void main(String[] args)
{
File file = new File("C:/temp/test.txt");
readContentIntoByteArray(file);
}
private static byte[] readContentIntoByteArray(File file)
{
FileInputStream fileInputStream = null;
byte[] bFile = new byte[(int) file.length()];
try
{
//convert file into array of bytes
fileInputStream = new FileInputStream(file);
fileInputStream.read(bFile);
fileInputStream.close();
for (int i = 0; i < bFile.length; i++)
{
System.out.print((char) bFile[i]);
}
}
catch (Exception e)
{
e.printStackTrace();
}
return bFile;
}
}
```
### NIO:Files按行读
lines()方法从文件中读取所有行以进行流传输并在stream被消耗时延迟填充。 使用指定的字符集将文件中的字节解码为字符。
readAllBytes()方法reads all the bytes from a file 。 该方法可确保在读取所有字节或引发I / O错误或其他运行时异常时关闭文件。
```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class ReadFileToString
{
public static void main(String[] args)
{
String filePath = "c:/temp/data.txt";
System.out.println( readLineByLineJava8( filePath ) );
}
//Read file content into string with - Files.lines(Path path, Charset cs)
private static String readLineByLineJava8(String filePath)
{
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines( Paths.get(filePath), StandardCharsets.UTF_8))
{
stream.forEach(s -> contentBuilder.append(s).append("\n"));
}
catch (IOException e)
{
e.printStackTrace();
}
return contentBuilder.toString();
}
}
```
### NIO:读取所有字节
读取所有字节后我们将这些字节传递给String类构造函数以创建一个字符串
```java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ReadFileToString
{
public static void main(String[] args)
{
String filePath = "c:/temp/data.txt";
System.out.println( readAllBytesJava7( filePath ) );
}
//Read file content into string with - Files.readAllBytes(Path path)
private static String readAllBytesJava7(String filePath)
{
String content = "";
try
{
content = new String ( Files.readAllBytes( Paths.get(filePath) ) );
}
catch (IOException e)
{
e.printStackTrace();
}
return content;
}
}
```
### commons-io
```java
//Using FileUtils.readFileToByteArray()
byte[] org.apache.commons.io.FileUtils.readFileToByteArray(File file)
//Using IOUtils.toByteArray
byte[] org.apache.commons.io.IOUtils.toByteArray(InputStream input)
```
## 7 Properties
任何复杂的应用程序都需要某种配置。 有时我们需要将此配置为只读(通常在应用程序启动时读取),有时(或很少)我们需要写回或更新这些属性配置文件上的内容。
在这个简单易用的教程中学习使用Properties.load()方法读取Java中的Properties.load() 文件 。 然后我们将使用Properties.setProperty()方法将新属性写入file 。
* 创建单实例的属性文件
```java
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Set;
public class PropertiesCache
{
private final Properties configProp = new Properties();
private PropertiesCache()
{
//Private constructor to restrict new instances
InputStream in = this.getClass().getClassLoader().getResourceAsStream("app.properties");
System.out.println("Read all properties from file");
try {
configProp.load(in);
} catch (IOException e) {
e.printStackTrace();
}
}
//Bill Pugh Solution for singleton pattern
private static class LazyHolder
{
private static final PropertiesCache INSTANCE = new PropertiesCache();
}
public static PropertiesCache getInstance()
{
return LazyHolder.INSTANCE;
}
public String getProperty(String key){
return configProp.getProperty(key);
}
public Set<String> getAllPropertyNames(){
return configProp.stringPropertyNames();
}
public boolean containsKey(String key){
return configProp.containsKey(key);
}
}
```
### 补充:静态内部类与懒汉式单例模式
这种方式是当被调用getInstance()时才去加载静态内部类LazyHolderLazyHolder在加载过程中会实例化一个静态的Singleton因为利用了classloader的机制来保证初始化instance时只有一个线程所以Singleton肯定只有一个是线程安全的这种比上面1、2都好一些既实现了线程安全又避免了同步带来的性能影响。
```java
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
```
## 8 Resource File
ClassLoader引用从应用程序的资源包中读取文件。加载上下文环境中的文件。
```java
package com.howtodoinjava.demo;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class ReadResourceFileDemo
{
public static void main(String[] args) throws IOException
{
String fileName = "config/sample.txt";
ClassLoader classLoader = new ReadResourceFileDemo().getClass().getClassLoader();
File file = new File(classLoader.getResource(fileName).getFile());
//File is found
System.out.println("File Found : " + file.exists());
//Read File Content
String content = new String(Files.readAllBytes(file.toPath()));
System.out.println(content);
}
}
```
### 在spring中可以这样
```java
File file = ResourceUtils.getFile("classpath:config/sample.txt")
//File is found
System.out.println("File Found : " + file.exists());
//Read File Content
String content = new String(Files.readAllBytes(file.toPath()));
System.out.println(content);
```
## 10 读写utf8数据
```java
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
public class WriteUTF8Data
{
public static void main(String[] args)
{
try
{
File fileDir = new File("c:\\temp\\test.txt");
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileDir), "UTF8"));
out.append("Howtodoinjava.com").append("\r\n");
out.append("UTF-8 Demo").append("\r\n");
out.append("क्षेत्रफल = लंबाई * चौड़ाई").append("\r\n");
out.flush();
out.close();
} catch (UnsupportedEncodingException e)
{
System.out.println(e.getMessage());
} catch (IOException e)
{
System.out.println(e.getMessage());
} catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}
```
```java
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
public class ReadUTF8Data
{
public static void main(String[] args)
{
try {
File fileDir = new File("c:\\temp\\test.txt");
BufferedReader in = new BufferedReader(
new InputStreamReader(
new FileInputStream(fileDir), "UTF8"));
String str;
while ((str = in.readLine()) != null) {
System.out.println(str);
}
in.close();
}
catch (UnsupportedEncodingException e)
{
System.out.println(e.getMessage());
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}
```
## 11 从控制台读取输入
### console对象
```java
private static void usingConsoleReader()
{
Console console = null;
String inputString = null;
try
{
// creates a console object
console = System.console();
// if console is not null
if (console != null)
{
// read line from the user input
inputString = console.readLine("Name: ");
// prints
System.out.println("Name entered : " + inputString);
}
} catch (Exception ex)
{
ex.printStackTrace();
}
}
```
### System.in封装
```java
private static void usingBufferedReader()
{
System.out.println("Name: ");
try{
BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
String inputString = bufferRead.readLine();
System.out.println("Name entered : " + inputString);
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
```
### 更加复杂的Scanner
```java
private static void usingScanner()
{
System.out.println("Name: ");
Scanner scanIn = new Scanner(System.in);
String inputString = scanIn.nextLine();
scanIn.close();
System.out.println("Name entered : " + inputString);
}
```
## 12 将String转换成输入流
### ByteArrayInputStream
```java
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class ConvertStringToInputStreamExample
{
public static void main(String[] args)
{
String sampleString = "howtodoinjava.com";
//Here converting string to input stream
InputStream stream = new ByteArrayInputStream(sampleString.getBytes());
}
}
```
### apach.IOUtils
```java
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
public class ConvertStringToInputStreamExample
{
public static void main(String[] args)
{
String sampleString = "howtodoinjava.com";
//Here converting string to input stream
InputStream stream = IOUtils.toInputStream(sampleString);
}
}
```

View File

@@ -0,0 +1,59 @@
# 对象操作
## 基本操作
### 序列化
序列化就是将一个对象转换成字节序列,方便存储和传输。
- 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
### Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
```java
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a1 = new A(123, "abc");
String objectFile = "file/a1";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
objectOutputStream.writeObject(a1);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) objectInputStream.readObject();
objectInputStream.close();
System.out.println(a2);
}
private static class A implements Serializable {
private int x;
private String y;
A(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}
```
### transient
transient 关键字可以使一些属性不会被序列化。
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
private transient Object[] elementData;
```

View File

@@ -0,0 +1,191 @@
# 网络操作
> https://blog.csdn.net/forezp/article/details/88414741
## 1 网络编程基础
Java 中的网络支持:
- InetAddress用于表示网络上的硬件资源即 IP 地址;
- URL统一资源定位符
- Sockets使用 TCP 协议实现网络通信;
- Datagram使用 UDP 协议实现网络通信。
### InetAddress
没有公有的构造函数,只能通过静态方法来创建实例。
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
```
### URL
可以直接从 URL 中读取字节流数据。
```java
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.baidu.com");
/* 字节流 */
InputStream is = url.openStream();
/* 字符流 */
InputStreamReader isr = new InputStreamReader(is, "utf-8");
/* 提供缓存功能 */
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
```
### Sockets
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1e6affc4-18e5-4596-96ef-fb84c63bf88a.png" width="550px"> </div><br>
### Datagram
- DatagramSocket通信类
- DatagramPacket数据包类
## 2 BIO Socket编程
```java
public class PlainOioServer {
public void serve(int port) throws IOException {
final ServerSocket socket = new ServerSocket(port); //1
try {
for (;;) {
final Socket clientSocket = socket.accept(); //2
System.out.println("Accepted connection from " + clientSocket);
new Thread(new Runnable() { //3
@Override
public void run() {
OutputStream out;
try {
out = clientSocket.getOutputStream();
out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8"))); //4
out.flush();
clientSocket.close(); //5
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start(); //6
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
## 3 NIO Socket编程
```java
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
```
```java
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Some files were not shown because too many files have changed in this diff Show More