commit
00b7234de9
|
@ -2,8 +2,17 @@
|
|||
* [一、概览](#一概览)
|
||||
* [二、磁盘操作](#二磁盘操作)
|
||||
* [三、字节操作](#三字节操作)
|
||||
* [实现文件复制](#实现文件复制)
|
||||
* [装饰者模式](#装饰者模式)
|
||||
* [四、字符操作](#四字符操作)
|
||||
* [编码与解码](#编码与解码)
|
||||
* [String](#string)
|
||||
* [Reader 与 Writer](#reader-与-writer)
|
||||
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
|
||||
* [五、对象操作](#五对象操作)
|
||||
* [序列化](#序列化)
|
||||
* [Serializable](#serializable)
|
||||
* [transient](#transient)
|
||||
* [六、网络操作](#六网络操作)
|
||||
* [InetAddress](#inetaddress)
|
||||
* [URL](#url)
|
||||
|
@ -37,7 +46,7 @@ Java 的 I/O 大概可以分成以下几类:
|
|||
|
||||
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
|
||||
|
||||
递归地输出一个目录下所有文件:
|
||||
递归地列出一个目录下所有文件:
|
||||
|
||||
```java
|
||||
public static void listAllFiles(File dir) {
|
||||
|
@ -56,7 +65,7 @@ public static void listAllFiles(File dir) {
|
|||
|
||||
# 三、字节操作
|
||||
|
||||
使用字节流操作进行文件复制:
|
||||
## 实现文件复制
|
||||
|
||||
```java
|
||||
public static void copyFile(String src, String dist) throws IOException {
|
||||
|
@ -77,13 +86,15 @@ public static void copyFile(String src, String dist) throws IOException {
|
|||
}
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
|
||||
## 装饰者模式
|
||||
|
||||
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
|
||||
|
||||
- InputStream 是抽象组件;
|
||||
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
|
||||
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
|
||||
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
|
||||
|
||||
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
|
||||
|
||||
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
|
||||
|
||||
|
@ -96,27 +107,7 @@ DataInputStream 装饰者提供了对更多数据类型进行输入的操作,
|
|||
|
||||
# 四、字符操作
|
||||
|
||||
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
|
||||
|
||||
- InputStreamReader 实现从字节流解码成字符流;
|
||||
- OutputStreamWriter 实现字符流编码成为字节流。
|
||||
|
||||
逐行输出文本文件的内容:
|
||||
|
||||
```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() 方法时会去调用 fileReader 的 close() 方法
|
||||
// 因此只要一个 close() 调用即可
|
||||
bufferedReader.close();
|
||||
}
|
||||
```
|
||||
## 编码与解码
|
||||
|
||||
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
|
||||
|
||||
|
@ -130,6 +121,8 @@ UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-
|
|||
|
||||
Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
|
||||
|
||||
## String
|
||||
|
||||
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
|
||||
|
||||
```java
|
||||
|
@ -145,13 +138,46 @@ System.out.println(str2);
|
|||
byte[] bytes = str1.getBytes();
|
||||
```
|
||||
|
||||
## Reader 与 Writer
|
||||
|
||||
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
|
||||
|
||||
- InputStreamReader 实现从字节流解码成字符流;
|
||||
- OutputStreamWriter 实现字符流编码成为字节流。
|
||||
|
||||
## 实现逐行输出文本文件的内容
|
||||
|
||||
```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();
|
||||
}
|
||||
```
|
||||
|
||||
# 五、对象操作
|
||||
|
||||
## 序列化
|
||||
|
||||
序列化就是将一个对象转换成字节序列,方便存储和传输。
|
||||
|
||||
- 序列化:ObjectOutputStream.writeObject()
|
||||
- 反序列化:ObjectInputStream.readObject()
|
||||
|
||||
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
|
||||
|
||||
## Serializable
|
||||
|
||||
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
|
||||
|
||||
```java
|
||||
|
@ -184,11 +210,11 @@ private static class A implements Serializable {
|
|||
}
|
||||
```
|
||||
|
||||
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
|
||||
## transient
|
||||
|
||||
transient 关键字可以使一些属性不会被序列化。
|
||||
|
||||
ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
|
||||
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
|
||||
|
||||
```java
|
||||
private transient Object[] elementData;
|
||||
|
@ -249,8 +275,8 @@ public static void main(String[] args) throws IOException {
|
|||
|
||||
## Datagram
|
||||
|
||||
- DatagramPacket:数据包类
|
||||
- DatagramSocket:通信类
|
||||
- DatagramPacket:数据包类
|
||||
|
||||
# 七、NIO
|
||||
|
||||
|
|
144
notes/Java 基础.md
144
notes/Java 基础.md
|
@ -62,7 +62,10 @@ int y = x; // 拆箱
|
|||
|
||||
## 缓存池
|
||||
|
||||
new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
|
||||
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
|
||||
|
||||
- new Integer(123) 每次都会新建一个对象
|
||||
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
|
||||
|
||||
```java
|
||||
Integer x = new Integer(123);
|
||||
|
@ -73,14 +76,6 @@ Integer k = Integer.valueOf(123);
|
|||
System.out.println(z == k); // true
|
||||
```
|
||||
|
||||
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
|
||||
|
||||
```java
|
||||
Integer m = 123;
|
||||
Integer n = 123;
|
||||
System.out.println(m == n); // true
|
||||
```
|
||||
|
||||
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
|
||||
|
||||
```java
|
||||
|
@ -125,7 +120,15 @@ static {
|
|||
}
|
||||
```
|
||||
|
||||
Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
|
||||
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
|
||||
|
||||
```java
|
||||
Integer m = 123;
|
||||
Integer n = 123;
|
||||
System.out.println(m == n); // true
|
||||
```
|
||||
|
||||
基本类型对应的缓冲池如下:
|
||||
|
||||
- boolean values true and false
|
||||
- all byte values
|
||||
|
@ -133,7 +136,7 @@ Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些
|
|||
- int values between -128 and 127
|
||||
- char in the range \u0000 to \u007F
|
||||
|
||||
因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
|
||||
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
|
||||
|
||||
[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123
|
||||
](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123)
|
||||
|
@ -186,15 +189,15 @@ String 不可变性天生具备线程安全,可以在多个线程中安全地
|
|||
|
||||
- String 不可变,因此是线程安全的
|
||||
- StringBuilder 不是线程安全的
|
||||
- StringBuffer 是线程安全的,内部使用 synchronized 来同步
|
||||
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
|
||||
|
||||
[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
|
||||
|
||||
## String.intern()
|
||||
|
||||
使用 String.intern() 可以保证相同内容的字符串变量引用相同的内存对象。
|
||||
使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。
|
||||
|
||||
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
|
||||
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
|
||||
|
||||
```java
|
||||
String s1 = new String("aaa");
|
||||
|
@ -223,7 +226,7 @@ System.out.println(s4 == s5); // true
|
|||
|
||||
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
|
||||
|
||||
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
|
||||
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针此时指向的是完全不同的对象,一方改变其所指向对象的内容对另一方没有影响。
|
||||
|
||||
```java
|
||||
public class Dog {
|
||||
|
@ -234,7 +237,11 @@ public class Dog {
|
|||
}
|
||||
|
||||
String getName() {
|
||||
return name;
|
||||
return this.name;
|
||||
}
|
||||
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
String getObjectAddress() {
|
||||
|
@ -262,6 +269,22 @@ public class PassByValueExample {
|
|||
}
|
||||
```
|
||||
|
||||
但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
|
||||
|
||||
```java
|
||||
class PassByValueExample {
|
||||
public static void main(String[] args) {
|
||||
Dog dog = new Dog("A");
|
||||
func(dog);
|
||||
System.out.println(dog.getName()); // B
|
||||
}
|
||||
|
||||
private static void func(Dog dog) {
|
||||
dog.setName("B");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value)
|
||||
|
||||
## float 与 double
|
||||
|
@ -317,7 +340,7 @@ switch (s) {
|
|||
}
|
||||
```
|
||||
|
||||
switch 不支持 long,是因为 switch 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
|
||||
switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
|
||||
|
||||
```java
|
||||
// long x = 111;
|
||||
|
@ -341,33 +364,37 @@ Java 中有三个访问权限修饰符:private、protected 以及 public,如
|
|||
|
||||
可以对类或类中的成员(字段以及方法)加上访问修饰符。
|
||||
|
||||
- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
|
||||
- 类可见表示其它类可以用这个类创建实例对象。
|
||||
- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
|
||||
|
||||
protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
|
||||
|
||||
设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
|
||||
|
||||
如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
|
||||
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
|
||||
|
||||
字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用公有的 getter 和 setter 方法来替换公有字段。
|
||||
字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 共有字段,如果在某个时刻,我们想要使用 int 去存储 id 字段,那么就需要去修改所有的客户端代码。
|
||||
|
||||
```java
|
||||
public class AccessExample {
|
||||
public int x;
|
||||
public String id;
|
||||
}
|
||||
```
|
||||
|
||||
可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
|
||||
|
||||
|
||||
```java
|
||||
public class AccessExample {
|
||||
private int x;
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
private int id;
|
||||
|
||||
public String getId() {
|
||||
return id + "";
|
||||
}
|
||||
|
||||
public void setX(int x) {
|
||||
this.x = x;
|
||||
public void setId(String id) {
|
||||
this.id = Integer.valueOf(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -396,7 +423,7 @@ public class AccessWithInnerClassExample {
|
|||
|
||||
**1. 抽象类**
|
||||
|
||||
抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
|
||||
抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
|
||||
|
||||
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
|
||||
|
||||
|
@ -415,7 +442,7 @@ public abstract class AbstractClassExample {
|
|||
```
|
||||
|
||||
```java
|
||||
public class AbstractExtendClassExample extends AbstractClassExample{
|
||||
public class AbstractExtendClassExample extends AbstractClassExample {
|
||||
@Override
|
||||
public void func1() {
|
||||
System.out.println("func1");
|
||||
|
@ -477,21 +504,21 @@ System.out.println(InterfaceExample.x);
|
|||
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
|
||||
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
|
||||
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
|
||||
- 接口的方法只能是 public 的,而抽象类的方法可以有多种访问权限。
|
||||
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
|
||||
|
||||
**4. 使用选择**
|
||||
|
||||
使用抽象类:
|
||||
|
||||
- 需要在几个相关的类中共享代码。
|
||||
- 需要能控制继承来的成员的访问权限,而不是都为 public。
|
||||
- 需要继承非静态(non-static)和非常量(non-final)字段。
|
||||
|
||||
使用接口:
|
||||
|
||||
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
|
||||
- 需要使用多重继承。
|
||||
|
||||
使用抽象类:
|
||||
|
||||
- 需要在几个相关的类中共享代码。
|
||||
- 需要能控制继承来的成员的访问权限,而不是都为 public。
|
||||
- 需要继承非静态和非常量字段。
|
||||
|
||||
在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
|
||||
|
||||
- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
|
||||
|
@ -499,8 +526,8 @@ System.out.println(InterfaceExample.x);
|
|||
|
||||
## super
|
||||
|
||||
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
|
||||
- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
|
||||
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
|
||||
- 访问父类的成员:如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
|
||||
|
||||
```java
|
||||
public class SuperExample {
|
||||
|
@ -549,7 +576,7 @@ SuperExtendExample.func()
|
|||
|
||||
## 重写与重载
|
||||
|
||||
- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法,子类的返回值类型要等于或者小于父类的返回值;
|
||||
- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。子类的返回值类型要等于或者小于父类的返回值;
|
||||
|
||||
- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
|
||||
|
||||
|
@ -623,7 +650,7 @@ x.equals(null); // false;
|
|||
**2. equals() 与 ==**
|
||||
|
||||
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
|
||||
- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价,根据引用对象 equals() 方法的具体实现来进行比较。
|
||||
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
|
||||
|
||||
```java
|
||||
Integer x = new Integer(1);
|
||||
|
@ -636,7 +663,7 @@ System.out.println(x == y); // false
|
|||
|
||||
- 检查是否为同一个对象的引用,如果是直接返回 true;
|
||||
- 检查是否是同一个类型,如果不是,直接返回 false;
|
||||
- 将 Object 实例进行转型;
|
||||
- 将 Object 对象进行转型;
|
||||
- 判断每个关键域是否相等。
|
||||
|
||||
```java
|
||||
|
@ -667,11 +694,11 @@ public class EqualExample {
|
|||
|
||||
## hashCode()
|
||||
|
||||
hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。
|
||||
hasCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
|
||||
|
||||
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
|
||||
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
|
||||
|
||||
下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
|
||||
下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
|
||||
|
||||
```java
|
||||
EqualExample e1 = new EqualExample(1, 1, 1);
|
||||
|
@ -683,7 +710,7 @@ set.add(e2);
|
|||
System.out.println(set.size()); // 2
|
||||
```
|
||||
|
||||
理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
|
||||
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
|
||||
|
||||
一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。
|
||||
|
||||
|
@ -725,7 +752,7 @@ ToStringExample@4554617c
|
|||
|
||||
**1. cloneable**
|
||||
|
||||
clone() 是 Object 的 protect 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
|
||||
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
|
||||
|
||||
```java
|
||||
public class CloneExample {
|
||||
|
@ -768,6 +795,8 @@ java.lang.CloneNotSupportedException: CloneExample
|
|||
|
||||
以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。
|
||||
|
||||
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
|
||||
|
||||
```java
|
||||
public class CloneExample implements Cloneable {
|
||||
private int a;
|
||||
|
@ -780,12 +809,9 @@ public class CloneExample implements Cloneable {
|
|||
}
|
||||
```
|
||||
|
||||
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
|
||||
**2. 浅拷贝**
|
||||
|
||||
**2. 深拷贝与浅拷贝**
|
||||
|
||||
- 浅拷贝:拷贝实例和原始实例的引用类型引用同一个对象;
|
||||
- 深拷贝:拷贝实例和原始实例的引用类型引用不同对象。
|
||||
拷贝对象和原始对象的引用类型引用同一个对象。
|
||||
|
||||
```java
|
||||
public class ShallowCloneExample implements Cloneable {
|
||||
|
@ -825,6 +851,10 @@ e1.set(2, 222);
|
|||
System.out.println(e2.get(2)); // 222
|
||||
```
|
||||
|
||||
**3. 深拷贝**
|
||||
|
||||
拷贝对象和原始对象的引用类型引用不同对象。
|
||||
|
||||
```java
|
||||
public class DeepCloneExample implements Cloneable {
|
||||
private int[] arr;
|
||||
|
@ -868,6 +898,8 @@ e1.set(2, 222);
|
|||
System.out.println(e2.get(2)); // 2
|
||||
```
|
||||
|
||||
**4. clone() 的替代方案**
|
||||
|
||||
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
|
||||
|
||||
```java
|
||||
|
@ -937,7 +969,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和
|
|||
|
||||
**1. 静态变量**
|
||||
|
||||
- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份。
|
||||
- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份。
|
||||
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
|
||||
|
||||
```java
|
||||
|
@ -956,7 +988,7 @@ public class A {
|
|||
|
||||
**2. 静态方法**
|
||||
|
||||
静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。
|
||||
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。
|
||||
|
||||
```java
|
||||
public abstract class A {
|
||||
|
@ -996,7 +1028,6 @@ public class A {
|
|||
A a2 = new A();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```html
|
||||
|
@ -1028,12 +1059,12 @@ public class OuterClass {
|
|||
|
||||
**5. 静态导包**
|
||||
|
||||
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
|
||||
|
||||
```java
|
||||
import static com.xxx.ClassName.*
|
||||
```
|
||||
|
||||
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
|
||||
|
||||
**6. 初始化顺序**
|
||||
|
||||
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
|
||||
|
@ -1075,11 +1106,12 @@ public InitialOrderTest() {
|
|||
- 子类(实例变量、普通语句块)
|
||||
- 子类(构造函数)
|
||||
|
||||
|
||||
# 七、反射
|
||||
|
||||
每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
|
||||
|
||||
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
|
||||
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。
|
||||
|
||||
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
|
||||
|
||||
|
|
107
notes/Java 容器.md
107
notes/Java 容器.md
|
@ -21,17 +21,17 @@
|
|||
|
||||
# 一、概览
|
||||
|
||||
容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。
|
||||
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
|
||||
|
||||
## Collection
|
||||
|
||||
<div align="center"> <img src="../pics//NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//VP4n3i8m34Ntd28NQ4_0KCJ2q044Oez.png"/> </div><br>
|
||||
|
||||
### 1. Set
|
||||
|
||||
- HashSet:基于哈希表实现,支持快速查找。但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
|
||||
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
|
||||
|
||||
- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN)。
|
||||
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
|
||||
|
||||
- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
|
||||
|
||||
|
@ -53,13 +53,14 @@
|
|||
|
||||
<div align="center"> <img src="../pics//SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png"/> </div><br>
|
||||
|
||||
- HashMap:基于哈希表实现;
|
||||
- TreeMap:基于红黑树实现。
|
||||
|
||||
- HashMap:基于哈希表实现。
|
||||
|
||||
- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
|
||||
|
||||
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
|
||||
|
||||
- TreeMap:基于红黑树实现。
|
||||
|
||||
# 二、容器中的设计模式
|
||||
|
||||
|
@ -129,12 +130,67 @@ private static final int DEFAULT_CAPACITY = 10;
|
|||
|
||||
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
|
||||
|
||||
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
|
||||
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
|
||||
|
||||
```java
|
||||
transient Object[] elementData; // non-private to simplify nested class access
|
||||
```
|
||||
|
||||
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
|
||||
|
||||
```java
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
throws java.io.IOException, ClassNotFoundException {
|
||||
elementData = EMPTY_ELEMENTDATA;
|
||||
|
||||
// Read in size, and any hidden stuff
|
||||
s.defaultReadObject();
|
||||
|
||||
// Read in capacity
|
||||
s.readInt(); // ignored
|
||||
|
||||
if (size > 0) {
|
||||
// be like clone(), allocate array based upon size not capacity
|
||||
ensureCapacityInternal(size);
|
||||
|
||||
Object[] a = elementData;
|
||||
// Read in all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
a[i] = s.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
private void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException{
|
||||
// Write out element count, and any hidden stuff
|
||||
int expectedModCount = modCount;
|
||||
s.defaultWriteObject();
|
||||
|
||||
// Write out size as capacity for behavioural compatibility with clone()
|
||||
s.writeInt(size);
|
||||
|
||||
// Write out all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
s.writeObject(elementData[i]);
|
||||
}
|
||||
|
||||
if (modCount != expectedModCount) {
|
||||
throw new ConcurrentModificationException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
|
||||
|
||||
```java
|
||||
ArrayList list = new ArrayList();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
|
||||
oos.writeObject(list);
|
||||
```
|
||||
|
||||
### 3. 扩容
|
||||
|
||||
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
|
||||
|
@ -241,14 +297,14 @@ public synchronized E get(int index) {
|
|||
}
|
||||
```
|
||||
|
||||
### 2. 与 ArrayList 的区别
|
||||
### 2. 与 ArrayList 的比较
|
||||
|
||||
- Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
|
||||
- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
|
||||
|
||||
### 3. 替代方案
|
||||
|
||||
为了获得线程安全的 ArrayList,可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。
|
||||
可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。
|
||||
|
||||
```java
|
||||
List<String> list = new ArrayList<>();
|
||||
|
@ -267,7 +323,7 @@ List<String> list = new CopyOnWriteArrayList<>();
|
|||
|
||||
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
|
||||
|
||||
写操作需要加锁,防止同时并发写入时导致的写入数据丢失。
|
||||
写操作需要加锁,防止并发写入时导致写入数据丢失。
|
||||
|
||||
写操作结束之后需要把原始数组指向新的复制数组。
|
||||
|
||||
|
@ -331,9 +387,9 @@ transient Node<E> first;
|
|||
transient Node<E> last;
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//49495c95-52e5-4c9a-b27b-92cf235ff5ec.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//49495c95-52e5-4c9a-b27b-92cf235ff5ec.png" width="500"/> </div><br>
|
||||
|
||||
### 2. ArrayList 与 LinkedList
|
||||
### 2. 与 ArrayList 的比较
|
||||
|
||||
- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
|
||||
- ArrayList 支持随机访问,LinkedList 不支持;
|
||||
|
@ -351,7 +407,7 @@ transient Node<E> last;
|
|||
transient Entry[] table;
|
||||
```
|
||||
|
||||
其中,Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即数组中的每个位置被当成一个桶,一个桶存放一个链表,链表中存放哈希值相同的 Entry。也就是说,HashMap 使用拉链法来解决冲突。
|
||||
Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Entry。
|
||||
|
||||
<div align="center"> <img src="../pics//8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png" width="600"/> </div><br>
|
||||
|
||||
|
@ -579,8 +635,8 @@ y&(x-1) : 00000010
|
|||
这个性质和 y 对 x 取模效果是一样的:
|
||||
|
||||
```
|
||||
x : 00010000
|
||||
y : 10110010
|
||||
x : 00010000
|
||||
y%x : 00000010
|
||||
```
|
||||
|
||||
|
@ -638,7 +694,7 @@ void addEntry(int hash, K key, V value, int bucketIndex) {
|
|||
}
|
||||
```
|
||||
|
||||
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。
|
||||
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。
|
||||
|
||||
```java
|
||||
void resize(int newCapacity) {
|
||||
|
@ -684,7 +740,10 @@ capacity : 00010000
|
|||
new capacity : 00100000
|
||||
```
|
||||
|
||||
对于一个 Key,它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 +16。
|
||||
对于一个 Key,
|
||||
|
||||
- 它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样;
|
||||
- 如果为 1,那么得到的结果为原来的结果 +16。
|
||||
|
||||
### 7. 扩容-计算数组容量
|
||||
|
||||
|
@ -723,7 +782,7 @@ static final int tableSizeFor(int cap) {
|
|||
|
||||
从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
|
||||
|
||||
### 9. HashMap 与 HashTable
|
||||
### 9. 与 HashTable 的比较
|
||||
|
||||
- HashTable 使用 synchronized 来进行同步。
|
||||
- HashMap 可以插入键为 null 的 Entry。
|
||||
|
@ -884,7 +943,7 @@ transient LinkedHashMap.Entry<K,V> head;
|
|||
transient LinkedHashMap.Entry<K,V> tail;
|
||||
```
|
||||
|
||||
accessOrder 决定了顺序,默认为 false,此时使用的是插入顺序。
|
||||
accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。
|
||||
|
||||
```java
|
||||
final boolean accessOrder;
|
||||
|
@ -899,7 +958,7 @@ void afterNodeInsertion(boolean evict) { }
|
|||
|
||||
### afterNodeAccess()
|
||||
|
||||
当一个节点被访问时,如果 accessOrder 为 true,则会将 该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
|
||||
当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
|
||||
|
||||
```java
|
||||
void afterNodeAccess(Node<K,V> e) { // move node to last
|
||||
|
@ -949,7 +1008,7 @@ removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承
|
|||
```java
|
||||
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### LRU 缓存
|
||||
|
@ -957,7 +1016,7 @@ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
|
|||
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
|
||||
|
||||
- 设定最大缓存空间 MAX_ENTRIES 为 3;
|
||||
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LUR 顺序;
|
||||
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
|
||||
- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
|
||||
|
||||
```java
|
||||
|
@ -1004,14 +1063,14 @@ private static class Entry<K,V> extends WeakReference<Object> implements Map.Ent
|
|||
|
||||
### ConcurrentCache
|
||||
|
||||
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。
|
||||
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
|
||||
|
||||
ConcurrentCache 采取的是分代缓存:
|
||||
|
||||
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
|
||||
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
|
||||
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,保证频繁被访问的节点不容易被回收。
|
||||
- 当调用 put() 方法时,如果缓存当前容量大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
|
||||
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
|
||||
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
|
||||
|
||||
```java
|
||||
public final class ConcurrentCache<K, V> {
|
||||
|
|
|
@ -8,15 +8,22 @@
|
|||
* [运行时常量池](#运行时常量池)
|
||||
* [直接内存](#直接内存)
|
||||
* [二、垃圾收集](#二垃圾收集)
|
||||
* [判断一个对象是否存活](#判断一个对象是否存活)
|
||||
* [判断一个对象是否可被回收](#判断一个对象是否可被回收)
|
||||
* [引用类型](#引用类型)
|
||||
* [垃圾收集算法](#垃圾收集算法)
|
||||
* [垃圾收集器](#垃圾收集器)
|
||||
* [内存分配与回收策略](#内存分配与回收策略)
|
||||
* [三、类加载机制](#三类加载机制)
|
||||
* [三、内存分配与回收策略](#三内存分配与回收策略)
|
||||
* [Minor GC 和 Full GC](#minor-gc-和-full-gc)
|
||||
* [内存分配策略](#内存分配策略)
|
||||
* [Full GC 的触发条件](#full-gc-的触发条件)
|
||||
* [四、类加载机制](#四类加载机制)
|
||||
* [类的生命周期](#类的生命周期)
|
||||
* [类初始化时机](#类初始化时机)
|
||||
* [类加载过程](#类加载过程)
|
||||
* [类加载器](#类加载器)
|
||||
* [类初始化时机](#类初始化时机)
|
||||
* [类与类加载器](#类与类加载器)
|
||||
* [类加载器分类](#类加载器分类)
|
||||
* [双亲委派模型](#双亲委派模型)
|
||||
* [自定义类加载器实现](#自定义类加载器实现)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
@ -48,19 +55,17 @@ java -Xss=512M HackTheJava
|
|||
|
||||
## 本地方法栈
|
||||
|
||||
本地方法不是用 Java 实现,对待这些方法需要特别处理。
|
||||
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
|
||||
|
||||
与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
|
||||
|
||||
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的, 并且被编译为基于本机硬件和操作系统的程序。
|
||||
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
|
||||
|
||||
<div align="center"> <img src="../pics//JNI-Java-Native-Interface.jpg" width="350"/> </div><br>
|
||||
|
||||
## 堆
|
||||
|
||||
所有对象实例都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
|
||||
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
|
||||
|
||||
现代的垃圾收集器基本都是采用分代收集算法,针对不同的对象采取不同的垃圾回收算法,可以将堆分成两块:
|
||||
现代的垃圾收集器基本都是采用分代收集算法,针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块:
|
||||
|
||||
- 新生代(Young Generation)
|
||||
- 老年代(Old Generation)
|
||||
|
@ -83,9 +88,7 @@ java -Xms=1M -Xmx=2M HackTheJava
|
|||
|
||||
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
||||
|
||||
和堆一样不需要连续的内存,并且可以动态扩展。
|
||||
|
||||
动态扩展失败一样会抛出 OutOfMemoryError 异常。
|
||||
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
|
||||
|
||||
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
|
||||
|
||||
|
@ -107,15 +110,15 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用
|
|||
|
||||
# 二、垃圾收集
|
||||
|
||||
垃圾回收主要是针对堆和方法区进行。
|
||||
垃圾收集主要是针对堆和方法区进行。
|
||||
|
||||
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
|
||||
|
||||
## 判断一个对象是否存活
|
||||
## 判断一个对象是否可被回收
|
||||
|
||||
### 1. 引用计数算法
|
||||
|
||||
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数不为 0 的对象仍然存活。
|
||||
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
|
||||
|
||||
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
|
||||
|
||||
|
@ -123,6 +126,7 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用
|
|||
|
||||
```java
|
||||
public class ReferenceCountingGC {
|
||||
|
||||
public Object instance = null;
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
@ -147,61 +151,7 @@ Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC
|
|||
- 方法区中类静态属性引用的对象
|
||||
- 方法区中的常量引用的对象
|
||||
|
||||
### 3. 引用类型
|
||||
|
||||
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
|
||||
|
||||
Java 具有四种强度不同的引用类型。
|
||||
|
||||
**(一)强引用**
|
||||
|
||||
被强引用关联的对象不会被回收。
|
||||
|
||||
使用 new 一个新对象的方式来创建强引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
```
|
||||
|
||||
**(二)软引用**
|
||||
|
||||
被软引用关联的对象只有在内存不够的情况下才会被回收。
|
||||
|
||||
使用 SoftReference 类来创建软引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
SoftReference<Object> sf = new SoftReference<Object>(obj);
|
||||
obj = null; // 使对象只被软引用关联
|
||||
```
|
||||
|
||||
**(三)弱引用**
|
||||
|
||||
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾收集。
|
||||
|
||||
使用 WeakReference 类来实现弱引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
WeakReference<Object> wf = new WeakReference<Object>(obj);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
**(四)虚引用**
|
||||
|
||||
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
|
||||
|
||||
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
|
||||
|
||||
使用 PhantomReference 来实现虚引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
### 4. 方法区的回收
|
||||
### 3. 方法区的回收
|
||||
|
||||
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。
|
||||
|
||||
|
@ -217,12 +167,66 @@ obj = null;
|
|||
|
||||
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
|
||||
|
||||
### 5. finalize()
|
||||
### 4. finalize()
|
||||
|
||||
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
|
||||
|
||||
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 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);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
## 垃圾收集算法
|
||||
|
||||
### 1. 标记 - 清除
|
||||
|
@ -376,20 +380,21 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和
|
|||
|
||||
更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
|
||||
|
||||
## 内存分配与回收策略
|
||||
# 三、内存分配与回收策略
|
||||
|
||||
### 1. Minor GC 和 Full GC
|
||||
## Minor GC 和 Full GC
|
||||
|
||||
- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
|
||||
|
||||
- Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
|
||||
|
||||
### 2. 内存分配策略
|
||||
## 内存分配策略
|
||||
|
||||
(一)对象优先在 Eden 分配
|
||||
### 1. 对象优先在 Eden 分配
|
||||
|
||||
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
|
||||
|
||||
(二)大对象直接进入老年代
|
||||
### 2. 大对象直接进入老年代
|
||||
|
||||
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
|
||||
|
||||
|
@ -397,41 +402,41 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和
|
|||
|
||||
-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。
|
||||
|
||||
### 3. Full GC 的触发条件
|
||||
## Full GC 的触发条件
|
||||
|
||||
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
|
||||
|
||||
(一)调用 System.gc()
|
||||
### 1. 调用 System.gc()
|
||||
|
||||
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
|
||||
|
||||
(二)老年代空间不足
|
||||
### 2. 老年代空间不足
|
||||
|
||||
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
|
||||
|
||||
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
|
||||
|
||||
(三)空间分配担保失败
|
||||
### 3. 空间分配担保失败
|
||||
|
||||
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
|
||||
|
||||
(四)JDK 1.7 及以前的永久代空间不足
|
||||
### 4. JDK 1.7 及以前的永久代空间不足
|
||||
|
||||
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
|
||||
|
||||
|
@ -439,13 +444,13 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和
|
|||
|
||||
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
|
||||
|
||||
(五)Concurrent Mode Failure
|
||||
### 5. Concurrent Mode Failure
|
||||
|
||||
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
|
||||
|
||||
# 三、类加载机制
|
||||
# 四、类加载机制
|
||||
|
||||
类是在运行期间动态加载的。
|
||||
类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存。
|
||||
|
||||
## 类的生命周期
|
||||
|
||||
|
@ -461,10 +466,108 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和
|
|||
- 使用(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。
|
||||
|
||||
```java
|
||||
public static final int value = 123;
|
||||
```
|
||||
|
||||
### 4. 解析
|
||||
|
||||
将常量池的符号引用替换为直接引用的过程。
|
||||
|
||||
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
|
||||
|
||||
### 5. 初始化
|
||||
|
||||
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
|
||||
|
||||
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
|
||||
|
||||
<clinit>() 方法具有以下特点:
|
||||
|
||||
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
static {
|
||||
i = 0; // 给变量赋值可以正常编译通过
|
||||
System.out.print(i); // 这句编译器会提示“非法向前引用”
|
||||
}
|
||||
static int i = 1;
|
||||
}
|
||||
```
|
||||
|
||||
- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
|
||||
|
||||
- 由于父类的 <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
|
||||
}
|
||||
```
|
||||
|
||||
- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。
|
||||
|
||||
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
|
||||
|
||||
- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
|
||||
|
||||
## 类初始化时机
|
||||
|
||||
### 1. 主动引用
|
||||
|
||||
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
|
||||
|
||||
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
|
||||
|
@ -477,6 +580,8 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和
|
|||
|
||||
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
|
||||
|
||||
### 2. 被动引用
|
||||
|
||||
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
|
||||
|
||||
- 通过子类引用父类的静态字段,不会导致子类初始化。
|
||||
|
@ -497,118 +602,13 @@ SuperClass[] sca = new SuperClass[10];
|
|||
System.out.println(ConstClass.HELLOWORLD);
|
||||
```
|
||||
|
||||
## 类加载过程
|
||||
## 类与类加载器
|
||||
|
||||
包含了加载、验证、准备、解析和初始化这 5 个阶段。
|
||||
|
||||
### 1. 加载
|
||||
|
||||
加载是类加载的一个阶段,注意不要混淆。
|
||||
|
||||
加载过程完成以下三件事:
|
||||
|
||||
- 通过一个类的全限定名来获取定义此类的二进制字节流。
|
||||
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
|
||||
- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
|
||||
|
||||
其中二进制字节流可以从以下方式中获取:
|
||||
|
||||
- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
|
||||
- 从网络中获取,这种场景最典型的应用是 Applet。
|
||||
- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
|
||||
- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
|
||||
- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
|
||||
...
|
||||
|
||||
### 2. 验证
|
||||
|
||||
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
|
||||
|
||||
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
|
||||
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
|
||||
- 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
|
||||
- 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
|
||||
|
||||
### 3. 准备
|
||||
|
||||
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
|
||||
|
||||
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
|
||||
|
||||
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
|
||||
|
||||
```java
|
||||
public static int value = 123;
|
||||
```
|
||||
|
||||
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
|
||||
|
||||
```java
|
||||
public static final int value = 123;
|
||||
```
|
||||
|
||||
### 4. 解析
|
||||
|
||||
将常量池的符号引用替换为直接引用的过程。
|
||||
|
||||
### 5. 初始化
|
||||
|
||||
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
|
||||
|
||||
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
|
||||
|
||||
<clinit>() 方法具有以下特点:
|
||||
|
||||
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
static {
|
||||
i = 0; // 给变量赋值可以正常编译通过
|
||||
System.out.print(i); // 这句编译器会提示“非法向前引用”
|
||||
}
|
||||
static int i = 1;
|
||||
}
|
||||
```
|
||||
|
||||
- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
|
||||
|
||||
- 由于父类的 <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); // 输出结果是父类中的静态变量 A 的值,也就是 2。
|
||||
}
|
||||
```
|
||||
|
||||
- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。
|
||||
|
||||
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
|
||||
|
||||
- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
|
||||
|
||||
## 类加载器
|
||||
|
||||
在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。
|
||||
|
||||
### 1. 类与类加载器
|
||||
|
||||
两个类相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
|
||||
两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
|
||||
|
||||
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
|
||||
|
||||
### 2. 类加载器分类
|
||||
## 类加载器分类
|
||||
|
||||
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
|
||||
|
||||
|
@ -624,7 +624,7 @@ public static void main(String[] args) {
|
|||
|
||||
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
|
||||
|
||||
### 3. 双亲委派模型
|
||||
## 双亲委派模型
|
||||
|
||||
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
|
||||
|
||||
|
@ -632,17 +632,17 @@ public static void main(String[] args) {
|
|||
|
||||
<div align="center"> <img src="../pics//class_loader_hierarchy.png" width="600"/> </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。
|
||||
例如 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,此时尝试自己去加载。
|
||||
|
||||
|
@ -690,7 +690,7 @@ public abstract class ClassLoader {
|
|||
}
|
||||
```
|
||||
|
||||
### 4. 自定义类加载器实现
|
||||
## 自定义类加载器实现
|
||||
|
||||
FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
## 危害
|
||||
|
||||
- 窃取用户的 Cookie 值
|
||||
- 窃取用户的 Cookie
|
||||
- 伪造虚假的输入表单骗取个人信息
|
||||
- 显示伪造的文章或者图片
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
|
||||
|
||||
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
|
||||
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,通过定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
|
||||
|
||||
以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
|
||||
|
||||
|
@ -131,7 +131,7 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
|
|||
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">。
|
||||
```
|
||||
|
||||
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
|
||||
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 美元。
|
||||
|
||||
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
|
||||
|
||||
|
@ -153,8 +153,6 @@ Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。
|
|||
|
||||
因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。
|
||||
|
||||
也可以要求用户输入验证码来进行校验。
|
||||
|
||||
# 三、SQL 注入攻击
|
||||
|
||||
## 概念
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。
|
||||
|
||||
<div align="center"> <img src="../pics//09b52bcb-88ba-4e36-8244-b375f16ad116.jpg"/> </div><br>
|
||||
<div align="center"> <img src="../pics//685a692f-8f76-4cac-baac-b68e2df9a30f.jpg"/> </div><br>
|
||||
|
||||
## 发布/订阅
|
||||
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
## 接收端的可靠性
|
||||
|
||||
接收端能够从消息中间件成功消费一次消息。
|
||||
接收端能够从消息队列成功消费一次消息。
|
||||
|
||||
实现方法:
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
实现可扩展主要有两种方式:
|
||||
|
||||
- 使用消息队列进行解耦,应用之间通过消息传递的方式进行通信;
|
||||
- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以用过调用可复用的服务来实现业务逻辑,对其它产品没有影响。
|
||||
- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以通过调用可复用的服务来实现业务逻辑,对其它产品没有影响。
|
||||
|
||||
# 四、可用性
|
||||
|
||||
|
|
14
notes/缓存.md
14
notes/缓存.md
|
@ -35,10 +35,10 @@
|
|||
|
||||
# 二、LRU
|
||||
|
||||
以下是一个基于 双向队列 + HashMap 的 LRU 算法实现,对算法的解释如下:
|
||||
以下是一个基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:
|
||||
|
||||
- 最基本的思路是当访问某个节点时,将其从原来的位置删除,并重新插入到链表头部,这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就删除链表尾部的节点。
|
||||
- 为了使删除操作时间复杂度为 O(1),那么就不能采用遍历的方式找到某个节点。HashMap 存储这 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
|
||||
- 为了使删除操作时间复杂度为 O(1),那么就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
|
||||
|
||||
```java
|
||||
public class LRU<K, V> implements Iterable<K> {
|
||||
|
@ -143,6 +143,10 @@ public class LRU<K, V> implements Iterable<K> {
|
|||
}
|
||||
```
|
||||
|
||||
源代码:
|
||||
|
||||
- [CyC2018/Algorithm](https://github.com/CyC2018/Algorithm/tree/master/Caching)
|
||||
|
||||
# 三、缓存位置
|
||||
|
||||
## 浏览器
|
||||
|
@ -165,9 +169,7 @@ public class LRU<K, V> implements Iterable<K> {
|
|||
|
||||
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
|
||||
|
||||
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。
|
||||
|
||||
不仅如此,服务器集群都可以访问分布式缓存。而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。
|
||||
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存。而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。
|
||||
|
||||
## 数据库缓存
|
||||
|
||||
|
@ -263,7 +265,7 @@ Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了
|
|||
|
||||
上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。
|
||||
|
||||
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得大,那么虚拟节点在哈希环上分布的均匀性就会比原来的真是节点好,从而使得数据分布也更加均匀。
|
||||
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得大,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。
|
||||
|
||||
参考资料:
|
||||
|
||||
|
|
22
notes/集群.md
22
notes/集群.md
|
@ -1,6 +1,6 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [一、负载均衡](#一负载均衡)
|
||||
* [算法实现](#算法实现)
|
||||
* [负载均衡算法](#负载均衡算法)
|
||||
* [转发实现](#转发实现)
|
||||
* [二、集群下的 Session 管理](#二集群下的-session-管理)
|
||||
* [Sticky Session](#sticky-session)
|
||||
|
@ -11,27 +11,27 @@
|
|||
|
||||
# 一、负载均衡
|
||||
|
||||
集群中的应用服务器通常被设计成无状态,用户可以请求任何一个节点(应用服务器)。
|
||||
集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个应用服务器。
|
||||
|
||||
负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。
|
||||
|
||||
负载均衡器可以用来实现高可用以及伸缩性:
|
||||
|
||||
- 高可用:当某个节点故障时,负载均衡器不会将用户请求转发到该节点上,从而保证所有服务持续可用;
|
||||
- 高可用:当某个节点故障时,负载均衡器会将用户请求转发到另外的节点上,从而保证所有服务持续可用;
|
||||
- 伸缩性:可以很容易地添加移除节点。
|
||||
|
||||
负载均衡运行过程包含两个部分:
|
||||
|
||||
1. 根据负载均衡算法得到请求转发的节点;
|
||||
2. 将请求进行转发;
|
||||
2. 将请求进行转发。
|
||||
|
||||
## 算法实现
|
||||
## 负载均衡算法
|
||||
|
||||
### 1. 轮询(Round Robin)
|
||||
|
||||
轮询算法把每个请求轮流发送到每个服务器上。
|
||||
|
||||
下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
|
||||
下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
|
||||
|
||||
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
|||
|
||||
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。
|
||||
|
||||
例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。
|
||||
例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开,此时 (6, 4) 请求连接服务器 2。该系统继续运行时,服务器 2 会承担过大的负载。
|
||||
|
||||
<div align="center"> <img src="../pics//3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg"/> </div><br>
|
||||
|
||||
|
@ -100,7 +100,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
|
|||
|
||||
### 2. DNS 域名解析
|
||||
|
||||
在 DNS 解析域名的同时使用负载均衡算法计算服务器地址。
|
||||
在 DNS 解析域名的同时使用负载均衡算法计算服务器 IP 地址。
|
||||
|
||||
优点:
|
||||
|
||||
|
@ -151,7 +151,9 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
|
|||
|
||||
在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。
|
||||
|
||||
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,直接转发给客户端,避免了负载均衡服务器的成为瓶颈。这是一种三角传输模式,被称为直接路由,对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
|
||||
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。
|
||||
|
||||
这是一种三角传输模式,被称为直接路由,对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
|
||||
|
||||
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVS(Linux Virtual Server)。
|
||||
|
||||
|
@ -188,7 +190,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
|
|||
|
||||
## Session Server
|
||||
|
||||
使用一个单独的服务器存储 Session 数据,可以使用 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。
|
||||
使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。
|
||||
|
||||
优点:
|
||||
|
||||
|
|
BIN
pics/685a692f-8f76-4cac-baac-b68e2df9a30f.jpg
Normal file
BIN
pics/685a692f-8f76-4cac-baac-b68e2df9a30f.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
pics/VP4n3i8m34Ntd28NQ4_0KCJ2q044Oez.png
Normal file
BIN
pics/VP4n3i8m34Ntd28NQ4_0KCJ2q044Oez.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
pics/urlnuri.jpg
Normal file
BIN
pics/urlnuri.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue
Block a user