Java 笔记

目录

1. Java 笔记

1.1. JDK 和 JRE 的区别

JRE(Java Runtime Enviroment) 是 Java 的运行环境。面向 Java 程序的使用者, 而不是开发者。如果你仅下载并安装了 JRE, 那么你的系统只能运行 Java 程序。JRE 是运行 Java 程序所必须环境的集合, 包含 JVM 标准实现及 Java 核心类库。它包括 Java 虚拟机、Java 平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

JDK(Java Development Kit) 又称 J2SDK(Java2 Software Development Kit), 是 Java 开发工具包, 它提供了 Java 的开发环境(提供了编译器 javac 等工具, 用于将 java 文件编译为 class 文件)和运行环境(提供了 JVM 和 Runtime 辅助包, 用于解析 class 文件使其得到运行)。如果你下载并安装了 JDK, 那么你不仅可以开发 Java 程序, 也同时拥有了运行 Java 程序的平台。JDK 是整个 Java 的核心, 包括了 Java 运行环境 (JRE), 一堆 Java 工具 tools.jar 和 Java 标准类库 (rt.jar)。

1.2. Java 的几个环境变量

1.2.1. CLASSPATH

什么是 CLASSPATH? 它的作用是什么?

它是 javac 编译器的一个环境变量。

它的作用与 importpackage 关键字有关。当你写下 improt java.util.* 时, 编译器面对 import 关键字时, 就知道你要引入 java.util 这个 package 中的类; 但是编译器如何知道你把这个 package 放在哪里了呢? 所以你首先得告诉编译器这个 package 的所在位置; 如何告诉它呢? 就是设置 CLASSPATH 啦 😃

如果 java.util 这个 packagec:/jdk/ 目录下, 你得把 c:/jdk/ 这个路径设置到 CLASSPATH 中去! 当编译器面对 import java.util.* 这个语句时, 它先会查找 CLASSPATH 所指定的目录, 并检视子目录 java/util 是否存在, 然后找出名称吻合的已编译文件(.class 文件)。如果没有找到就会报错!

CLASSPATH 有点像 c/c++ 编译器中的 include 路径的设置哦, 是不是? 当 c/c++ 编译器遇到 include 这样的语句, 它是如何运作的? 哦, 其实道理都差不多! 搜索 include 路径, 检视文件! 当你自己开发一个 package 时, 然后想要用这个 package 中的类; 自然, 你也得把这个 package 所在的目录设置到 CLASSPATH 中去!

CLASSPATH 的设定, 对 JAVA 的初学者而言是一件棘手的事。所以 Sun 让 JAVA2 的 JDK 更聪明一些。你会发现, 在你安装之后, 即使完全没有设定 CLASSPATH, 你仍然能够编译基本的 JAVA 程序, 并且加以执行。

1.2.2. 设置 CLASSPATH

我们需要把 JDK 安装目录下的 lib 子目录中的 dt.jartools.jar 设置到 CLASSPATH 中, 当然, 当前目录 . 也必须加入到该变量中。这里 CLASSPATH 为:

.;C:/Program Files/Java/jdk1.6.0_21/lib/dt.jar;C:/Program Files/Java/jdk1.6.0_21/lib/tools.jar

1.2.3. 命令行中用到 CLASSPATH

From

文件目录结构:

Microsoft Windows:

D:\myprogram\
      |
      ---> org\  
            |
            ---> mypackage\
                     |
                     ---> HelloWorld.class       
                     ---> SupportClass.class   
                     ---> UtilClass.class     

Linux:

/home/user/myprogram/
            |
            ---> org/  
                  |
                  ---> mypackage/
                           |
                           ---> HelloWorld.class       
                           ---> SupportClass.class   
                           ---> UtilClass.class     

When we invoke Java, we specify the name of the application to run: org.mypackage.HelloWorld. However we must also tell Java where to look for the files and directories defining our package. So to launch the program, we use the following command:

Microsoft Windows:

java -classpath D:\myprogram org.mypackage.HelloWorld     

Linux:

java -cp /home/user/myprogram org.mypackage.HelloWorld     
  • java is the Java runtime launcher, a type of SDK Tool (A command-line tool, such as javac, javadoc, or apt)
  • -classpath D:\myprogram sets the path to the packages used in the program (on Linux, -cp /home/user/myprogram) and
  • org.mypackage.HelloWorld is the name of the main class

or we could also use on Windows:

set CLASSPATH=D:\myprogram
java org.mypackage.HelloWorld
  • Setting the path of a Jar file
D:\myprogram\
      |
      ---> lib\
            |
            ---> supportLib.jar
      |
      ---> org\
            |
            --> mypackage\
                       |
                       ---> HelloWorld.class
                       ---> SupportClass.class
                       ---> UtilClass.class

command-line: java -classpath D:\myprogram;D:\myprogram\lib\supportLib.jar org.mypackage.HelloWorld

  • Adding all JAR files in a directory

Windows example:

java -classpath ".;c:\mylib\*" MyApp

Linux example:

java -classpath '.:/mylib/*' MyApp

  • Package manifest

Linux distributions rely heavily on package management systems for distributing software. In this scheme, a package is an archive file containing a manifest file. The primary purpose is to enumerate the files which are included in the distribution, either for processing by various packaging tools or for human consumption. Manifests may contain additional information; for example, in JAR (a package format for delivering software written in Java programming language), they can specify a version number and an entry point for execution. The manifest may optionally contain a cryptographic hash or checksum of each file. By creating a cryptographic signature for such a manifest file, the entire contents of the distribution package can be validated for authenticity and integrity, as altering any of the files will invalidate the checksums in the manifest file.

  • Setting the path in a manifest file
D:\myprogram\
      |
      ---> helloWorld.jar 
      |
      ---> lib\  
            |
            ---> supportLib.jar

The manifest file defined in helloWorld.jar has this definition:

Main-Class: org.mypackage.HelloWorld
Class-Path: lib/supportLib.jar

The manifest file should end with either a new line or carriage return.

The program is launched with the following command:

java -jar D:\myprogram\helloWorld.jar [app arguments]

This automatically starts org.mypackage.HelloWorld specified in class Main-Class with the arguments. The user cannot replace this class name using the invocation java -jar. Class-Path describes the location of supportLib.jar relative to the location of the library helloWorld.jar. Neither absolute file path, which is permitted in -classpath parameter on the command line, nor jar-internal paths are supported. This means that if the main class file is contained in a jar, org/mypackage/HelloWorld.class must be a valid path on the root within the jar.

Multiple classpath entries are separated with spaces:

Class-Path: lib/supportLib.jar lib/supportLib2.jar

  • 重点: OS specific notes

Being closely associated with the file system, the command-line Classpath syntax depends on the operating system. For example:

  • on all Unix-like operating systems (such as Linux and Mac OS X), the directory structure has a Unix syntax, with separate file paths separated by a colon (“:”).
  • on Windows, the directory structure has a Windows syntax, and each file path must be separated by a semicolon (“;”).

This does not apply when the Classpath is defined in manifest files, where each file path must be separated by a space (" "), regardless of the operating system.

1.3. JAVA_HOME

这个太简单就不说了。

1.4. 命令行编译运行 Java 程序

这里以一个 HTTP Server 为例:

SimpleHTTPServer.java:

import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.Date;
import java.lang.System;

/**
 * Java program to create a simple HTTP Server to demonstrate how to use
 * ServerSocket and Socket class.
 * 
 * @author Javin Paul
 */
public class SimpleHTTPServer {

  public static void main(String[] args) throws IOException {
      int port = 8080;
      HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
      HttpContext context = server.createContext("/");
      context.setHandler(SimpleHTTPServer::handleRequest);
      System.out.printf("HTTP server listening on port %d...\n", port);
      server.start();
  }

  private static void handleRequest(HttpExchange exchange) throws IOException {
      Date today = new Date();
      String response = "Hi there!\n\n" + today;
      exchange.sendResponseHeaders(200, response.getBytes().length);//response code and length
      OutputStream os = exchange.getResponseBody();
      os.write(response.getBytes());
      os.close();
  }

}

编译: javac SimpleHTTPServer.java

运行: java SimpleHTTPServer

命令详解:

  • javac: Java programs are compiled using the javac command. It takes .java files as input and generates bytecode.
  • java: The java command is used to execute Java bytecodes. It takes bytecode as input, executes it, and outputs the result.

1.5. jar 文件

JAR 文件又称为 JAR 包, 是 Java 的打包文件, 通常包含的是一个完整的 Java 应用程序。

JAR(Java Archive, Java 归档文件)是与平台无关的文件格式, 它允许将许多文件组合成一个压缩文件。为 J2EE 应用程序创建的 JAR 文件是 EAR 文件(企业 JAR 文件)。

JAR 包是由 JDK 安装目录 \bin\jar.exe 命令生成的, 当我们安装好 JDK, 设置好 PATH 路径, 就可以正常使用 jar.exe 命令, 它会用 lib\tool.jar 工具包中的类。这些细节就不用管它了。

1.5.1. jar 命令参数

jar 命令格式: jar {c t x u f }[ v m e 0 M i ][-C 目录] 文件名。..

其中 {ctxu} 这四个参数必须选选其一。[v f m e 0 M i ] 是可选参数, 文件名也是必须的。

  • -c 创建一个 jar 包
  • -t 显示 jar 中的内容列表
  • -x 解压 jar 包
  • -u 添加文件到 jar 包中
  • -f 指定 jar 包的文件名
  • -v 生成详细的报造, 并输出至标准设备
  • -m 指定 MANIFEST.MF 文件。(MANIFEST.MF 文件中可以对 jar 包及其中的内容作一些一设置)
  • -0 产生 jar 包时不对其中的内容进行压缩处理
  • -M 不产生所有文件的清单文件 ( MANIFEST.MF)。这个参数与忽略掉 -m 参数的设置
  • -i 为指定的 jar 文件创建索引文件
  • -C 表示转到相应的目录下执行 jar 命令, 相当于 cd 到那个目录, 然后不带 -C 执行 jar 命令

1.5.2. 打包成 jar 文件

  • command line 打包

不能只打包 .class 文件, 否则会报错误: Error: Could not find or load main class xxx, 意思是找不到 main 类, 这里需要一个 Manifest 文件来指明哪个是 main 类。

首先新建一个 Manifest 文件 META-INF/MANIFEST.MF(文件的目录结构务必按照这个):

Manifest-Version: 1.0 (optional)
Built-By: Arpit Mandliya [YOUR NAME] (optional)
Build-Jdk: 1.8.0_361 (optional)
Created-By: Maven Integration for Eclipse [YOUR ORG NAME] (optional)
Main-Class: SimpleHTTPServer (required) # 语法是: Main-Class: com.mypackage.MyClass

然后是打包命令:

javac SimpleHTTPServer.java
# java SimpleHTTPServer ## for test
jar cmvf META-INF/MANIFEST.MF MyJarFile.jar SimpleHTTPServer.class # 语法是: jar cmvf META-INF/MANIFEST.MF <new-jar-filename>.jar  <files to include>

jar 包测试运行命令:

java -jar MyJarFile.jar # 或 java -cp SimpleHTTPServer MyJarFile.jar, cp 是 classpath 的意思。

打包好的 jar 文件是一个 zip 文件, 可以解压打开。通过这个我们可以解压打开然后排查文件问题。

上面是命令行形式, 如果用了 IDE/框架 的话就省事多了。每样框架的对应(参考自 这里):

  • Maven: pom.xml

  • Ant

  • Gradle

  • maven 打包

mvn package
  • 创建可执行 JAR

创建一个可执行 JAR 很容易。首先将所有应用程序代码放到一个目录中。假设应用程序中的主类是 com.mycompany.myapp.Sample。您要创建一个包含应用程序代码的 JAR 文件并标识出主类。为此, 在某个位置(不是在应用程序目录中)创建一个名为 manifest 的文件, 并在其中加入以下一行:
Main-Class: com.mycompany.myapp.Sample //结尾键入回车

然后, 像这样创建 JAR 文件:
jar cmf manifest ExecutableJar.jar application-dir

所要做的就是这些了。现在可以用 java -jar 执行这个 JAR 文件 ExecutableJar.jar
一个可执行的 JAR 必须通过 manifest 文件的头引用它所需要的所有其他从属 JAR。如果使用了 -jar 选项, 那么环境变量 CLASSPATH 和在命令行中指定的所有类路径都被 JVM 所忽略。

1.5.3. java 中如何打包成 jar 包

  • 手动打包可直接执行的 jar 包
  1. 先使用 javac 编译 java 文件, 得到 class 文件;
  2. 新建文件, 名字任起, 比如可以叫 manifest, 内容如下 (注意: 1. 冒号后面加一个空格, 2. 最后必须回车到新的空行, 否则出错, 如下面内容就必须在 addJarPkg 后面再输入一个回车);
Manifest-Version: 1.0
Main-Class: addJarPkg
  1. 把编译好的 class 文件和第 2 步新建的文件放入指定文件夹, 如 test;
  2. cmd 中运行命令
jar -cvfm main.jar manifest -C test .

注意路径问题, 其中:

test 后面的 “.” 代表所有文件, jar 后面的 -m 选项会将第 2 步新建的文件合并到 jar 包中的 META-INF/MANIFEST.MF, 也就是更新清单配置文件, -C 后面指定要打包的目录, 目录后面的 . 代表目录下所有文件。

  • 使用 intellij idea 工具打包可直接执行的 jar 包
  1. 点击项目
  2. 点击 intellij idea 左上角的 “File” 菜单 -> Project Structure
  3. 点击 “Artifacts” -> 绿色的 “+” -> “JAR” -> Empty
  4. Name 栏填入自定义的名字, Output ditectory 选择 jar 包目标目录, Available Elements 里双击需要添加到 jar 包的文件, 即可添加到左边的 jar 包目录下
  5. 点击 Create Manifest, 选择放置 MANIFEST.MF 的文件路径 (直接默认项目根目录就行, 尽量不要选别的路径, 可能会造成不必要的错误), 点击 OK
  6. 点击 Main Class 后面选择按钮
  7. 弹出框中选择需要运行程序入口 main 函数, 点击 OK
  8. 以上设置完之后, 点击 OK
  9. 点击菜单中 “Build” -> “Build Artifacts”
  10. 双击弹出框中待生成 jar 包下面的 build 即可

1.5.4. MANIFEST.MF 文件编写规则

MANIFEST.MF 的编写一定要注意一些细节, 它是很苛刻的, 我在此也载过不少跟头, 谁让它这么小气呢, 没办法, 所以专门给大家列出来。

  • 不能有空行和空格的地方

第一行不可以是空行(第一行的行前不可以有空行), 行与行之间不能有空行, 第行的行尾不可以有空格。

  • 一定要有空行的地方

最后一行得是空行(在输完你的内容后加一个回车就 OK)。

  • 一定有空格的地方

key: value 在分号后面一定要写写一个空格。

1.5.5. 执行 jar 文件

注意: 直接执行或者双击 jar 文件不会执行 jar 包, 只会检查 jar 包是否正确。jar 包可以在命令行窗口输出。

  • 方式一

java -jar XXX.jar

特点: 当前 ssh 窗口被锁定, 可按 CTRL + C 打断程序运行, 或直接关闭窗口, 程序退出

那如何让窗口不锁定?

  • 方式二

java -jar XXX.jar &

& 代表在后台运行。

特定: 当前 ssh 窗口不被锁定, 但是当窗口关闭时, 程序中止运行。

继续改进, 如何让窗口关闭时, 程序仍然运行?

  • 方式三

nohup java -jar XXX.jar &

nohup 意思是不挂断运行命令, 当账户退出或终端关闭时, 程序仍然运行

当用 nohup 命令执行作业时, 缺省情况下该作业的所有输出被重定向到 nohup.out 的文件中, 除非另外指定了输出文件。

  • 方式四

nohup java -jar XXX.jar >temp.txt &

解释下 >temp.txt:

- command >out.file 是将 command 的输出重定向到 out.file 文件, 即输出内容不打印到屏幕上, 而是输出到 out.file 文件中。

可通过 jobs 命令查看后台运行任务

jobs

那么就会列出所有后台执行的作业, 并且每个作业前面都有个编号。

如果想将某个作业调回前台控制, 只需要 fg + 编号 即可: fg 23

查看某端口占用的线程的 PID: netstat -nlp |grep :9181

1.5.6. 怎样使用 jar 包中的类

还是写个小例子吧, 这样直观!

public final class Person
{
   public static int age()
   {
   return 30;
   }
}

-> javac Person.java
-> jar cvf person.jar Person.class 

将上面的文件打成一个 jar 包

再写一个类对其进行调用:

public class MyAge
{
      public static void getAge()
   {
         System.out.println(Person.age());
      }
}

-> javac MyAge.java
-> java -classpath person.jar MyAge

1.5.7. 创建可执行 jar 包

有时自己写个程序, 类一大堆, 时间一长连自己都不知道那个是主类, 而且有可能用到图片或其它文件一大堆, 看得也乱, 这时你可以考虑把它做成一个可执行 jar 包。…

  • 编辑 MANIFEST.MF 文件加入下面一行

Main-Class: MyApplet

注意: Main-Class 的大小定, 冒号后的空格, MyApplet 后一定输入回车, 然后保存。

  • 1.3.2. 打包

jar cvfm FirstApplet.jar MANIFEST.MF MyApplet.class

注意: MANIFEST.MF 指定为存放 Mani-Class: MyApplet 文件的 class 路径(如: hello.Hello) 或者文件名 (applet)

  • 1.3.3. 可执行 jar 的使用

java -jar FirstApplet.jar

也可以 <applet></applet> 中使用:

<applet code=MyApplet archive=FirstApplet.jar width=200 height=100>
</applet>

注意: 类并没有给出, 大家随便写一个就行, 类名包名自己随意定, 相应的更改就可以。…

1.5.8. 扩展自己的类

在 JDK 的安装目录 \jre\lib\ext 目录下, SUN 为大家为我们扩展自己类的提供了方便, 大家可以将自己的类文件打成 .jar 包放在此目录下, 它由 ExtClassLoader 类装器负责进行装载, ExtClassLoader 类装器是 AppClassLoader 类装载器的父装载器, AppClassLoader 主要负责加载 CLASSPATH 路径下的文件, 而在 java 中采用的又是委托父装载器的机制, 所以此目录下存放的 jar 中的类文件不做任何的设置, 类装载器就可以找到正常的加载, 是不是很方便啊, 呵。…

如果你的 .jar 是给 applet 小应用程序看的, 可以在打成 jar 包之前, 在其 MANIFEST.MF 加入下面两行。

Class-Path: FirstApplet.jar
Class-path: SecondApplet.jar
Main-Class: MyApplet

注意: Class-path 可以设置多项, 直接写 jar 包名既可。Main-Class 主要当 jar 中有多个 .class 类文件时, java 并不知道那个才是主类, 所以要指定, 如果 jar 包中只有一个类当然可以不指定。

Java 调用类的顺序: java\lib\ext 中的类 --> MANIFEST.MF 中指定的类 --> 当前目录中的类 --> set CLASSPATH 中指定的类。

1.5.9. jar 使用范例

  • 创建 jar 包

jar cf hello.jar test
利用 test 目录生成 hello.jar 包, 如 hello.jar 存在, 则覆盖。

  • 创建并显示打包过程

jar cvf hello.jar hello

利用 hello 目录创建 hello.jar 包, 并显示创建过程。

例:

E:\>jar cvf hello.jar hello

标明清单 (manifest)
增加: hello/(读入= 0) (写出= 0)(存储了 0%)
增加: hello/TestServlet2.class(读入= 1497) (写出= 818)(压缩了 45%)
增加: hello/HelloServlet.class(读入= 1344) (写出= 736)(压缩了 45%)
增加: hello/TestServlet1.class(读入= 2037) (写出= 1118)(压缩了 45%)
  • 显示 jar 包:

jar tvf hello.jar 查看 hello.jar 包的内容

指定的 jar 包必须真实存在, 否则会发生 FileNoutFoundException。

  • 解压 jar 包:

jar xvf hello.jar

解压 hello.jar 至当前目录

  • jar 中添加文件:

jar uf hello.jar HelloWorld.java

将 HelloWorld.java 添加到 hello.jar 包中

  • 创建不压缩内容 jar 包:

jar cvf0 hello.jar *.class

利用当前目录中所有的 .class 文件生成一个不压缩 jar 包

  • 创建带 MANIFEST.MF 文件的 jar 包:

jar cvfm hello.jar MANIFEST.MF hello

创建的 jar 包多了一个 META-INF 目录, META-INF 止录下多了一个 MANIFEST.MF 文件, 至于 MANIFEST.MF 的作用, 后面会提到。

  • 忽略 MANIFEST.MF 文件

jar cvfM hello.jar hello

生成的 jar 包中不包括 META-INF 目录及 MANIFEST.MF 文件。

  • -C 应用

jar cvfm hello.jar myMANIFEST.MF -C hello/

表示在切换到 hello 目录下然后再执行 jar 命令。

  • -i 为 jar 文件生成索引列表:

当一个 jar 包中的内容很好的时候, 你可以给它生成一个索引文件, 这样看起来很省事。

jar i hello.jar

执行完这条命令后, 它会在 hello.jar 包的 META-INF 文件夹下生成一个名为 INDEX.LIST 的索引文件, 它会生成一个列表, 最上边为 jar 包名。

  • 导出解压列表:

jar tvf hello.jar >hello.txt

如果你想查看解压一个 jar 的详细过程, 而这个 jar 包又很大, 屏幕信息会一闪而过, 这时你可以把列表输出到一个文件中, 慢慢欣赏!

  • jar -cvf hello.jar hello/*

例如原目录结构如下:

hello
    |---com
    |---org

你本想只把 com 目录和 org 目录打包, 而这时 jar 命令会连同 hello 目录也一块打包进。这点大家要注意。jar 命令生成的压缩文件会包含它后边出的目录。我们应该进入到 hello 目录再执行 jar 命令。

注意: MANIFEST.MF 这个文件名, 用户可以任指定, 但 jar 命令只认识 MANIFEST.MF, 它会对用户指定的文件名进行相应在的转换, 这不需用户担心。

1.5.10. 调用 URL 网络上的 jar 包

  • 生成 jar 包的 URL

URL u=new URL("jar:"+"FirstAppplet.jar"+!/");

  • 建立 jarURLConnection 对象

JarURLConnection juc=(JarURLConnection)u.openConnection();

  • 返回 jar 包中主类的名字
Attributes attr=juc.getMainAttributes();
String name=attr.getValue("Mani-Class");

一定要确保你的 jar 包中的 MANIFEST.MF 中已正确的设置了 Mani-Class 属性, 再强调一下一定要注意规则。

  • 3.8.4. 根据得到的主类名创建 Class 对象

Class c=Class.forName(name);

  • 3.8.5. 根据 Class 对象调用其 main 方法:
Method cm=c.getMethod("main",new Class[]{String.class});
  cm.invoke(null,new Object[]{});

提示: 上边用到了 Reflection 反射机制的相关知识, 大家如果多反射机制有兴趣, 可查看 java.lang.reflect 包中的相关内容。

1.5.11. jar 包和 war 包的区别

jar 包是 Java 打的包, war 包可以理解为 Javaweb 打的包, 这样会比较好记。

jar 包中只是用 Java 来写的项目打包来的, 里面只有编译后的 class 和一些部署文件。

而 war 包里面的东西就全了, 包括写的代码编译成的 class 文件, 依赖的包, 配置文件, 所有的网站页面, 包括 html, jsp 等等。

一个 war 包可以理解为是一个 web 项目, 里面是项目的所有东西。

  • 什么时候使用 jar 包或 war 包

当你的项目在没有完全完成的时候, 不适合使用 war 文件, 因为你的类会由于调试之类的经常改, 这样来回删除、创建 war 文件很不方便, 来回修改, 来回打包, 最好是你的项目已经完成了, 不做修改的时候, 那就打个 war 包吧, 这个时候一个 war 文件就相当于一个 web 应用程序;

而 jar 文件就是把类和一些相关的资源封装到一个包中, 便于程序中引用。

  • 其它

之前在写小项目的时候真的遇到过 war 包, 当时为了找到 jar 包, 把 war 包的后缀名改成了 .zip 的压缩文件, 在里面提取出来 jar 包来用。

其实 jar 包和 war 包都可以看成压缩文件, 用解压软件都可以打开, jar 包和 war 包所存在的原因是, 为了项目的部署和发布, 通常把项目打包, 通常在打包部署的时候, 会在里面加上部署的相关信息。

这个打包实际上就是把代码和依赖的东西压缩在一起, 变成后缀名为 .jar.war 的文件, 就是我们说的 jar 包和 war 包。

但是这个 “压缩包” 可以被编译器直接使用, 把 war 包放在 tomcat 目录的 webapp 下, tomcat 服务器在启动的时候可以直接使用这个 war 包。通常 tomcat 的做法是解压, 编译里面的代码, 所以当文件很多的时候, tomcat 的启动会很慢。

1.5.12. JAR 命令使用技巧

  • jar 创建压 ZIP 文件

jar cvfM TestZIP.jar test

M 参数为了不生成 META-INF 相关内容

然后将 TestZIP.jar 改为 TestZIP.zip 就可以, 是不是很简单。…

  • 使用 WinRAR 解压 .jar 文件

上边我们已经说过了, 说 JAR 文件是一种特殊的压缩文件, 所以它当然可以用我们常用的一些解压缩工具来解了, 至于怎么解, 这就不用我说了吧。

  • 用 WinRAR 生成 .jar 文件

我们已经说过 JAR 包与 ZIP 包主要区别就是 JAR 包中多一个 META-INF 的目录, META-INF 目录下有一个 MANIFEST.MF 文件, 我们只要建立好相关的目录一压缩就可以了。

目录的结构如下:

TestJar
    |--META-INF
        |--MANIFEST.MF
    |--相关的类文件

1.5.13. 如何查看 jar 文件中包含哪些类、方法

  1. 用 WinRAR 打开
  2. 找到想知道类和方法的 class
  3. 反编译 javap -c Class 文件名, 不能加 class 后缀, 例如 javap -c WordCount

1.5.14. 如何知道编译 jar 包的是哪个版本的 JDK

解压 jar 文件: jar xf jolokia.jar

查看文件 META-INF\MANIFEST.MF:

Manifest-Version: 1.0
Premain-Class: org.jolokia.jvmagent.JvmAgent
Archiver-Version: Plexus Archiver
Built-By: roland
Agent-Class: org.jolokia.jvmagent.JvmAgent
Can-Redefine-Classes: false
Can-Retransform-Classes: false
Can-Set-Native-Method-Prefix: false
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_201
Main-Class: org.jolokia.jvmagent.client.AgentLauncher

可以知道其是用 1.8.0_201 版本编译的。

1.6. java -jar 和 java -cp 区别

运行 jar 包的两种方式

  1. java -jar Test.jar
  2. java -cp com.test.Test Test.jar

1.6.1. java -jar

就是通过属性 Main-Class 来找到运行的 main 方法, 所以如果你的 MANIFEST.MF 中没有 Main-Class 属性, 就会提示 Cant load main-class 之类的错误。所以在导出 jar 包的时候一定要指定 Main-class

1.6.2. java -cp

对于 java -cp 就不需要指定 Main-Class 来指定入口。因为第一个参数就指定了你的入口类, 第二个参数就是你的 jar 包。它会根据你的 jar 包找到第一个参数指定的类。

1.6.3. 怎么选择

假设我们这个程序的运行需要依赖一个叫 Dep.jar 的包。

  • 如果我们使用 -jar 的话, 就只能把 Dep.jar 放到 Test.jar 中, 因为 -jar 只能指定一个 jar 包。
  • 如果是使用 -cp, 我们可以选择将 Dep.jar 放到 Test.jar 中, 也可以选择使用以下命令来运行:
java -cp com.test.Test Test.jar:Dep.jar

cp 其实就是 classpath, 在 linux 中多个 jar 包用 : 分割, 代表了程序运行需要的所有 jar 包。这样就可以不用将所有依赖都放到 Test.jar 下。这样做的好处就是, 假如修改了 Test 类, 只上传修改后的 Test.jar 到服务器即可, 不需要再将所有依赖放到 Test.jar 中再上传一遍, 节约了时间。

1.6.4. 技术原理

java -jar 执行 jar 包过程, 到底背后有哪些技术步骤:

  1. 通过 MANIFEST.MF 中的 Main-Class 找到入口类, 启动程序
  2. 启动 JVM, 分配内存 (java 内存结构和 GC 知识)
  3. 根据引用关系加载类(类加载、类加载器、双亲委托机制), 初始化静态块等
  4. 执行程序, 在虚拟机栈创建方法栈桢, 局部变量等信息

1.7. Java 名称解释

1.7.1. Java JMX

Java Management Extensions, Java 管理扩展。是一个为应用程序植入管理功能的框架。JMX 有以下用途:

  1. 监控应用程序的运行状态和相关统计信息。
  2. 修改应用程序的配置(无需重启)。
  3. 状态变化或出错时通知处理。

举个例子, 我们可以通过 jconsole 监控应用程序的堆内存使用量、线程数、类数, 查看某些配置信息, 甚至可以动态地修改配置。另外, 有时还可以利用 JMX 来进行测试。

JMX 的架构分层图:

各层次的简单描述如下:

  • Instrumentation: 主要包括了一系列的接口定义和描述如何开发 MBean 的规范。在 JMX 中 MBean 代表一个被管理的资源实例, 通过 MBean 中暴露的方法和属性, 外界可以获取被管理的资源的状态和操纵 MBean 的行为。
  • Agent: 用来管理相应的资源, 并且为远端用户提供访问的接口。该层的核心是 MBeanServer, 所有的 MBean 都要向它注册, 才能被管理。注册在 MBeanServer 上的 MBean 并不直接和远程应用程序进行通信, 他们通过协议适配器 (Adapter) 和连接器 (Connector) 进行通信。
  • Distributed: 定义了一系列用来访问 Agent 的接口和组件, 包括 Adapter 和 Connector 的描述。注意, Adapter 和 Connector 的区别在于: Adapter 是使用某种 Internet 协议来与 Agent 获得联系, Agent 端会有一个对象 (Adapter) 来处理有关协议的细节。比如 SNMP Adapter 和 HTTP Adapter。而 Connector 则是使用类似 RPC 的方式来访问 Agent, 在 Agent 端和客户端都必须有这样一个对象来处理相应的请求与应答。比如 RMI Connector。

1.7.2. Java RMI

Remote Method Invocation, 远程方法调用。

它支持存储在不同地址空间的程序级对象之间彼此进行通信, 实现远程对象之间的无缝远程调用。

Java RMI:

  1. 用于不同虚拟机之间的通信
  2. 这些虚拟机可以在不同的主机上、也可以在同一个主机上;
  3. 一个虚拟机中的对象调用另一个虚拟上中的对象的方法, 只不过是允许被远程调用的对象要通过一些标志加以标识
  • 优点: 避免重复造轮子;
  • 缺点: 调用过程很慢, 而且该过程是不可靠的, 容易发生不可预料的错误, 比如网络错误等;

从方法调用角度来看, RMI 要解决的问题

  • 是让客户端对远程方法的调用可以相当于对本地方法的调用而屏蔽其中关于远程通信的内容, 即使在远程上, 也和在本地上是一样的。

从客户端-服务器模型来看, 客户端程序直接调用服务端, 两者之间是通过 JRMP( Java Remote Method Protocol) 协议通信, 这个协议类似于 HTTP 协议, 规定了客户端和服务端通信要满足的规范。

实际上, 客户端只与代表远程主机中对象的 Stub 对象进行通信, 丝毫不知道 Server 的存在。

客户端只是调用 Stub 对象中的本地方法, Stub 对象是一个本地对象, 它实现了远程对象向外暴露的接口, 也即它的方法和远程对象暴露的方法的签名是相同的。

客户端认为它是在调用远程对象的方法, 实际上是调用 Stub 对象中的方法。可以理解为 Stub 对象是远程对象在本地的一个代理, 当客户端调用方法的时候, Stub 对象会将调用通过网络传递给远程对象。

1.8. 深入理解 Java 虚拟机的故障处理工具

1.8.1. 前言

本文主要给大家介绍的是 Java 虚拟机的故障处理工具, 文中提到这些工具包括:

名称主要作用
jpsJVM process Status Tool, 显示指定系统内所有的 HotSpot 虚拟机进程, 通常是本地主机
jstatJVM Statistics Monitoring Tool, 用于收集 HotSpot 虚拟机各方面的运行数据
jinfoConfiguration Info for java, 显示虚拟机配置信息
jmapMemory Map for Java, 生成虚拟机的内存存储快照 (heapdump 文件)
jhatJVM Heap Dump Browser, 用于分析 heapdump 文件, 它建立一个 HTTP/HTML 服务器, 让用户可以在浏览器上查看分析结果
jstackStack Trace for Java, 显示虚拟机的线程快照

1.8.2. jps: 虚拟机进程状况工具

jps (Java Virtual Machine Process Status Tool) 是 JDK 1.5 提供的一个显示当前所有 Java 进程 PID 的命令。

jps 的功能和 Unix/Liunx 中的 ps 命令是类似。只不过它是打印出正在运行的虚拟机进程, 并显示虚拟机执行主类的名称以及这些进程的本地虚拟机唯一 ID(Local Virtual Machine Identifier, LVMID, 通常是系统进程 ID)。

查看命令帮助: jps -help

usage: jps [-help]
       jps [-q] [-mlvV] [<hostid>]

Definitions:
    <hostid>:      <hostname>[:<port>]

功能描述: jps 是用于查看有权访问的 hotspot 虚拟机的进程。当未指定 hostid 时, 默认查看本机 JVM 进程, 否者查看指定的 hostid 机器上的 JVM 进程, 此时 hostid 所指机器必须开启 jstatd 服务。jps 可以列出 JVM 进程 lvmid, 主类类名, main 函数参数, JVM 参数, jar 名称等信息。

  • jps 命令格式

jps [options] [hostId]

jps 可以通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程状态, hostId 为 RMI 注册表中注册的主机名称。

  • jps 其他常用选项
-q 只输出 LVMID, 省略主类的名称; 
-m 输出虚拟机进程启动时候传递给主类 main() 函数的参数; 
-l 输出主类的全称, 如果进程执行的是 jar 包, 输出 jar 路径; 
-v 输出虚拟机进程启动时候 JVM 参数。
  • jps 命令样例:
[root@localhost ~]## jps -l
3914 org.zhangyoubao.payservice.App
12180 sun.tools.jps.Jps
6913 org.zhangyoubao.userprofiler.App
  • jps

列出 PID 和 Java 主类名:

2017 Bootstrap
2576 Jps
  • jps -m

输出主函数传入的参数。下面的 hello 就是在执行程序时从命令行输入的参数:

673 Main hello
674 Jps -m
  • jps -l

列出 PID 和 Java 主类全称:

2017 org.apache.catalina.startup.Bootstrap
2612 sun.tools.jps.Jps
  • jps -lm

列出皮带、主类全称和应用程序参数:

2017 org.apache.catalina.startup.Bootstrap start
2588 sun.tools.jps.Jps -lm
  • jps -v

列出 pid 和 JVM 参数:

2017 Bootstrap -Djava.util.logging.config.file=/usr/local/tomcat-web/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Dfile.encoding=UTF-8 -Xms256m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512m -verbose:gc -Xloggc:/usr/local/tomcat-web/logs/gc.log-2014-02-07 -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xnoclassgc -Djava.endorsed.dirs=/usr/local/tomcat-web/endorsed -Dcatalina.base=/usr/local/tomcat-web -Dcatalina.home=/usr/local/tomcat-web -Djava.io.tmpdir=/usr/local/tomcat-web/temp
2624 Jps -Dapplication.home=/usr/lib/jvm/jdk1.6.0_43 -Xms8m
  • jps -V

输出通过 .hotsportrc-XX:Flags=<filename> 指定的 JVM 参数。

1.8.3. jstat: 虚拟机统计信息监视工具

jstat 是用于监视虚拟机各种运行状态信息的工具。它可以显示本地或远程虚拟机进程中类 load, 内存 gc.jit 等运行参数。

1.8.4. jstat 命令格式:

jstat [option vmid [interval [s|ms] [count]]]

intervalcount 代表查询间隔和次数。如果省略这两个参数, 说明只查询一次。

1.8.5. jstat 其他常用选项:

-class            监视类 load/unload 数量、总空间已经装载时间; 
-compiler         输出 JIT 编译器编译过的方法、耗时等信息; 
-printcompilation 输出已经被 JIT 编译的方法; 
-gc               监视 Java 堆状况; 
-gccapacity       监视内容与 -gc 基本相同, 但输出关注 Java 各个区域的最大/最小空间; 
-gcutil           监视内容与 -gc 基本相同, 但输出关注已使用空间占用百分百比; 
-gccause          与 -gcutil 功能一样, 额外输出导致上一次 GC 产生原因; 
-gcnew            监视新生代 GC 状况; 
-gcnewcapacity    监视新生代, 输出同 -gccapacity;
-gcold            监视老年代 GC 状况; 
-gcoldcapacity    监视老年代, 输出同 -gccapacity;
-gcpermcapactiy   监视永久代(代码区), 输出同 -gccapacity;

1.8.6. jstat 命令样例:

[root@localhost ~]## jstat -gc 6913
 S0C S1C S0U S1U  EC  EU  OC   OU  PC  PU YGC  YGCT FGC FGCT  GCT 
34048.0 34048.0 0.0 3217.8 272640.0 171092.7 683264.0 168910.7 46872.0 28031.2 37857 380.644 69

1.8.7. jinfo: Java 配置信息工具

jinfo 的作用是实时的查看和调整虚拟机各项参数。

  • jinfo 命令格式:

jinfo [option] pid

  • jinfo 其他常用选项:
-flag name=value 修改参数
-flag name 参数参数
  • jinfo 命令样例:
[root@localhost ~]## jinfo 6913
Attaching to process ID 6913, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 24.91-b01
Java System Properties:
...

VM Flags:

-Xms1000m -Xmx1000m -Dconf=/usr/local/user_profiler/conf -Dserver.root=/usr/local/user_profiler 

1.8.8. jmap: Java 内存映射工具

jmap 命令可以用于生产堆存储快照 (dump 文件)。它还可以查下 finalize 队列(自我拯救队列)、Java 堆和代码区的详细信息。

  • jmap 命令格式

jmap [option] vmid

  • jmap 其他常用选项:
-dump          生成 java 堆存储快照。格式: -dump:[live,]format=b,file=<filename>;
-finalizerinfo 显示 F-Queue 中等待 Finalizer 现象执行 finalize 方法的对象; 
-heap          显示 Java 堆详细信息, 如使用哪种回收器、参数配置、分代状况等待; 
-histo         显示堆中对象统计信息, 包括类、实例书、合计容量; 
-permstat      以 ClassLoader 为统计入口显示永久代内存信息; 
-F             当虚拟机进程堆 -dump 选项没有响应时候, 可以使用这个选项强制生成 dump 快照。
  • jmap 命令样例:
[root@localhost ~]## jmap -histo 6913|head -20

 num  #instances   #bytes class name
----------------------------------------------
 1:  1864966  113459432 [C
 2:  201846  49201192 [B
 3:  1597065  38329560 java.lang.String
 4:  117477  15037056 org.zhangyoubao.thriftdef.UserUsefulInfo
 5:   47104  11072048 [I
 6:  268631  8596192 java.util.HashMap$Entry
 7:   48812  7451760 <constMethodKlass>
 8:  100683  6443712 com.mysql.jdbc.ConnectionPropertiesImpl$BooleanConnectionProperty
 9:   48812  6257856 <methodKlass>
 10:   4230  5271640 <constantPoolKlass>
 11:  159491  5103712 java.util.Hashtable$Entry
 12:  120226  4809040 org.zhangyoubao.common.cache.adv.Node
 13:  127027  4064864 java.util.concurrent.ConcurrentHashMap$HashEntry
 14:  230433  3686928 java.lang.Integer
 15:   3765  3049824 <constantPoolCacheKlass>
 16:   20917  3012048 com.mysql.jdbc.Field
 17:   4230  2943840 <instanceKlassKlass>

其中 [C=char[],[B=byte[],[S=short[],[I=int[],[[I=int[][]

1.8.9. jhat: 虚拟机堆转存快照分析工具

jhat 命令用于与 jmap 搭配使用, 用来分析 jmap 生成的 dump 文件。jhat 内置了一个微型的 HTTP/HTML 服务器, 生成的 dump 文件的分析结果后, 可以在浏览器查看。

  • jhat 命令格式

jmap filename

  • jhat 命令样例
[root@localhost ~]## jhat html_intercept_server.dump 
Reading from html_intercept_server.dump...
Dump file created Wed Nov 23 13:05:33 CST 2016
Snapshot read, resolving...
Resolving 203681 objects...
Chasing references, expect 40 dots........................................
Eliminating duplicate references........................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

1.8.10. jstack: Java 线程堆栈跟踪工具

jstack 用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机每一条线程正在执行的方法堆栈计划, 生成线程快照的主要目的是定位线程长时间停顿的原因。在线程停顿的时候, 通过 jstack 来查看没有响应的线程在后台做些什么事情, 或者等待着什么资源。

  • jstack 命令格式

jstack [option] vmid

  • jstack 其他选项
-F 当正常输出的请求不被响应的时候, 强制输出线程堆栈; 
-l 除了显示堆栈外, 显示关于锁的附加信息; 
-m 如果调用本地方法, 可以显示 C/C++ 的堆栈。
  • jstack 命令样例
[root@localhost ~]## jstack 29577|head -20
2016-11-23 12:58:23
Full thread dump OpenJDK Server VM (24.91-b01 mixed mode):

"pool-1-thread-7261" prio=10 tid=0x0893a400 nid=0x6b0d waiting on condition [0x652ad000]
 java.lang.Thread.State: TIMED_WAITING (parking)
  at sun.misc.Unsafe.park(Native Method)
  - parking to wait for <0x75b5b400> (a java.util.concurrent.SynchronousQueue$TransferStack)
  at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
  at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:460)
  at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:359)
  at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:942)
  at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
  at java.lang.Thread.run(Thread.java:745)

"service_hot_lscs-0" daemon prio=10 tid=0x6982dc00 nid=0x6aeb waiting on condition [0x64ce1000]
 java.lang.Thread.State: TIMED_WAITING (sleeping)
  at java.lang.Thread.sleep(Native Method)
  at org.zhangyoubao.video.client.runner.SimpleVideoRunner.doWork(SimpleVideoRunner.java:150)

1.9. 类

1.9.1. 封装类

所有的基本类型, 都有对应的类类型, 比如 int 对应的类类型就是 Integer, 这种类就叫做封装类。

  • 数字封装类/Number 类

数字封装类有 Byte,Short,Integer,Long,Float,Double, 这些类都是抽象类 Number 的子类。

  • 基本类型转封装类
int i = 5;
Integer it = new Integer(i);
  • 封装类转基本类型
int i2 = it.intValue();
  • 自动装箱 与 自动拆箱

自动装箱:

不需要调用构造方法, 通过 = 符号自动把 基本类型 转换为 类类型 就叫装箱

int i = 5;

// 自动转换就叫装箱
Integer it2 = i;

自动拆箱:

不需要调用 IntegerintValue 方法, 通过 = 就自动转换成 int 类型, 就叫拆箱

int i3 = it;

1.9.2. Integer 的最大值, 最小值

int 的最大值可以通过其对应的封装类 Integer.MAX_VALUE 获取。

System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);

1.10. 接口与抽象类 (abstract class, interface)

1.10.1. 接口

在 Java 中抽象的关键字为 interface, 接口也可以说是一个更加抽象的抽象类, 对行为进行抽象, 只提供一种形式, 并不提供实施的细节。

接口的语法如下:

[public] interface InterfaceName {
 
}

继承时采用关键字 implements:

class ClassName implements Interface1,Interface2,[....] {
}

接口有以下几个特性:

  1. 接口可以包含变量, 成员变量会被隐式地指定为 public static final 变量(并且只能是 public static final 变量, 用 private 修饰会报编译错误);
  2. 接口可以包含方法, 方法会被隐式地指定为 public abstract 方法且只能是 public abstract 方法(用其他关键字, 比如 private、protected、static、 final 等修饰会报编译错误), 并且接口中所有的方法不能有具体的实现, 也就是说, 接口中的方法必须都是抽象方法;
  3. 一个类可以同时继承多个接口, 且需要实现所继承接口的所有方法

1.10.2. 抽象类

在 Java 中抽象的关键字为 abstract, 抽象类被创造出来就是为了继承, 简单明了地告诉用户跟编译器自己大概是长什么样子的。例如抽象类申明的语法:

abstract class Abc {
    abstract void fun();
}

抽象类有以下几个特性:

  1. 抽象方法必须为 public、protected (若为 private, 则不能给子类继承, 子类无法实现该方法, 所以无意义), 缺省时为 public;
  2. 抽象类不能直接用来创建对象, 必须由子类继承并实现其父类方法才能创建对象;
  3. 抽象类可以继承抽象类, 子类必须复制继承父类的抽象方法;
  4. 只要包含一个抽象方法的类, 该方法必须要定义成抽象类, 不管是否还包含有其他方法。

1.10.3. 抽象类跟接口的区别

查看了很多文章, 无非就以下几点:

  1. 抽象类可以提供成员方法的实现细节, 而接口中只能存在 public abstract 方法;
  2. 抽象类中的成员变量可以是各种类型的, 而接口中的成员变量只能是 public static final 类型的;
  3. 接口中不能含有静态代码块以及静态方法, 而抽象类可以有静态代码块和静态方法;
  4. 一个类只能继承一个抽象类, 而一个类却可以实现多个接口。

那么何时用抽象类? 何时用接口呢?

从生活的角度看:

把编程映射会日常生活进行对照, 那么一个东西, 抽象类表示它是什么, 接口表示它能做什么。举一个栗子, 一个 Person, 他有眼睛、肤色, 这些描述一个人的特征可以定义在抽象类中, 而一个人的行为如打篮球, 所以这些可以定义在接口中。

//抽象类 Person
abstract class Person {
    abstract void eyes();
    abstract void skin();
}
//接口 Action
public interface Action {
    void playBasketball();
}

那么有个中国人, 他不会打篮球, 这个类可以这样写:

public class Chinese extends Person {
    @Override
    void eyes() {
        System.out.print("我的眼睛是黑色的");
    }

    @Override
    void skin() {
        System.out.print("我的皮肤是黄色的");
    }
}

有个俄罗斯人, 他会打篮球, 这个类可以这样写:

public class Russian extends Person implements Action {
    @Override
    void eyes() {
        System.out.print("我的眼睛是黑色的");
    }

    @Override
    void skin() {
        System.out.print("我的皮肤是白色的");
    }

    @Override
    public void playBasketball() {
        System.out.print("我能扣篮");
    }
}

从编程的角度看:

  1. 抽象类适合用来定义某个领域的固有属性, 也就是本质, 接口适合用来定义某个领域的扩展功能
  2. 当需要为一些类提供公共的实现代码时, 应优先考虑抽象类。因为抽象类中的非抽象方法可以被子类继承下来, 使实现功能的代码更简单。
  3. 当注重代码的扩展性可维护性时, 应当优先采用接口。
    • 接口与实现它的类之间可以不存在任何层次关系, 接口可以实现毫不相关类的相同行为, 比抽象类的使用更加方便灵活;
    • 接口只关心对象之间的交互的方法, 而不关心对象所对应的具体类。接口是程序之间的一个协议, 比抽象类的使用更安全、清晰。一般使用接口的情况更多

1.10.4. 接口与抽象类的使用守则

  1. 尽可能使每一个类或成员不被外界访问

这里的外界有个度, 比如包级或者公有的。这样子可以更好地模块化, 模块与模块之间通过暴露的 API 调动。这样如果有个模块改动接口或者类。只要担心该模块, 而不会涉及其他模块。

  1. 适当的使用类(抽象类)继承, 更多的使用复合

继承, 实现了代码重用。内部中使用继承非常安全, 但是要记住什么时候使用继承。即当子类真正是超类的子类型时, 才适用继承。否则尽可能使用复合, 即在一个类中引用另一个类的实例。也就是说将另一个类包装了一下, 这也就是装饰模式所体现的。

  1. 优先考虑使用接口, 相比抽象类

首先 Java 只许单继承, 这导致抽象类定义收到极大的限制。二者, 接口无法实现方法。但是 Java 8 提供了函数式接口。

但是接口在设计的时候注意, 设计公有接口必须谨慎。接口如果被公开发行, 则肯定会被广泛实现, 那样改接口几乎不可能, 会是巨大的工程。(这和我犯的错误一样。)

1.11. Java 如何实现协程

协程 (Coroutine) 这个词其实有很多叫法, 比如有的人喜欢称为纤程 (Fiber), 或者绿色线程 (GreenThread)。其实究其本质, 对于协程最直观的解释是线程的线程。虽然读上去有点拗口, 但本质上就是这样。

协程的核心在于调度那块由他来负责解决, 遇到阻塞操作, 立刻放弃掉, 并且记录当前栈上的数据, 阻塞完后立刻再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑, 这样看上去好像跟写同步代码没有任何差别, 这整个流程可以称为 coroutine, 而跑在由 coroutine 负责调度的线程称为 Fiber。

1.11.1. Java 协程的实现

早期, 在 JVM 上实现协程一般会使用 kilim, 不过这个工具已经很久不更新了, 现在常用的工具是 Quasar, 而本文章会全部基于 Quasar 来介绍。

下面尝试通过 Quasar 来实现类似于 go 语言的 coroutine 以及 channel。

为了能有明确的对比, 这里先用 go 语言实现一个对于 10 以内自然数分别求平方的例子。

func counter(out chan<- int) {
  for x := 0; x < 10; x++ {
    out <- x
  }
  close(out)
}

func squarer(out chan<- int, in <-chan int) {
  for v := range in {
    out <- v * v
  }
  close(out)
}

func printer(in <-chan int) {
  for v := range in {
    fmt.Println(v)
  }
}

func main() {
  //定义两个 int 类型的 channel
  naturals := make(chan int)
  squares := make(chan int)

  //产生两个 Fiber, 用 go 关键字
  go counter(naturals)
  go squarer(squares, naturals)
  //获取计算结果
  printer(squares)
}

上面这个例子, 通过 channel 两解耦两边的数据共享。对于这个 channel, 大家可以理解为 Java 里的 SynchronousQueue。下面我直接上 Quasar 版 JAVA 代码的, 几乎可以原封不动的复制 go 语言的代码。

public class Example {

  private static void printer(Channel<Integer> in) throws SuspendExecution,  InterruptedException {
    Integer v;
    while ((v = in.receive()) != null) {
      System.out.println(v);
    }
  }

  public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution {
    //定义两个 Channel
    Channel<Integer> naturals = Channels.newChannel(-1);
    Channel<Integer> squares = Channels.newChannel(-1);

    //运行两个 Fiber 实现。
    new Fiber(() -> {
      for (int i = 0; i < 10; i++)
        naturals.send(i);
      naturals.close();
    }).start();

    new Fiber(() -> {
      Integer v;
      while ((v = naturals.receive()) != null)
        squares.send(v * v);
      squares.close();
    }).start();

    printer(squares);
  }
}

两者对比, 看上去 Java 似好像更复杂些, 没办法这就是 Java 的风格, 而且这还是通过第三方的库来实现的。

说到这里各位肯定对 Fiber 很好奇了。也许你会表示怀疑 Fiber 是不是如上面所描述的那样, 下面我们尝试用 Quasar 建立一百万个 Fiber, 看看内存占用多少, 我先尝试了创建百万个 Thread。

for (int i = 0; i < 1_000_000; i++) {
  new Thread(() -> {
    try {
      Thread.sleep(10000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }).start();
}

很不幸, 直接报 Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread, 这是情理之中的。下面是通过 Quasar 建立百万个 Fiber。

public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution {
  int FiberNumber = 1_000_000;
  CountDownLatch latch = new CountDownLatch(1);
  AtomicInteger counter = new AtomicInteger(0);

  for (int i = 0; i < FiberNumber; i++) {
    new Fiber(() -> {
      counter.incrementAndGet();
      if (counter.get() == FiberNumber) {
        System.out.println("done");
      }
      Strand.sleep(1000000);
    }).start();
  }
  latch.await();
}

我这里加了 latch, 阻止程序跑完就关闭, Strand.sleep 其实跟 Thread.sleep 一样, 只是这里针对的是 Fiber。

最终控制台是可以输出 done 的, 说明程序已经创建了百万个 Fiber, 设置 Sleep 是为了让 Fiber 一直运行, 从而方便计算内存占用。官方宣称一个空闲的 Fiber 大约占用 400Byte, 那这里应该是占用 400MB 堆内存, 但是这里通过 jmap -heap pid 显示大约占用了 1000MB, 也就是说一个 Fiber 占用 1KB。

1.12. 问题

1.12.1. OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

Question

My current stack is:

  • Java 13
  • Spring boot 2.2.5
  • dd-java-agent 0.44.0

Answer

“Application Class-Data Sharing” is an optimization where users can record class load metadata into a “class-data archive”. Launching your application again reads from this archive so that the application starts faster.

As of Java 12, OpenJDK based JDKs have a class-data archive for JDK classes. Because the Datadog agent appends to the bootstrap classpath, the class-data archive is incorrect for the system class loader.

The warning is telling you that the class data optimization is partially turned off.

The only way to get rid of the warning is to turn the optimization off fully with “-Xshare:off”. However, this could potentially worsen the startup performance of your application.

This is an informational warning which you can safely ignore, the behaviour of your application shouldn’t be affected.

1.13. Quasar 是怎么实现 Fiber 的

其实 Quasar 实现的 coroutine 的方式与 Go 语言很像, 只不过前者是使用框架来实现, 而 go 语言则是语言内置的功能。

不过如果你熟悉了 Go 语言的调度机制的话, 那么对于 Quasar 的调度机制就会好理解很多了, 因为两者有很多相似之处。

Quasar 里的 Fiber 其实是一个 continuation, 他可以被 Quasar 定义的 scheduler 调度, 一个 continuation 记录着运行实例的状态, 而且会被随时中断, 并且也会随后在他被中断的地方恢复。

Quasar 其实是通过修改 bytecode 来达到这个目的, 所以运行 Quasar 程序的时候, 你需要先通过 java-agent 在运行时修改你的代码, 当然也可以在编译期间这么干。go 语言的内置了自己的调度器, 而 Quasar 则是默认使用 ForkJoinPool 这个具有 work-stealing 功能的线程池来当调度器。work-stealing 非常重要, 因为你不清楚哪个 Fiber 会先执行完, 而 work-stealing 可以动态的从其他的等等队列偷一个 context 过来, 这样可以最大化使用 CPU 资源。

那这里你会问了, Quasar 怎么知道修改哪些字节码呢, 其实也很简单, Quasar 会通过 java-agent 在运行时扫描哪些方法是可以中断的, 同时会在方法被调用前和调度后的方法内插入一些 continuation 逻辑, 如果你在方法上定义了 @Suspendable 注解, 那 Quasar 会对调用该注解的方法做类似下面的事情。

这里假设你在方法 f 上定义了 @Suspendable, 同时去调用了有同样注解的方法 g, 那么所有调用 f 的方法会插入一些字节码, 这些字节码的逻辑就是记录当前 Fiber 栈上的状态, 以便在未来可以动态的恢复。(Fiber 类似线程也有自己的栈)。在 suspendable 方法链内 Fiber 的父类会调用 Fiber.park, 这样会抛出 SuspendExecution 异常, 从而来停止线程的运行, 好让 Quasar 的调度器执行调度。这里的 SuspendExecution 会被 Fiber 自己捕获, 业务层面上不应该捕获到。如果 Fiber 被唤醒了(调度器层面会去调用 Fiber.unpark), 那么 f 会在被中断的地方重新被调用(这里 Fiber 会知道自己在哪里被中断), 同时会把 g 的调用结果 (g 会 return 结果)插入到 f 的恢复点, 这样看上去就好像 g 的 return 是 f 的 local variables 了, 从而避免了 callback 嵌套。

上面说了一大堆, 其实简单点来讲就是, 想办法让运行中的线程栈停下来, 然后让 Quasar 的调度器介入。

JVM 线程中断的条件有两个:

  1. 抛异常
  2. return。

而在 Quasar 中, 一般就是通过抛异常的方式来达到的, 所以你会看到上面的代码会抛出 SuspendExecution。但是如果你真捕获到这个异常, 那就说明有问题了, 所以一般会这么写。

@Suspendable
public int f() {
  try {
    // do some stuff
    return g() * 2;
  } catch(SuspendExecution s) {
    //这里不应该捕获到异常。
    throw new AssertionError(s);
  }
}
Java笔记是由北京大学青鸟教育推出的一款专门针对Java语言的学习工具。它以全面、系统、实践为特点,通过详细的代码示例和清晰的讲解,帮助学习者全面掌握Java编程语言。 Java笔记采用了线上与线下相结合的学习模式。学员可以通过手机、平板电脑、电脑等设备在线学习,还可以在学习过程中随时记录自己的学习笔记。同时,北大青鸟还为学员提供线下实践环境,学员可以在实验室里亲自动手实践所学知识,加深理解和应用。 Java笔记的内容非常全面,包括了Java语言的基本语法、面向对象编程、异常处理、流操作、多线程、数据库操作等众多知识点。除了理论知识,Java笔记还提供了大量的实例代码,可供学员参考和模仿。这样的学习方式既帮助学员理解Java的基本概念,又能让他们运用所学知识解决实际问题。 与此同时,Java笔记还注重学员的互动交流。在学习过程中,学员可以利用笔记功能记录学习心得和疑惑,还可以在论坛上与其他学员进行讨论和交流。这种互动形式既能促进学员之间的学习互助,也能更好地帮助学员理解和应用所学知识。 总之,Java笔记是北大青鸟推出的一款专注于Java语言学习的工具,通过系统的课程设置、丰富的实例代码和互动交流的方式,帮助学员全面掌握Java编程知识,提升编程能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值