image.PNG
image.PNG
image.PNG
image.PNG
image.PNG
image.PNG
image.PNG
image.PNG
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写, JVM 是一种用于计算设备的规范, 它是一个虚构出来的计算机, 是通过在实际的计算机上仿真模拟各种计算机功能来实现的.
Java 语言的一个非常重要的特点就是与平台的无关性. 而使用 Java 虚拟机是实现这一特点的关键. 一般的高级语言如果要在不同的平台上运行, 至少需要编译成不同的目标代码. 而引入 Java 语言虚拟机后, Java 语言在不同平台上运行时不需要重新编译. Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息, 使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码), 就可以在多种平台上不加修改地运行. Java 虚拟机在执行字节码时, 把字节码解释成具体平台上的机器指令执行. 这就是 Java 的能够 "一次编译, 到处运行" 的原因.
JVM 基本数据类型
- short://2 字节有符号整数的补码
- int://4 字节有符号整数的补码
- long://8 字节有符号整数的补码
- float://4 字节 IEEE754 单精度浮点数
- double://8 字节 IEEE754 双精度浮点数
- char://2 字节无符号 Unicode 字符
boolean:boolean 数据类型表示一位的信息
image.PNG
几乎所有的 Java 类型检查都是在编译时完成的. 上面列出的原始数据类型的数据在 Java 执行时不需要用硬件标记. 操作这些原始数据类型数据的字节码 (指令) 本身就已经指出了操作数的数据类型, 例如 iadd,ladd,fadd 和 dadd 指令都是把两个数相加, 其操作数类型分别是 int,long,float 和 double. 虚拟机没有给 boolean(布尔)类型设置单独的指令. boolean 型的数据是由 integer 指令, 包括 integer 返回来处理的. boolean 型的数组则是用 byte 数组来处理的. 虚拟机使用 IEEE754 格式的浮点数. 不支持 IEEE 格式的较旧的计算机, 在运行 Java 数值计算程序时, 可能会非常慢.
其它数据类型
- object// 对一个 Javaobject(对象)的 4 字节引用
- returnAddress//4 字节, 用于 jsr/ret/jsr-w/ret-w 指令
注: Java 数组被当做 object 处理.
JVM
虚拟机的规范对于 object 内部的结构没有任何特殊的要求. 在 Sun 公司的实现中, 对 object 的引用是一个句柄, 其中包含一对指针: 一个指针指向该 object 的方法表, 另一个指向该 object 的数据. 用 Java 虚拟机的字节码表示的程序应该遵守类型规定. Java 虚拟机的实现应拒绝执行违反了类型规定的字节码程序. Java 虚拟机由于字节码定义的限制似乎只能运行于 32 位地址空间的机器上. 但是可以创建一个 Java 虚拟机, 它自动地把字节码转换成 64 位的形式. 从 Java 虚拟机支持的数据类型可以看出, Java 对数据类型的内部格式进行了严格规定, 这样使得各种 Java 虚拟机的实现对数据的解释是相同的, 从而保证了 Java 的与平台无关性和可移植性.
规格编辑
JVM 的设计目标是提供一个基于抽象规格描述的计算机模型, 为解释程序开发人员提供很好的灵活性, 同时也确保 Java 代码可在符合该规范的任何系统上运行. JVM 对其实现的某些方面给出了具体的定义, 特别是对 Java 可执行代码, 即字节码 (Bytecode) 的格式给出了明确的规格. 这一规格包括操作码和操作数的语法和数值, 标识符的数值表示方式, 以及 Java 类文件中的 Java 对象, 常量缓冲池在 JVM 的存储映象. 这些定义为 JVM 解释器开发人员提供了所需的信息和开发环境. Java 的设计者希望给开发人员以随心所欲使用 Java 的自由.
JVM 定义了控制 Java 代码解释执行和具体实现的五种规格, 它们是:
JVM 指令系统
JVM 寄存器
JVM 栈结构
JVM 碎片回收堆
JVM 存储区
原理编辑
JVM 是 java 的核心和基础, 在 java 编译器和 os 平台之间的虚拟处理器. 它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机, 可以在上面执行 java 的字节码程序.
JVM 运行原理
JVM 运行原理 [1]
java 编译器只需面向 JVM, 生成 JVM 能理解的代码或字节码文件. Java 源文件经编译器, 编译成字节码程序, 通过 JVM 将每一条指令翻译成不同平台机器码, 通过特定平台运行.
JVM 执行程序的过程 :
I. 加载. class 文件
II. 管理并分配内存
III. 执行垃圾收集
JRE(java 运行时环境)包含 JVM 的 java 程序的运行环境 [1]
JVM 是 Java 程序运行的容器, 但是他同时也是操作系统的一个进程, 因此他也有他自己的运行的生命周期, 也有自己的代码和数据空间.
JVM 在整个 jdk 中处于最底层, 负责与操作系统的交互, 用来屏蔽操作系统环境, 提供一个完整的 Java 运行环境, 因此也叫虚拟计算机. 操作系统装入 JVM 是通过 jdk 中 Java.exe 来完成, 通过下面 4 步来完成 JVM 环境.
1. 创建 JVM 装载环境和配置
2. 装载 JVM.dll
3. 初始化 JVM.dll 并挂接到 JNIENV(JNI 调用接口)实例
4. 调用 JNIEnv 实例装载并处理 class 类. [2]
指令系统编辑
JVM 指令系统同其他计算机的指令系统极其相似. Java 指令也是由操作码和操作数两部分组
JVM
操作码为 8 位二进制数, 操作数紧随在操作码的后面, 其长度根据需要而不同. 操作码用于指定一条指令操作的性质(在这里我们采用汇编符号的形式进行说明), 如 iload 表示从存储器中装入一个整数, anewarray 表示为一个新数组分配空间, iand 表示两个整数的 "与",ret 用于流程控制, 表示从对某一方法的调用中返回. 当长度大于 8 位时, 操作数被分为两个以上字节存放. JVM 采用了 "big endian [3]" 的编码方式来处理这种情况, 即高位 bits 存放在低字节中. 这同 Motorola 及其他的 RISC CPU 采用的编码方式是一致的, 而与 Intel 采用的 "little endian" 的编码方式即低位 bits 存放在低位字节的方法不同. Java 指令系统是以 Java 语言的实现为目的设计的, 其中包含了用于调用方法和监视多线程系统的指令. Java 的 8 位操作码的长度使得 JVM 最多有 256 种指令, 已使用了 160 多种操作码.
寄存器编辑
所有的 CPU 均包含用于保存系统状态和处理器所需信息的寄存器组. 如果虚拟机定义较多的寄存器, 便可以从中得到更多的信息而不必对栈或内存进行访问, 这有利于提高运行速度. 然而, 如果虚拟机中的寄存器比实际 CPU 的寄存器多, 在实现虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器, 这反而会降低虚拟机的效率. 针对这种情况, JVM 只设置了 4 个最为常用的寄存器. 它们是:
pc 程序计数器
optop 操作数栈顶指针
frame 当前执行环境指针
vars 指向当前执行环境中第一个局部变量的指针
所有寄存器均为 32 位. pc 用于记录程序的执行. optop,frame 和 vars 用于记录指向 Java 栈区的指针.
栈结构编辑
作为基于栈结构的计算机, Java 栈是 JVM 存储信息的主要方法. 当 JVM 得到一个 Java 字节码应用程序后, 便为该代码中一个类的每一个方法创建一个栈框架, 以保存该方法的状态信息. 每个栈框架包括以下三类信息:
局部变量
执行环境
操作数栈
局部变量用于存储一个类的方法中所用到的局部变量. vars 寄存器指向该变量表中的第一个局部变量.
执行环境用于保存解释器对 Java 字节码进行解释过程中所需的信息. 它们是: 上次调用的方法, 局部变量指针和操作数栈的栈顶和栈底指针. 执行环境是一个执行一个方法的控制中心. 例如: 如果解释器要执行 iadd(整数加法), 首先要从 frame 寄存器中找到当前执行环境, 而后便从执行环境中找到操作数栈, 从栈顶弹出两个整数进行加法运算, 最后将结果压入栈顶.
操作数栈用于存储运算所需操作数及运算的结果.
碎片回收编辑
Java 类的实例所需的存储空间是在堆上分配的. 解释器具体承担为类实例分配空间的工作. 解释器在为一个实例分配完存储空间后, 便开始记录对该实例所占用的内存区域的使用. 一旦对象使用完毕, 便将其回收到堆中. 在 Java 语言中, 除了 new 语句外没有其他方法为一对象申请和释放内存. 对内存进行释放和回收的工作是由 Java 运行系统承担的. 这允许 Java 运行系统的设计者自己决定碎片回收的方法. 在 SUN 公司开发的 Java 解释器和 Hot Java 环境中, 碎片回收用后台线程的方式来执行. 这不但为运行系统提供了良好的性能, 而且使程序设计人员摆脱了自己控制内存使用的风险.
image.PNG
存储区编辑
JVM 有两类存储区: 常量缓冲池和方法区. 常量缓冲池用于存储类名称, 方法和字段名称以及串常量. 方法区则用于存储 Java 方法的字节码. 对于这两种存储区域具体实现方式在 JVM 规格中没有明确规定. 这使得 Java 应用程序的存储布局必须在运行过程中确定, 依赖于具体平台的实现方式. JVM 是为 Java 字节码定义的一种独立于具体平台的规格描述, 是 Java 平台独立性的基础. JVM 还存在一些限制和不足, 有待于进一步的完善, 但无论如何, JVM 的思想是成功的.
对比分析: 如果把 Java 原程序想象成我们的 C++ 原程序, Java 原程序编译后生成的字节码就相当于 C++ 原程序编译后的 80x86 的机器码(二进制程序文件),JVM 虚拟机相当于 80x86 计算机系统, Java 解释器相当于 80x86CPU. 在 80x86CPU 上运行的是机器码, 在 Java 解释器上运行的是 Java 字节码. Java 解释器相当于运行 Java 字节码的 "CPU", 但该 "CPU" 不是通过硬件实现的, 而是用软件实现的. Java 解释器实际上就是特定的平台下的一个应用程序. 只要实现了特定平台下的解释器程序, Java 字节码就能通过解释器程序在该平台下运行, 这是 Java 跨平台的根本. 当前, 并不是在所有的平台下都有相应 Java 解释器程序, 这也是 Java 并不能在所有的平台下都能运行的原因, 它只能在已实现了 Java 解释器程序的平台下运行.
运行数据编辑
JVM 定义了若干个程序执行期间使用的数据区域. 这个区域里的一些数据在 JVM 启动的时候创建, 在 JVM 退出的时候销毁. 而其他的数据依赖于每一个线程, 在线程创建时创建, 在线程退出时销毁. 分别有程序计数器, 堆, 栈, 方法区, 运行时常量池.
image.PNG
体系结构编辑
JVM 可以由不同的厂商来实现. 由于厂商的不同必然导致 JVM 在实现上的一些不同, 然而 JVM 还是可以实现跨平台的特性, 这就要归功于设计 JVM 时的体系结构了. 我们知道, 一个 JVM 实例的行为不光是它自己的事, 还涉及到它的子系统, 存储区域, 数据类型和指令这些部分, 它们描述了 JVM 的一个抽象的内部体系结构, 其目的不光规定实现 JVM 时它内部的体系结构, 更重要的是提供了一种方式, 用于严格定义实现时的外部行为. 每个 JVM 都有两种机制, 一个是装载具有合适名称的类(类或是接口), 叫做类装载子系统; 另外的一个负责执行包含在已装载的类或接口中的指令, 叫做运行引擎. 每个 JVM 又包括方法区, 堆, Java 栈, 程序计数器和本地方法栈这五个部分, 这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:
JVM 体系结构
image.PNG
JVM 体系结构
JVM 的每个实例都有一个它自己的方法域和一个堆, 运行于 JVM 内的所有的线程都共享这些区域; 当虚拟机装载类文件的时候, 它解析其中的二进制数据所包含的类信息, 并把它们放到方法域中; 当程序运行的时候, JVM 把程序初始化的所有对象置于堆上; 而每个线程创建的时候, 都会拥有自己的程序计数器和 Java 栈, 其中程序计数器中的值指向下一条即将被执行的指令, 线程的 Java 栈则存储为该线程调用 Java 方法的状态; 本地方法调用的状态被存储在本地方法栈, 该方法栈依赖于具体的实现.
下面分别对这几个部分进行说明.
执行引擎处于 JVM 的核心位置, 在 Java 虚拟机规范中, 它的行为是由指令集所决定的. 尽管对于每条指令, 规范很详细地说明了当 JVM 执行字节码遇到指令时, 它的实现应该做什么, 但对于怎么做却言之甚少. Java 虚拟机支持大约 248 个字节码. 每个字节码执行一种基本的 CPU 运算, 例如, 把一个整数加到寄存器, 子程序转移等. Java 指令集相当于 Java 程序的汇编语言. Java 指令集中的指令包含一个单字节的操作符, 用于指定要执行的操作, 还有 0 个或多个操作数, 提供操作所需的参数或数据. 许多指令没有操作数, 仅由一个单字节的操作符构成.
虚拟机的内层循环的执行过程如下:
do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)
由于指令系统的简单性, 使得虚拟机执行的过程十分简单, 从而有利于提高执行的效率. 指令中操作数的数量和大小是由操作符决定的. 如果操作数比一个字节大, 那么它存储的顺序是高位字节优先. 例如, 一个 16 位的参数存放时占用两个字节, 其值为:
第一个字节 * 256 + 第二个字节字节码.
指令流一般只是字节对齐的. 指令 tableswitch 和 lookup 是例外, 在这两条指令内部要求强制的 4 字节边界对齐. 对于本地方法接口, 实现 JVM 并不要求一定要有它的支持, 甚至可以完全没有. Sun 公司实现 Java 本地接口 (JNI [3] ) 是出于可移植性的考虑, 当然我们也可以设计出其它的本地接口来代替 Sun 公司的 JNI [4] . 但是这些设计与实现是比较复杂的事情, 需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉.
Java 的堆是一个运行时数据区, 类的实例 (对象) 从中分配空间, 它的管理是由垃圾回收来负责的: 不给程序员显式释放对象的能力. Java 不规定具体使用的垃圾回收算法, 可以根据系统的需求使用各种各样的算法.
Java 方法区与传统语言中的编译后代码或是 Unix 进程中的正文段类似. 它保存方法代码 (编译后的 java 代码) 和符号表. 在当前的 Java 实现中, 方法代码不包括在垃圾回收堆中, 但计划在将来的版本中实现. 每个类文件包含了一个 Java 类或一个 Java 界面的编译后的代码. 可以说类文件是 Java 语言的执行代码文件. 为了保证类文件的平台无关性, Java 虚拟机规范中对类文件的格式也作了详细的说明. 其具体细节请参考 Sun 公司的 Java 虚拟机规范.
Java 虚拟机的寄存器用于保存机器的运行状态, 与微处理器中的某些专用寄存器类似. Java 虚拟机的寄存器有四种:
pc: Java 程序计数器;
optop: 指向操作数栈顶端的指针;
frame: 指向当前执行方法的执行环境的指针;.
vars: 指向当前执行方法的局部变量区第一个变量的指针.
在上述体系结构图中, 我们所说的是第一种, 即程序计数器, 每个线程一旦被创建就拥有了自己的程序计数器. 当线程执行 Java 方法的时候, 它包含该线程正在被执行的指令的地址. 但是若线程执行的是一个本地的方法, 那么程序计数器的值就不会被定义.
Java 虚拟机的栈有三个区域: 局部变量区, 运行环境区, 操作数区.
局部变量区
每个 Java 方法使用一个固定大小的局部变量集. 它们按照与 vars 寄存器的字偏移量来寻址. 局部变量都是 32 位的. 长整数和双精度浮点数占据了两个局部变量的空间, 却按照第一个局部变量的索引来寻址.(例如, 一个具有索引 n 的局部变量, 如果是一个双精度浮点数, 那么它实际占据了索引 n 和 n+1 所代表的存储空间)虚拟机规范并不要求在局部变量中的 64 位的值是 64 位对齐的. 虚拟机提供了把局部变量中的值装载到操作数栈的指令, 也提供了把操作数栈中的值写入局部变量的指令.
JRE 和 JVM 的区别
JRE(JavaRuntimeEnvironment,Java 运行环境), 也就是 Java 平台. 所有的 Java 程序都要在 JRE 下才能运行. JDK 的工具也是 Java 程序, 也需要 JRE 才能运行. 为了保持 JDK 的独立性和完整性, 在 JDK 的安装过程中, JRE 也是安装的一部分. 所以, 在 JDK 的安装目录下有一个名为 jre 的目录, 用于存放 JRE 文件.
JVM(JavaVirtualMachine,Java 虚拟机)是 JRE 的一部分. 它是一个虚构出来的计算机, 是通过在实际的计算机上仿真模拟各种计算机功能来实现的. JVM 有自己完善的硬件架构, 如处理器, 堆栈, 寄存器等, 还具有相应的指令系统. Java 语言最重要的特点就是跨平台运行. 使用 JVM 就是为了支持与操作系统无关, 实现跨平台. [3]
运行环境区
在运行环境中包含的信息用于动态链接, 正常的方法返回以及异常捕捉.
动态链接
运行环境包括对指向当前类和当前方法的解释器符号表的指针, 用于支持方法代码的动态链接. 方法的 class 文件代码在引用要调用的方法和要访问的变量时使用符号. 动态链接把符号形式的方法调用翻译成实际方法调用, 装载必要的类以解释还没有定义的符号, 并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址. 动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码.
正常的方法返回
如果当前方法正常地结束了, 在执行了一条具有正确类型的返回指令时, 调用的方法会得到一个返回值. 执行环境在正常返回的情况下用于恢复调用者的寄存器, 并把调用者的程序计数器增加一个恰当的数值, 以跳过已执行过的方法调用指令, 然后在调用者的执行环境中继续执行下去.
异常捕捉
异常情况在 Java 中被称作 Error(错误)或 Exception(异常), 是 Throwable 类的子类, 在程序中的原因是:1动态链接错, 如无法找到所需的 class 文件.2运行时错, 如对一个空指针的引用. 程序使用了 throw 语句.
当异常发生时, Java 虚拟机采取如下措施:
§ 检查与当前方法相联系的 catch 子句表. 每个 catch 子句包含其有效指令范围, 能够处理的异常类型, 以及处理异常的代码块地址.
§ 与异常相匹配的 catch 子句应该符合下面的条件: 造成异常的指令在其指令范围之内, 发生的异常类型是其能处理的异常类型的子类型. 如果找到了匹配的 catch 子句, 那么系统转移到指定的异常处理块处执行; 如果没有找到异常处理块, 重复寻找匹配的 catch 子句的过程, 直到当前方法的所有嵌套的 catch 子句都被检查过.
§ 由于虚拟机从第一个匹配的 catch 子句处继续执行, 所以 catch 子句表中的顺序是很重要的. 因为 Java 代码是结构化的, 因此总可以把某个方法的所有的异常处理器都按序排列到一个表中, 对任意可能的程序计数器的值, 都可以用线性的顺序找到合适的异常处理块, 以处理在该程序计数器值下发生的异常情况.
§ 如果找不到匹配的 catch 子句, 那么当前方法得到一个 "未截获异常" 的结果并返回到当前方法的调用者, 好像异常刚刚在其调用者中发生一样. 如果在调用者中仍然没有找到相应的异常处理块, 那么这种错误将被传播下去. 如果错误被传播到最顶层, 那么系统将调用一个缺省的异常处理块.
操作数栈区
机器指令只从操作数栈中取操作数, 对它们进行操作, 并把结果返回到栈中. 选择栈结构的原因是: 在只有少量寄存器或非通用寄存器的机器 (如 Intel486) 上, 也能够高效地模拟虚拟机的行为. 操作数栈是 32 位的. 它用于给方法传递参数, 并从方法接收结果, 也用于支持操作的参数, 并保存操作的结果. 例如, iadd 指令将两个整数相加. 相加的两个整数应该是操作数栈顶的两个字. 这两个字是由先前的指令压进堆栈的. 这两个整数将从堆栈弹出, 相加, 并把结果压回到操作数栈中.
每个原始数据类型都有专门的指令对它们进行必须的操作. 每个操作数在栈中需要一个存储位置, 除了 long 和 double 型, 它们需要两个位置. 操作数只能被适用于其类型的操作符所操作. 例如, 压入两个 int 类型的数, 如果把它们当作是一个 long 类型的数则是非法的. 在 Sun 的虚拟机实现中, 这个限制由字节码验证器强制实行. 但是, 有少数操作(操作符 dupe 和 swap), 用于对运行时数据区进行操作时是不考虑类型的.
本地方法栈, 当一个线程调用本地方法时, 它就不再受到虚拟机关于结构和安全限制方面的约束, 它既可以访问虚拟机的运行期数据区, 也可以使用本地处理器以及任何类型的栈. 例如, 本地栈是一个 C 语言的栈, 那么当 C 程序调用 C 函数时, 函数的参数以某种顺序被压入栈, 结果则返回给调用函数. 在实现 Java 虚拟机时, 本地方法接口使用的是 C 语言的模型栈, 那么它的本地方法栈的调度与使用则完全与 C 语言的栈相同.
运行过程编辑
上面对虚拟机的各个部分进行了比较详细的说明, 下面通过一个具体的例子来分析它的运行过程.
虚拟机通过调用某个指定类的方法 main 启动, 传递给 main 一个字符串数组参数, 使指定的类被装载, 同时链接该类所使用的其它的类型, 并且初始化它们. 新建一 java 源文件并取名 HelloApp.java, 内容如下:
- class HelloApp {
- public static void main(String[] args) {
- System.out.println("Hello World!");
- for (int i = 0; i < args.length; i++ ) {
- System.out.println(args);
- }
- }
- }
在命令模式下输入: javac HelloApp.java 进行编译, 这时同目录下会产生一个编译后的文件: HelloApp.class
然后在命令行模式下键入: java HelloApp run virtual machine
将通过调用 HelloApp 的方法 main 来启动 java 虚拟机, 传递给 main 一个包含三个字符串 "run","virtual","machine" 的数组. 我们略述虚拟机在执行 HelloApp 时可能采取的步骤.
JVM 虚拟机运行过程
JVM 虚拟机运行过程
开始试图执行类 HelloApp 的 main 方法, 发现该类并没有被装载, 也就是说虚拟机当前不包含该类的二进制代表, 于是虚拟机使用 ClassLoader 试图寻找这样的二进制代表. 如果这个进程失败, 则抛出一个异常. 类被装载后同时在 main 方法被调用之前, 必须对类 HelloApp 与其它类型进行链接然后初始化. 链接包含三个阶段: 检验, 准备和解析. 检验检查被装载的主类的符号和语义, 准备则创建类或接口的静态域以及把这些域初始化为标准的默认值, 解析负责检查主类对其它类或接口的符号引用, 在这一步它是可选的. 类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行. 一个类在初始化之前它的父类必须被初始化.
总结:
JVM 是 java 的虚拟机, 是一个解释字节码文件的解释器; 假如你成功启动了一个 java 程序, 那么它的第一步就是由 JVM 将程序的 class 文件解释为机器能够识别的指令. 换言之, JVM 无需自行启动
image.PNG
image.PNG
半编译半解释的特点
先编译成字节 ----- 然后在对字节解析
image.PNG
image.PNG
image.PNG
image.PNG
对应代码:
- package com.neusoft.demo01;
- /**
- * 这是我们学到的第一个 JAVA 类
- * public 公有的 关键字 (有特殊意义的单词) 都是小写
- * class 类 关键字
- * HelloTest 类名
- * 第一个类都可以创建一个入口的 main 方法, 这个方法的作用可以输出一些结果
- * @author ttc
- * 文档注释
- */
- public class HelloTest {
- // 当前所有的程序都要写在 main 方法中
- public static void main(String[] args){
- // 输出一句话(快捷键)syso alt+/
- System.out.println("这是我们的第一个程序");
- System.out.println("这是我们的第一个程序");
- System.out.println("这是我们的第一个程序");
- System.out.println("这是我们的第一个程序");
- System.out.println("这是我们的第一个程序");
- System.out.println("adfadsfadsf");
- System.out.println("adfasdfadsf");
- System.out.println("fffffffrfff");
- }
- }
来源: http://www.jianshu.com/p/b09de71c9b6f