@songhanshi
2020-11-16T17:20:03.000000Z
字数 34194
阅读 732
Java学习
是什么 what
为什么 why
如何使用 how
使用出现的问题 issue
对问题的量化 evaluate
造成问题的因素 why
如何优化 optimize
版本迭代 iterate
运行时编译
说到编译,我猜你一定会想到 .java 文件被编译成 .class文件的过程,这个编译我们一般称为前端编译。Java的编译和运行过程非常复杂,除了前端编译,还有运行时编译。由于机器无法直接运行 Java生成的字节码,所以在运行时,JIT 或解释器会将字节码转换成机器码,这个过程就叫运行时编译。
类编译加载执行过程
你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值 (sxt2)
JVM参数类型(3种)
盘点家底查看JVM默认值
你平时工作用过的JVM常用基本配置参数有哪些 (sxt2)
强引用、软饮用、弱引用、虚引用分别是什么 (sxt2)
谈谈你对OOM的认识 (sxt2)
GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈 (sxt2)
怎么查看服务器默认的垃圾回收器是哪个 (sxt2)
生产上如何配置垃圾回收器的
谈谈你对垃圾回收器的理解
G1 垃圾回收器 (sxt2)
生产环境服务器变慢,诊断思路和性能评估谈谈 (sxt2)
假如生产环境出现CPU占用过高,谈谈你的分析思路和定位 (sxt2)
对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的? (sxt2)

一些概念?
JDK(Java Development Kit)?
JRE(Java Runtime Environment)?
Java虚拟机是什么
Java虚拟机是什么,操作系统层面(进程角度)
现象:
// C语言运行程序// 编译:gcc HelloWorld.c -o HelloWorld// 运行C:zhangjg@linux:/deve/workspace/HelloWorld/src$ ./HelloWorldhello world// Java程序运行// 编译:zhangjg@linux:/deve/workspace/HelloJava/src$ javac HelloWorld.javazhangjg@linux:/deve/workspace/HelloJava/src$ lsHelloWorld.class HelloWorld.java// 运行:zhangjg@linux:/deve/workspace/HelloJava/src$ java -classpath . HelloWorldHelloWorld
问题:
Java版运行时执行的不是 ./HelloWorld.class class文件并不是可以直接被操作系统识别的二进制可执行文件 。
而“java”这个命令说明首先启动的是一个叫做java的程序,这个java程序在运行起来之后就是一个JVM进程实例。
参考
https://blog.csdn.net/zhangjg_blog/article/details/20380971
主流:HotSpot VM
提前编译(Ahead of Time Compilation,AOT)
(jvm3:介绍完Java虚拟机的运行时数据区域之后,我们大致明白了Java虚拟机内存模型的概况。)
运行时数据区域?
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。(运行时数据区域,强调对内存空间的划分
程序计数器?
Java虚拟机栈?
本地方法栈
Java堆(Java Heap)
方法区(Method Area)
方法区-运行时常量池(Runtime Constant Pool)
直接内存(Direct Memory)
语言层:创建对象
虚拟机层:创建对象
创建过程
存储布局
对象头?

实例数据
对齐填充
Java角度
使用句柄访问对象

直接指针访问对象
Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

参数:https://www.cnblogs.com/anyehome/p/9071619.html
1. OOM
* 概念:内存溢出(OutOfMemory,OOM):通常出现在某一块内存空间块耗尽的时候。(jvms)
跟踪垃圾回收-读懂虚拟机日志
| 指令 | 作用 |
|---|---|
| -XX:+PrintGC | 启动JVM后,只要遇到GC就会打印日志 |
| -XX:+PrintGCDetails | 使JVM在退出前打印堆的信息,详细描述了当前堆的各个区间的使用情况 |
| -XX:+PrintHeapAtGC | 输出更全面的堆信息 |
| -XX:+PrintGCTimeStamps | 在每次GC发生时,额外输出GC发生的时间,该输出时间为JVM启动后的时间偏移量 |
| -XX:+PrintGCApplicationConcurrentTime | 打印应用程序的执行时间 |
| -XX:+PrintGCApplicationStopedTime | 打印应用程序由于GC而产生的停顿时间 |
| -XX:+PrintReferenceGC | 可以跟踪系统内的软引用、弱引用、虚引用和Finallize队列 |
| -Xloggc | JVM将日志以文件形式输出(默认GC日志在控制台输出);如用-Xloggc:log/gc.log启动JVM,log文件夹下的gc.log记录所有GC文件 |
类加载/卸载的跟踪
| 指令 | 作用 |
|---|---|
| -verbose:class | 跟踪类的加载和卸载 |
| -XX:+TraceClassLoading | 单独跟踪加载 |
| -XX:+TraceClassUnloading | 单独跟踪类的卸载 |
| -XX:+PrintClassHistogram | 在运行时打印、查看系统中类的分布情况;会显示当前的类信息柱状图 |
系统参数查看
| 指令 | 作用 |
|---|---|
| -XX:+PrintVMOptions | 可以在程序运行时,打印JVM接受到的命令行显式参数 |
| -XX:+PrintCommandLineFlags | 打印传递给JVM的显示和隐式参数,参数可能是通过命令行直接给出的,也可以是JVM启动时设置的 |
| -XX:+PrintFlagsFinal | 打印所有系统参数的值 |
堆的参数配置
| 指令 | 作用 |
|---|---|
| -Xms | 指定初始堆空间的大小 |
| -Xmx | 指定最大堆空间,为最大可用内存;初始堆空间耗尽,JVM对堆空间扩展,扩展上限为最大堆空间 |
| -XX:MaxHeapSize | -XX:MaxHeapSize=20971520,当前总内存不小于-Xms的设定,当前总内存在-Xms和-Xmx之间,从-Xms开始根据需求向上增长;当前空闲内存为当前总内存减去当前已使用的空间 |
| 指令 | 作用 |
|---|---|
| -Xmn | 设置新生代的大小。设置较大新生代会减少老年代的大小,影响系统性能以及GC行为。一般设置为整个堆空间的1/3-1/4 |
| -Xmx | 指定最大堆空间,为最大可用内存;初始堆空间耗尽,JVM对堆空间扩展,扩展上限为最大堆空间 |
| -XX:SurvivorRation | -XX:SurvivorRation=eden/from=eden/to,设置新生代的eden空间和from/to空间的比例关系;如-XX:SurvivorRation=2,(eden=512k)/(from=256 |
| -XX:NewRatio | -XX:NewRatio=老年代/新生代;同-Xmn功能,用来设置新生代和老年代的比例 |

堆溢出处理
| 指令 | 作用 |
|---|---|
| -XX:+HeapDumpOnOutOfMemoryError | 在内存溢出时导出整个堆信息 |
| -XX:HeapDumpPath | 指定导出堆的存放路径;如,-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump |
// printstack.bat文件D:/tool/jdk.7_40/bin/jstack -F %1 > D:/a.txt // D:/tool/jdk.7_40 jdk目录// 完整参数配置-Xmx20m -Xms5m "-XX:OnOutOfMemoryError=D:/tool/jdk.7_40/bin/printstack.bat %p" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
非堆内存的参数配置-方法区
| 指令 | 作用 |
|---|---|
| jdk1.6、jdk1.7 | -XX:PermSize、-XX:MaxPermSize配置永久区大小。-XX:PermSize表示初始永久区大小,-XX:MaxPermSize表示最大永久区 |
| jdk1.8 | 永久区被删除,元空间存放类的元数据,默认元空间值受系统可用内存的限制,但依然可以使用-XX:MaxMetaspaceSize指定永久区的最大可用值 |
非堆内存的参数配置-栈
| 指令 | 作用 |
|---|---|
| -Xss | 指定线程的栈大小。栈是每个线程私有的内存空间 |
非堆内存的参数配置-直接内存
| 指令 | 作用 |
|---|---|
| -XX:MaxDirectMemorySize | 最大可用直接内存,不设置则默认为最大堆空间,即-Xmx。直接内存使用量达到-XX:MaxDirectMemorySize,会触发GC,如果GC不能有效释放足够空间,直接内存溢出会引起系统OOM |
虚拟机工作模式
| 指令 | 作用 |
|---|---|
| -client | 指定使用Client模式 |
| -server | 指定使用Server模式 |
| -version | 查看当前模式 |
内存溢出
例子
VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
代码
public class HeapOOM {static class OOMObject {}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while (true) {// 一个ArrayList对象总是持有OOMObject对象的强引用,导致其无法回收list.add(new OOMObject());}}}// 结果//java.lang.OutOfMemoryError:// Java heap space Dumping heap to java_pid3404.hprof ...// Heap dump file created [22045981 bytes in 0.663 secs]
堆内存溢出
出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”,表示一次堆空间的溢出
常规的处理方法
参数设置
异常类型
出现异常的情况(单线程)
例子2
代码
public class JavaVMStackSOF {private static int stackLength = 0;public static void test() {long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11, unused12, unused13, unused14, unused15,unused16, unused17, unused18, unused19, unused20, unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29,unused30, unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41, unused42, unused43,unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57,unused58, unused59, unused60, unused61, unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71,unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81, unused82, unused83, unused84, unused85,unused86, unused87, unused88, unused89, unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99,unused100;stackLength++;test();unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12 = unused13 = unused14= unused15 = unused16 = unused17 = unused18 = unused19 = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27= unused28 = unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40= unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53= unused54 = unused55 = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66= unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 = unused77 = unused78 = unused79= unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 = unused92= unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0;}public static void main(String[] args) {try {test();} catch (Error e) {System.out.println("stack length:" + stackLength);throw e;}}}// 结果// stack length:5675 Exception in thread "main"// java.lang.StackOverflowError at org.fenixsoft.oom.// JavaVMStackSOF.leak(JavaVMStackSOF.java:27) at org.fenixsoft.oom.// JavaVMStackSOF.leak(JavaVMStackSOF.java:28) at org.fenixsoft.oom.// JavaVMStackSOF.leak(JavaVMStackSOF.java:28)// ……后续异常堆栈信息省略
实验结果表明:无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。
出现异常的情况(多线程)
多线程例子
VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)
代码
public class JavaVMStackOOM {private void dontStop() {while (true) {}}public void stackLeakByThread() {while (true) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {dontStop();}});thread.start();}}public static void main(String[] args) throws Throwable {JavaVMStackOOM oom = new JavaVMStackOOM();oom.stackLeakByThread();}}// 在32位操作系统下的运行结果:// Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。这种通过“减少内存”的手段来解决内存溢出的方式.
// 字符串常量池
背景
例子jdk6
VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M
以JDK6来运行代码:
public class RuntimeConstantPoolOOM {public static void main(String[] args) {// 使用Set保持着常量池引用,避免Full GC回收常量池行为Set<String> set = new HashSet<String>();// 在short范围内足以让6MB的PermSize产生OOM了short i = 0;while (true) {set.add(String.valueOf(i++).intern());}}}// 运行结果// Exception in thread "main" java.lang.OutOfMemoryError: PermGen space// at java.lang.String.intern(Native Method)// at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
运行结果表明:运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK6的HotSpot虚拟机中的永久代)的一部分。
例子jdk7或+
// OOM异常一:Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.base/java.lang.Integer.toString(Integer.java:440)at java.base/java.lang.String.valueOf(String.java:3058)at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)// OOM异常二:Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.base/java.util.HashMap.resize(HashMap.java:699)at java.base/java.util.HashMap.putVal(HashMap.java:658)at java.base/java.util.HashMap.put(HashMap.java:607)at java.base/java.util.HashSet.add(HashSet.java:220)at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)
字符串常量池的实现在哪里出现问题?
示例代码
public class RuntimeConstantPoolOOM {public static void main(String[] args) {String str1 = new StringBuilder("计算机").append("软件").toString();System.out.println(str1.intern() == str1);String str2 = new StringBuilder("ja").append("va").toString();System.out.println(str2.intern() == str2);}}
问题:
在JDK6中运行,会得到两个false,而在JDK7中运行,会得到一个true和一个false;
// 其他部分
背景:
VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
借助CGLib直接操作字节码运行时生成了大量的动态类,使得方法区出现内存溢出异常:
public class JavaMethodAreaOOM {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args);}});enhancer.create();}}static class OOMObject {}}// 在JDK7中的运行结果:Caused by: java.lang.OutOfMemoryError: PermGen spaceat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more
此处模拟的场景并非纯粹是一个实验,类似这样的代码确实可能会出现在实际应用中:
-- Spring、Hibernate很多主流框架对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。
-- 另外,很多运行于Java虚拟机上的动态语言(例如Groovy等)通常都会持续创建新类型来支撑语言的动态性,会遇到相似溢出场景
tips
发展,参数,jdk8后
在JDK8以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。使用者有预防实际应用里出现类似于代码清单2-9那样的破坏性的操作,HotSpot还是提供了一 些参数作为元空间的防御措施,主要包括: ·-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
·-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
直接内存参数
代码示例
VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
使用unsafe分配本机内存:
public class DirectMemoryOOM {private static final int _1MB = 1024 * 1024;public static void main(String[] args) throws Exception {Field unsafeField = Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafe unsafe = (Unsafe) unsafeField.get(null);while (true) {unsafe.allocateMemory(_1MB);}}}// 运行结果Exception in thread "main" java.lang.OutOfMemoryErrorat sun.misc.Unsafe.allocateMemory(Native Method)at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了
引用计数算法(Reference Counting)
/*** testGC()方法执行后,objA和objB会不会被GC呢? * @author zzm */public class ReferenceCountingGC {public Object instance = null;private static final int _1MB = 1024 * 1024;/*** 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */private byte[] bigSize = new byte[2 * _1MB];public static void testGC() {ReferenceCountingGC objA = new ReferenceCountingGC();ReferenceCountingGC objB = new ReferenceCountingGC();objA.instance = objB;objB.instance = objA;objA = null;objB = null;// 假设在这行发生GC,objA和objB是否能被回收?System.gc();}}//运行结果://看到内存回收日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,//这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象 是否存活的。
引用-JDK1.2版之前
引用-JDK1.2版之后
4种引用的概念
垃圾的两次标记?
对象执行finalize()方法
finalize()工程使用?
一次对象自我拯救的演示
/*** 此代码演示了两点:* 1.对象可以在被GC时自我拯救。* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次*/public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK = null;public void isAlive() {System.out.println("yes, i am still alive :)");}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize method executed!");// (重新与引用链上的任何一个对象建立关联,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,// (那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了FinalizeEscapeGC.SAVE_HOOK = this;//}public static void main(String[] args) throws Throwable {SAVE_HOOK = new FinalizeEscapeGC();//对象第一次成功拯救自己SAVE_HOOK = null;System.gc();// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它Thread.sleep(500);if (SAVE_HOOK != null) {SAVE_HOOK.isAlive();} else {System.out.println("no, i am dead :(");}// 下面这段代码与上面的完全相同,但是这次自救却失败了(因为任何一个对象的finalize()方法都只会被系统自动调用一次SAVE_HOOK = null;System.gc();// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它Thread.sleep(500);if (SAVE_HOOK != null) {SAVE_HOOK.isAlive();} else {System.out.println("no, i am dead :(");}}}// 结果:// finalize method executed!// yes, i am still alive :)// no, i am dead :(
回收的内容?
回收-废弃常量?
回收-类型
算法分类
名词概念
分代假说:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
垃圾收集器设计原则:
基于分代假说,收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
跨代引用
“标记-清除”(Mark-Sweep)算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
主要的两个缺点:
示意图

标记-复制算法
优缺点
图示

复制-新生代应用
“标记-整 理”(Mark-Compact)算法
示意图

移动问题-Stop The World
根节点枚举现象-STW?
OopMap的数据结构解决的问题?
GCRoots枚举带来的问题?
什么是安全点?
如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来?
垃圾收集器

并行、并发
概念
运作过程的四个步骤?
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
示意图

优缺点
缺点引发问题
Garbage First(简称G1)收集器概念
G1收集器的运作过程大致可划分为以下四个步骤
1)初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2)并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3)最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4)筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
示意图

优缺点
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,