0%

单例模式是一次只创建一个实例,不允许多个存在。所以让类自身保存它的唯一实例。这个类保证没有其他实例可以被创建,并提供一个其他类访问的方法。先前在设计模式-单例模式中简单了介绍一下,现在开始填这个坑,比较几种单例模式的线程安全问题。

懒汉模式

懒汉模式就是在对象第一次使用时创建对象,下面这种方式,是非线程安全的,因为当多个线程同时判断成功instance == null,就会创建多个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author Gavin
* 懒汉模式,单例实例在第一次使用时创建对象
*/
public class SingletonExample1 {
//私有构造函数
private SingletonExample1() {

}
//单例对象
private static SingletonExample1 instance = null;
//静态工厂方法
public static SingletonExample1 getInstance(){
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}

为了保证线程安全,如下面代码所示,我们对getInstance()添加synchronized关键字,保证同一时间只有一个线程访问getInstance()方法。这种方法一次只能允许一个线程访问,会造成严重的性能问题,虽然是线程安全,但是不推荐使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
*
* @author Gavin
* 懒汉模式,单例实例在第一次使用时创建对象
*/
public class SingletonExample1 {
//私有构造函数
private SingletonExample1() {

}
//单例对象
private static SingletonExample1 instance = null;
//静态工厂方法
public static synchronized SingletonExample1 getInstance(){
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}

}

为了解决性能问题,我们使用双重同步锁单例模式,如下面代码所示,在进入锁之前,判断一次instance是否为空,获取锁之后,再判断一次,提高了程序的性能。但是,这真的是线程安全吗? 答案是否定的, 因为会发生CPU指令的重排序问题。
当执行instance = new SingletonExample1()时,会进行3步操作:

  • 1.memory = allocate() //分配对象内存空间
  • 2.ctorInstance() //初始化对象
  • 3.instance = memory //设置instance指向刚分配的内存

当JVM和CPU发生指令重排时,假如上面2和3步骤发生交换,当一个线程将instance指向刚分配的内存,另一个线程使用了返回的instance,但是这时还没有初始化对象,就会发生错误,这种错误发生的几率很小,但也是非线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
*
* @author Gavin
* 懒汉模式,单例实例在第一次使用时创建对象
*/
public class SingletonExample1 {
//私有构造函数
private SingletonExample1() {

}
//单例对象
private static SingletonExample1 instance = null;
//静态工厂方法
public static SingletonExample1 getInstance(){
if (instance == null) {
synchronized (SingletonExample1.class) {
if(instance == null){
instance = new SingletonExample1();
}
}

}
return instance;
}
}

为了解决上面可能会出现的错误问题,需要用volatile修饰instance变量,防止指令重排序。如下面所示代码,为线程安全,并且推荐使用的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
*
* @author Gavin
* 懒汉模式,单例实例在第一次使用时创建对象
*/
public class SingletonExample1 {
//私有构造函数
private SingletonExample1() {

}
//单例对象
private static volatile SingletonExample1 instance = null;
//静态工厂方法
public static SingletonExample1 getInstance(){
if (instance == null) {
synchronized (SingletonExample1.class) {
if(instance == null){
instance = new SingletonExample1();
}
}

}
return instance;
}
}

饿汉模式

饿汉模式是在类装载时进行创建。下面这个类,是线程安全的,如果instance未被调用,会浪费资源。如果构造函数中包含过多处理,会导致加载非常慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*
* @author Gavin
* 饿汉模式 类装载时创建
*/
public class SingletonExample2 {
//私有构造函数
private SingletonExample2() {

}
//单例对象
private static SingletonExample2 instance = new SingletonExample2();
//静态工厂方法
public static SingletonExample2 getInstance(){
return instance;
}
}

除了上面直接创建对象,还有一种方法,可以将对象的创建放在静态代码块中,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
*
* @author Gavin
* 饿汉模式 类装载时创建
*/
public class SingletonExample2 {
//私有构造函数
private SingletonExample2() {

}
//单例对象
private static SingletonExample2 instance = null;

static {
instance = new SingletonExample2();
}
//静态工厂方法
public static SingletonExample2 getInstance(){
return instance;
}
}

枚举单例

枚举单例模式,和懒汉模式相比,更容易保证线程安全;和饿汉模式相比,只有在实际调用时才初始化,保证了性能。因此推荐使用枚举模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package fun.gwt.base;

/**
*
* @author Gavin
* 线程安全
*/
public class SingletonExample3 {
//私有构造函数
private SingletonExample3() {

}

//静态工厂方法
public static SingletonExample3 getInstance(){
return Singleton.INSTANCE.getInstance();
}

private enum Singleton {
INSTANCE;

private SingletonExample3 singleton;
//jvm保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample3();
}
public SingletonExample3 getInstance() {
return singleton;
}
}
}

简介

在Java运行过程中创建的对象,随着JVM的的停止,对象也会消失。为了将对象保存在本地硬盘或者是进行网络传输,需要将对象序列化,在需要的时候重新获取对象状态。Java为对象序列化和反序列化提供了两个接口:

1
2
java.io.Serializable
java.io.Externalizable

下面通过例子来演示两个接口的作用和区别。

Serializable

实现对象的序列化,必须实现Serializable接口。首先实现要序列化的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.Serializable;
public class Student implements Serializable{
private static final long serialVersionUID = 1195127496667537096L;
private String name;
private int age;
private String sex;
private int id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
//重写toString方法,方便输出信息
@Override
public String toString(){
return "姓名是:"+name+",年龄是"+age+",性别是:"+sex+",学号是:"+id;

}
}

演示怎么样序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package fun.gwt.base;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializableDemos {

public static void main(String[] args) throws Exception {
//初始化对象
Student stu = new Student();
stu.setName("Gavin");
stu.setAge(24);
stu.setSex("男");
stu.setId(123456);
System.out.println(stu);

//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new
//将对象保存在了本地文件中
FileOutputStream("studentObject"));
oos.writeObject(stu);
oos.close();

//读出来对象
File file = new File("studentObject");
//读取文件内容
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student stu1 = (Student) ois.readObject();
System.out.println(stu1);
ois.close();
}
}
/*
姓名是:Gavin,年龄是24,性别是:男,学号是:123456
姓名是:Gavin,年龄是24,性别是:男,学号是:123456
*/

Externalizable

将上面的代码修改为实现Externalizable接口的Demo,需要在类中添加writeExternal()和readExternal()方法,下面是修改好的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package fun.gwt.base;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;


public class Student implements Externalizable{
/**
*
*/
private static final long serialVersionUID = 1195127496667537096L;
private String name;
private int age;
private String sex;
private int id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString(){
return "姓名是:"+name+",年龄是"+age+",性别是:"+sex+",学号是:"+id;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {

}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {

}
}

序列化和反序列化的main方法不需要修改,直接执行:

1
2
姓名是:Gavin,年龄是24,性别是:男,学号是:123456
姓名是:null,年龄是0,性别是:null,学号是:0

输出结果发现对象序列化了,但是对象的状态并没有序列化成功,里面的值都变成了默认值。Externalizable继承Serializable类,并且增加了writeExternal()和readExternal()方法,用户必须自己实现这两个方法。现在修改代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(id);
out.writeObject(sex);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
name = (String)in.readObject();
id = in.readInt();
sex = (String)in.readObject();
}

这里只穿进去3个值,输出结果为:

1
2
姓名是:Gavin,年龄是24,性别是:男,学号是:123456
姓名是:Gavin,年龄是0,性别是:男,学号是:123456

可以看到,年龄没有序列化,这就是Externalizable类和Serializable类的区别。

serialVersionUID

serialVersionUID主要是判断类是否发生改变。主要分为两种情况:

  • 没有指定serialVersionUID的值
  • 指定了serialVersionUID的值

1、没有指定serialVersionUID的值,JVM为自动为类创建一个serialVersionUID,对类的任何修改都会使JVM改变serialVersionUID的值。如果对象反序列化之前改变了类,那么在反序列化时,JVM会对比serialVersionUID的值,修改了类,serialVersionUID的值不相同,会反序列化失败。
2、如果指定serialVersionUID的值,在对象反序列化之前修改了类,如果不影响反序列化,那么在反序列化时,对比serialVersionUID值相同,还是可以进行反序列化操作。

transient

对于一些成员变量不需要进行序列化,因此可以使用transient关键字修饰该成员变量,反序列化后该成员变量为默认值。

简介

程序计数器、虚拟机栈和本地方法栈的生存周期就是线程的生存周期,因此不需要考虑垃圾回收。Java中,GC的对象是堆空间和永久区。

怎么判断一个对象是否需要回收?

引用计数法

引用计数法就是给对象中添加一个引用计数器,当有对象引用时,计数器数值加1,引用失效时,计数器数值减1,当计数器数值为0,就通知GC回收对象。
QQ20190211172059jpg
图片中的B对象引用了D对象,如果B对象不再引用D对象,D对象的计数器数值就置为0,这样GC也会收回D对象。如果对象之间存在循环引用,就会出现无法回收的对象。如下图所示:
imagepng
图中BCD循环引用,根对象已经不引用B对象,也就是说BCD对象应该被GC,但因为BCD计数器数值都不为0,因此引用计数法无法通知GC来回收对象。就是存在这种问题,Java虚拟机中没有选用引用计数法来判断对象是否存活。

可达性分析算法

在Java和C#等主流商用程序语言中,都是使用可达性分析算法来判断对象是否存活。算法思路是从一系列的“GC Roots”的对象最为起始点,从这些节点向下搜索,走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,就说明此对象不可用。如下图所示:
imagepng
设A为根对象,如果A和B之间没有引用链相连,那么可以判定BCD不可达,需要GC来回收。
GC Roots对象包括:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

对象:我是不是还能拯救一下

对象GC之前,至少要经过两次标记。如果对象在可达性分析算法之后,没有引用链与GC Roots相连,就会被第一次标记,如果对象没有覆盖finalize()方法或者已经调用过一次finalize()方法(finalize()方法只能被调用一次),就将这个对象回收。如果对象已经覆盖finalize()方法,并且未被调用finalize()方法,就将这个对象放在F-Queue的队列中,由Finalizer线程执行finalize()方法,但不承诺等待运行结束,如果对象未能在执行finalize()方法时自救成功,将会被GC第二次标记,并回收。
finalize()方法中,对象重新和引用链中任何一个对象关联上,例如this指向某个类变量或者对象的成员变量,那这个对象就不会被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package fun.gwt.jvm;

public class GC {
public static GC SAVE_HOOK = null;
public void isAlive(){
System.out.println("我还活着!");
}
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("执行finalize()方法!");
GC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new GC();
//第一次拯救自己
System.out.println("开始第一次拯救自己.....");
SAVE_HOOK = null;
System.gc();
Thread.sleep(1000);
if (SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("我死了!");
}

//第二次拯救自己
System.out.println("开始第二次拯救自己.....");
SAVE_HOOK = null;
System.gc();
Thread.sleep(1000);
if (SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("我死了!");
}

}
}
/*
开始第一次拯救自己.....
执行finalize()方法!
我还活着!
开始第二次拯救自己.....
我死了!
*/

垃圾收集算法

标记-清除算法

标记清楚算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
标记阶段首先根据根结点,标记所有从根结点可达的对象,未被标记的对象就是垃圾对象。在清除阶段,清除所有未被标记的对象。
imagepng
可以看到,该算法有两个不足:一是效率问题,标记和清除的效率都不高;二是空间问题,标记清楚之后产生大量不连续的碎片。以上图为例,如果需要一块大小为4的连续空间,整理后的空间就无法分配。如果无法分配,将提前触发下一次垃圾收集动作。

复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法,但是不适合像老年代这种存活对象较多的场合。存活对象较多,复制算法效率降低。复制算法将内存分为两个大小相同的部分,每次只用一块空间,垃圾收集时,将正在使用空间中的存活的对象复制到未使用的空间中,然后清除正在使用的空间,并交换两个内存的角色,完成垃圾回收。
imagepng

标记-整理/压缩算法

标记-整理算法适合存活对象较多的场合,比如老年代。标记-整理算法将所有对象做标记后,将存活的对象整理到内存的一段,然后清理边界外的所有空间。
imagepng

总结

虚拟机采用这种方法回收新生代,将新生代中的内存分为三部分,一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor空间。回收时将Eden和Survivor中存活的对象复制到另一块Survivor空间中,然后清理Eden和Survivor空间。Eden和两块Survivor空间的默认大小是8:1:1,每次都有10%的空间浪费。如果Survivor空间不足时将放到老年代中。
具体详见:深入理解Java虚拟机–内存分代策略及常用参数配置
下面的图实现了一次垃圾收集过程内存变化情况:新生代Eden中的大对象,如果Eden和Survivor中都无法存储,将直接存放到老年代中,Survivor中的老年对象进入老年代。最后Eden区和一块Survivor空间空闲。

imagepng

垃圾收集器

Serial收集器(串行收集器)

Serial收集器是最古老,最稳定的收集器,但是在执行垃圾回收时会暂停其他所有线程,直到它收集结束。
imagepng
虚拟机运行在Client模式下的默认新生代收集器,比其他收集器简单高效。

1
-XX:+UseSerialGC    //新生代老年代使用串行回收

该收集器在新生代和老年代使用串行回收;其中新生代使用复制算法,老年代使用标记-整理算法

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,新生代可以并行收集,老年代还是串行收集,所以只影响新生代。
imagepng
使用参数:

1
2
-XX:+UseParNewGC     //使用ParNew收集器
-XX:ParallelGCThreads //限制线程数量

Parallel收集器

Parallel 收集器和ParNew类似,新生代使用复制算法,老年代使用标记-整理算法。这个收集器更加关注吞吐量。
imagepng

1
2
3
4
5
-XX:+UseParallelGC      //使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC //使用Parallel收集器+ 并行老年代
//下面两个参数矛盾,因为停顿时间和吞吐量不能同时兼顾
-XX:MaxGCPauseMills //最大停顿时间,单位毫秒,GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio //0-100的取值范围,垃圾收集时间占总时间的比,默认99,即最大允许1%时间做GC

CMS收集器

CMS(Concurrent Mark Sweep )并发标记清除收集器是一种以获取最短回收停顿时间作为目标的收集器。CMS收集器使用的是标记-清除算法,并发阶段会降低吞吐量。CMS收集器是老年代收集器,新生代使用ParNew收集器。

1
-XX:+UseConcMarkSweepGC   //启用CMS收集器

CMS的标记过程分为四个阶段:

1
2
3
4
初始标记
并发标记
重新标记
并发清除

如下图所示,初始标记和重新标记阶段需要“Stop The World”,整个过程中最耗时的并发标记和并发清除是和用户线程一起执行,所以是一种并发低停顿收集器。
imagepng
在JDK1.5默认设置,CMS收集器在老年代使用68%空间就会被激活,因为和用户线程一起运行,不能在空间快满时再清理,如果内存预留不够,会引起concurrent mode failure

1
-XX:CMSInitiatingOccupancyFraction  //设置触发GC的阈值

虽然CMS收集器尽可能降低了停顿,但是会影响系统整体性能,在用户线程运行过程中,分一半CPU去执行GC,反应速度就下降一半。另外在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理,导致清理不彻底。
CMS使用的是标记-清理算法,因此会产生大量碎片,可以通过以下参数来整理碎片:

1
2
3
4
-XX:+ UseCMSCompactAtFullCollection //Full GC后,进行一次整理,整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction //设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads //设定CMS的线程数量

G1收集器

G1收集器是JDK1.7中引入,在Java9中为JVM默认垃圾收集器。G1垃圾收集器主要应用在多CPU和大内存的服务器环境中,可以极大减少垃圾收集器的停顿时间,提升服务器性能,可以解决CMS中Concurrent Mode Failed问题,并逐步替换掉CMS.
G1垃圾收集器不再将内存分为新生代和老年代,直接将堆空间划分为大小相同的子空间(Region),JVM可以设置这些Region空间的大小(每个子空间大小范围为1MB~32MB[必须是2的幂],最多可以设置2048个区域,最大支持32MB*2048=64G内存),每个Region标记为E、S、O和H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。
imagepng
与CMS的标记-整理算法不同,G1从整体来看是基于标记-整理算法实现,从两个Region之间来看是基于复制算法实现。

G1跟踪每个Region里面垃圾堆积的价值大小(回收所获空间大小以及回收所需时间的经验值),后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证G1收集器在有限的时间内获得更高的效率。

GC参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:NewRatio:新生代和老年代的比
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

参考资料

1.周志明. 深入理解Java虚拟机[M]. 机械工业出版社, 2013.
2G1从入门到放弃(一)

介绍

Java为每种基本数据类型提供了对应的包装类型,比如int对应的包装类型是Integer,创建一个Integer对象需要使用new关键字,在JDK5之后提供了自动装箱拆箱,可以自动将基本数据类型直接转换为包装类型,也可以直接将包装类型自动转成基本数据类型,这就叫做自动装箱拆箱。

1
2
Integer i = 100;    //自动装箱
int j = i; //自动拆箱

Integer中的实现

Integer自动装箱拆箱是基于valueOf()和intValue方法实现。当我们执行

1
2
3
4
5
Integer i = 100; 
```java
其实就是执行了:
```java
Integer i = Integer.valueOf(100);

当执行:

1
int j = i;  

就是执行了:

1
int j = i.intValue();

valueOf()

这是valueOf的实现代码:

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

当获取到值i后,首先判断i是否存在IntegerCache中,这里IntegerCache是Integer的缓存,如果i在缓存中,直接返回缓存中的数据。注意只有在自动装箱中才用到缓存,使用new Integer中不会用到。看一下IntegerCache的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class IntegerCache {
static final int low = -128; //默认缓存最小值是-128
static final int high; //最大值不是固定的,可以修改
static final Integer cache[]; //存放缓存的数组

static {
// high value may be configured by property
int h = 127; //默认最大值是127
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); //使用JVM参数设置最大值
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127); //如果设置的最大值小于默认最大值127,就使用127
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);//最大值的范围
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

总结一下,IntegerCache缓存的最小值是-128,最大值默认是127,可以通过 JVM 的启动参数 -XX:AutoBoxCacheMax=size 修改,最大值的范围是127到Integer.MAX_VALUE - (-low) -1。cache数组中下标0存的是-128,下标1存-127,依次类推,因此i的缓存是i + (-IntegerCache.low)
举个例子:

1
2
3
4
5
6
7
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a==b); //true
System.out.println(c==d);//false
System.out.println(c.equals(d));//true

上面的代码中,a和b使用的是缓存,所以a和b的地址都相同,而c和d是新建的对象,所以地址不相同。

1
2
3
4
5
6
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

Integer中重写的equals方法是直接对比value的值,不是对比的对象地址,所以c.equals(d)为true。

intValue()

intValue中直接返回value的值。

1
2
3
public int intValue() {
return value;
}

其他包装类

Java中提供了八种包装类Integer(int)、Long(long)、Double(double)、Short(short)、Float(float)、Byte(byte)、Character(char)、Boolean(boolean)
八种包装类中,xxxValue基本一样,都是返回value值,我们重点看一下valueOf方法:

Long

1
2
3
4
5
6
7
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}

Long并没有提供可以更改的缓存最大值,直接默认为127,并且不能更改。

Double

1
2
3
public static Double valueOf(double d) {
return new Double(d);
}

Double的valueOf()方法没有使用缓存,道理很简单,以为小数位没法缓存,同理,Float一样。

Short

1
2
3
4
5
6
7
8
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}

Short包装类也是直接缓存-128到127。

Byte

1
2
3
4
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}

byte的取值范围是-128~127,因此byte不用判断上下界

Character

1
2
3
4
5
6
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}

char的最小值是0,所以没有判断最小值,缓存的最大值设置为127.

Boolean

1
2
3
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

注意这里如果用“==”对比Boolean的包装类,都会返回true,例如:

1
2
3
4
5
6
  Boolean a = true;
Boolean b = true;
Boolean c = false;
Boolean d = false;
System.out.println(a==b); //true
System.out.println(c==d); //true

最后

1
2
3
4
5
6
7
8
9
10
Integer a = 1;
Integer b = 2;
Integer c = 3;
Long d = 2L;
Long e = 3L;
System.out.println(c==(a+b)); //true
System.out.println(c.equals(a+b)); //true
System.out.println(e==(a+b)); //true
System.out.println(e.equals(a+b)); //false
System.out.println(e.equals(a+d)); //true
  • c==(a+b) 中,a+b会触发自动拆箱和装箱,再和c比较,所以是true
  • c.equals(a+b),a+b触发自动拆箱和装箱,对比结果也是true
  • e==(a+b),a+b自动拆箱装箱变为3,e也自动拆箱变成3,所以返回true
  • e.equals(a+b),a+b自动拆箱装箱变成Integer类型的3,而e是Long类型,equals方法会先判断obj instanceof Long,如果传入的不是Long类型,会返回false
  • e.equals(a+d) ,a和d自动拆箱后,因为d是Long类型,会自动转换成Long类型,再触发装箱变成Long类型,对比后为true。

介绍

String类是final修饰,说明不可以被继承,String一旦被创建就不能改变,对String修改就会创建一个新的字符串,因此禁止使用循环修改字符串。

Sring在内存中的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
String str1 = new String("hello");
String str2 = new String("hello");
String str3 = "hello";
String str4 = "hello";
String str5 = "he"+"llo";
String str6 = "he";
String str7 = "llo";
System.out.println(str1==str2); //false
System.out.println(str1==str3); //false
System.out.println(str3==str4); //true
System.out.println(str4 == str5); //true
System.out.println(str3=="hello"); //true
System.out.println(str4==(str6+str7)); //false

使用new创建一个String对象,会在堆内分配内存,每个对象地址都不一样,而str3会先在字符串常量池查找,没有才会添加进去,如果存在直接引用地址。最后str6+str7是在JVM运行时才操作,会创建一个新的对象。

源码

继承

1
2
3
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {}

String实现了Serializable、Comparable和CharSequence接口

成员变量

1
2
3
4
5
6
//存放final类型的char数组,不可修改
private final char value[];
//hash值
private int hash;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//创建一个空的字符串
public String() {
this.value = "".value;
}

//新创建的String是参数String的副本
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//传入char数组,直接拷贝数据到value
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}

//offset是第一个字符的索引,count是字节数,表示char数组从offset开始,获取长度为count的子数组
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
//长度为0
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// 数组越界
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

//Unicode 代码点数组参数一个子数组的字符
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}

final int end = offset + count;

// Pass 1: Compute precise size of char[]
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
continue;
else if (Character.isValidCodePoint(c))
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}

// Pass 2: Allocate and fill in char[]
final char[] v = new char[n];

for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);
}

this.value = v;
}

//不推荐使用
@Deprecated
public String(byte ascii[], int hibyte, int offset, int count) {
checkBounds(ascii, offset, count);
char value[] = new char[count];

if (hibyte == 0) {
for (int i = count; i-- > 0;) {
value[i] = (char)(ascii[i + offset] & 0xff);
}
} else {
hibyte <<= 8;
for (int i = count; i-- > 0;) {
value[i] = (char)(hibyte | (ascii[i + offset] & 0xff));
}
}
this.value = value;
}

//不推荐使用
@Deprecated
public String(byte ascii[], int hibyte) {
this(ascii, hibyte, 0, ascii.length);
}
//
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}

//传入字节数组,使用指定字符集
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}


public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}


public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}


public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}


public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}


public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}


public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}


String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}

其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
 //获取长度
public int length() {
return value.length;
}

//判断是否为空
public boolean isEmpty() {
return value.length == 0;
}

//获取char指定索引的值
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
//获取指定索引处的字符(Unicode代码点)
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
//获取指定索引之前的字符
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}

//获取String文本范围内Unicode代码点数
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
}

//返回此 String 中从给定的 index 处偏移 codePointOffset 个代码点的索引。
public int offsetByCodePoints(int index, int codePointOffset) {
if (index < 0 || index > value.length) {
throw new IndexOutOfBoundsException();
}
return Character.offsetByCodePointsImpl(value, 0, value.length,
index, codePointOffset);
}

//将字符串中的字符复制到目标数组中,dstBegin是目标数组中的起始偏移量
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}

//同上,要复制字符串中,从srcBegin开始,到srcEnd-1结束
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
//获取命名的字符集字节数组
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}

//获取指定字符集的字节数组
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}

//获取默认字符集的字节数组
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}

//重写equals方法
public boolean equals(Object anObject) {
//如果对象地址相同,就是相同的对象
if (this == anObject) {
return true;
}
//首先判断是否是String对象
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//判断长度是否相同,不相同不是相同的字符串
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//判断每一个字符是否相同
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

//equals对比的是String对象,contentEquals对比CharSequence或其子类的对象
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}

public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
//StringBuffer 是线程安全的,用synchronized块
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// 如果是String,就用equals方法
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}

private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
char v1[] = value;
char v2[] = sb.getValue();
int n = v1.length;
//判断长度是否相同
if (n != sb.length()) {
return false;
}
//对比每一个字符
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
return true;
}

//对比String,忽略大小写
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}

//如果字符串等于参数字符串anotherString,返回0;如果字符串小于参数字符串anotherString,返回小于0的值;如果字符串大于参数字符串anotherString,返回大于0的值
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);//取长度最小的
char v1[] = value;
char v2[] = anotherString.value;

int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
//如果不相同,就比较不相同的字符
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//上面遍历结束还未返回,如果两个长度相同那说明两个字符串完全相同,返回0
return len1 - len2;
}

//忽略大小写的对比
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;

public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}


private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
//忽略大小写
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}


public int indexOf(int ch) {
return indexOf(ch, 0);
}
//返回指定字符第一次出现的字符串内的索引,从fromIndex开始查找,ch是字符,Unicode代码点
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
//如果偏移量小于0,则从0开始查
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// 如果偏移量大于字符数量,那就返回-1,表示没找到
return -1;
}

if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
//循环查找,只返回第一次出现的位置
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}

//增补字符验证
private int indexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}

//返回指定字符最后一次出现的位置
public int lastIndexOf(int ch) {
return lastIndexOf(ch, value.length - 1);
}


public int lastIndexOf(int ch, int fromIndex) {
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
int i = Math.min(fromIndex, value.length - 1);
for (; i >= 0; i--) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return lastIndexOfSupplementary(ch, fromIndex);
}
}


private int lastIndexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
char hi = Character.highSurrogate(ch);
char lo = Character.lowSurrogate(ch);
int i = Math.min(fromIndex, value.length - 2);
for (; i >= 0; i--) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}


public int indexOf(String str) {
return indexOf(str, 0);
}


public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
//去除字符串两端空格
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */

while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//判断前后空格,然后取子集
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

内存分代策略

简介

HotSpot中为了提高内存对象内存分配和垃圾回收的效率,将内存分为了新生代(Eden+From Survivor+To Survivor)、老年代(OldGen)和永久代(PermGen)。新创建的对象分配在新生代,多次回收还存活的对象放在老年代,类信息、静态变量和字符串常量等存放在永久代中。新生代中需要频繁执行垃圾回收,老年代中不需要频繁垃圾回收,永久代一般来说不实现垃圾回收。这样对不同的区域实行不同的垃圾回收算法,可以提高垃圾回收效率。
在Java7之前,方法区在永久代,永久代和堆隔离,使用的是堆空间,永久代大小在启动JVM时设置一个固定的值,不可变;Java7中将static从永久代移到堆中;Java8中取消永久代,使用元空间(Metaspace)替代,与堆共享物理内存,逻辑上可以认为和堆在一起。元空间使用的是系统内存,因此有足够空间。
图片来源网络

新生代

新生代垃圾回收效率高,Minor GC是新生代的GC,回收速度快,Eden空间不足时会触发Minor GC进行回收操作。新生代分为三块空间,一个Eden和两个Survivor(通常称为To Survivor和From Survivor),当Eden被填满,会执行Minor GC,将存活下来的对象转移到To Survivor,From Survivor中存活的对象,判断年龄阈值(默认15,一轮GC表示增加一岁),如果超过阈值就放到老年代中,没超过的存到To Survivor中,然后将To Survivor 和From Survivor互换,也就是To Survivor在GC之后永远是空的。

老年代

老年代包含长期存活的对象和多次Minor GC后存活下来的对象,通常老年代被占满或者显式调用System.gc()方法才开始垃圾回收,老年代垃圾回收也就叫Full GC/Major GC,

JVM常用参数配置

了解了内存分代策略,下面可以学习一下常用的参数配置,并查看不同内存区域的变化情况。

怎样设置JVM参数

方法很多,可以上网查询,我这里说我使用的Eclipse中设置方式。
在运行Java的按钮下面,Run As下面,有一个Run Configurations->Arguments->VMarguments
QQ20190130141444jpg

Trace跟踪参数

Trace跟踪参数就是跟踪GC运行的参数。

打印GC简要信息

1
2
-verbose:gc  
-XX:+PrintGC

-verbose:gc 表示输出虚拟机中GC的情况
-XX:+PrintGC 功能和-verbose:gc一样

1
2
3
4
5
6
7
   Object obj = new byte[1*1024*1024];
obj = null;
System.gc();
/* 输出:
[GC (System.gc()) 3020K->704K(125952K), 0.0008022 secs]
[Full GC (System.gc()) 704K->514K(125952K), 0.0049984 secs]
*/

上面的代码,将obj设置为null,让GC回收创建的BYTE数组,是属于新生代的Minor GC。调用System.gc(),是Full GC

打印GC详细信息

1
2
-XX:+PrintGCDetails     //打印GC详细信息
-XX:+PrintGCTimeStamps //打印GC发生的时间戳
1
2
3
4
5
6
7
8
9
10
-XX:+PrintGCDetails的输出:
Heap
PSYoungGen total 38400K, used 3686K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
eden space 33280K, 11% used [0x00000000d5f00000,0x00000000d6299b30,0x00000000d7f80000)
from space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
to space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
ParOldGen total 87552K, used 0K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
object space 87552K, 0% used [0x0000000081c00000,0x0000000081c00000,0x0000000087180000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

下面我简单介绍一下里面的参数:

1
PSYoungGen      total 38400K, used 3686K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)

第一行的PSYoungGen表示新生代;total 38400K表示总大小为38400K;used 3686K表示已经使用了3686K;[0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)分别表示当前区域在内存中的位置,分别是低边界,当前边界和高边界,(0x00000000d8980000-0x00000000d5f00000)/1024/1024 = 42M,
33280K+5120K+5120K=42M(eden space+from space + to space)

文件输出GC log

有时候在运行环境下,需要查看错误信息,就需要将log文件保存在本地,应该使用下面的命令:

1
-Xloggc:F:/gc.log

这样在F盘就出现gc.log文件,打开信息为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java HotSpot(TM) 64-Bit Server VM (25.192-b12) for windows-amd64 JRE (1.8.0_192-b12), built on Oct  6 2018 17:12:23 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8272984k(2132632k free), swap 16544068k(9949684k free)
CommandLine flags: -XX:InitialHeapSize=132367744 -XX:MaxHeapSize=2117883904 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
0.100: [GC (System.gc()) [PSYoungGen: 3020K->664K(38400K)] 3020K->672K(125952K), 0.0009640 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.101: [Full GC (System.gc()) [PSYoungGen: 664K->0K(38400K)] [ParOldGen: 8K->514K(87552K)] 672K->514K(125952K), [Metaspace: 2627K->2627K(1056768K)], 0.0054643 secs] [Times: user=0.08 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 333K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
eden space 33280K, 1% used [0x00000000d5f00000,0x00000000d5f534a8,0x00000000d7f80000)
from space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
to space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
ParOldGen total 87552K, used 514K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
object space 87552K, 0% used [0x0000000081c00000,0x0000000081c80ba0,0x0000000087180000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

监控加载了哪些类

1
-XX:+TraceClassLoading

使用这个参数能获取到Java加载到的类。

1
2
3
4
5
6
7
8
9
10
[Opened D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Object from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.io.Serializable from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Comparable from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.String from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
.......

打印类的信息

1
-XX:+PrintClassHistogram

按下Ctrl+Break后,打印类的信息(四个参数分别是:序号、实例数量、总大小、类型
):

1
2
3
4
5
6
7
8
 num     #instances         #bytes  class name
----------------------------------------------
1: 890617 470266000 [B
2: 890643 21375432 java.util.HashMap$Node
3: 890608 14249728 java.lang.Long
4: 13 8389712 [Ljava.util.HashMap$Node;
5: 2062 371680 [C
6: 463 41904 java.lang.Class

堆的分配参数

指定最大堆和最小堆

指定参数:

1
2
3
4
5
-Xmx
-Xms
//下面表示最大堆为20m,最小堆是5m
-Xmx20m
-Xms5m

获取JVM最大堆数据:

1
2
3
4
5
6
7
8
9
   //获取最大堆大小
System.out.print("Xmx=");
System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M");
//获取空闲内存大小
System.out.print("free mem=");
System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M");
//获取总计内存大小
System.out.print("total mem=");
System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M");

输出结果:

1
2
3
Xmx=1796.0M  
free mem=121.04998016357422M
total mem=123.0M

也就是说,我的JVM设置最大堆是1796.0M ,总计123.0M ,还有121.04998016357422M 可用。那么设置一下-Xmx20m -Xms5m参数,再看一下结果:

1
2
3
Xmx=18.0M
free mem=4.761444091796875M
total mem=5.5M

如果分配1M的数组,再次执行:

1
byte[] b = new byte[1*1024*1024];

分配1M数组后,空闲空间变少:

1
2
3
Xmx=18.0M
free mem=3.76141357421875M
total mem=5.5M

如果空闲空间全部使用完会怎么样呢?那我们创建一个5M的数组,看一下结果:

1
byte[] b = new byte[5*1024*1024];

输出的结果变成了:

1
2
3
Xmx=18.0M
free mem=5.26141357421875M
total mem=11.0M

这里的最大堆没变,还是18M,总的内存大小从5.5M变成了11M,也就是说如果空闲空间不能支撑对象所需容量,那么就会扩容。

堆的其他参数

设置新生代大小(绝对参数,设置多少就是多少):

1
-Xmn

新生代(eden+2*s)和老年代(不包含永久区)的比值:
4表示 新生代:老年代=1:4,即年轻代占堆的1/5

1
-XX:NewRatio

设置两个Survivor区和eden的比:
8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10

1
-XX:SurvivorRatio

我们测试一下堆内空间的变化,创建一个10M的数组,数组是循环创建:

1
2
3
byte[] b=null;
for(int i=0;i<10;i++)
b=new byte[1*1024*1024];

先看一下新生代分配1M空间:

1
2
3
4
5
6
7
8
9
10
11
12
配置参数:-Xmx20m -Xms20m -Xmn1m  -XX:+PrintGCDetails 
[GC (Allocation Failure) [PSYoungGen: 507K->504K(1024K)] 507K->504K(19968K), 0.0010025 secs] [Times: user=0.05 sys=0.02, real=0.00 secs]
Heap
PSYoungGen total 1024K, used 721K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
eden space 512K, 42% used [0x00000000ffe80000,0x00000000ffeb6790,0x00000000fff00000)
from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 18944K, used 10240K [0x00000000fec00000, 0x00000000ffe80000, 0x00000000ffe80000)
object space 18944K, 54% used [0x00000000fec00000,0x00000000ff6000a0,0x00000000ffe80000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: NewSize (1536k) is greater than the MaxNewSize (1024k). A new max generation size of 1536k will be used.

我用的JDK1.8,这里还报了Java HotSpot(TM) 64-Bit Server VM warning,主要是因为新生代的空间不足,不能分配。我们可以看到,新生代空间不足1M,因此新生代无法分配,就在老年代里面分配了10M空间。新生代发生了一次GC,回收了3K空间,可以忽略不计。那么继续增加新生代空间到15M:

1
2
3
4
5
6
7
8
9
10
11
执行参数:-Xmx20m -Xms20m -Xmn15m  -XX:+PrintGCDetails

Heap
PSYoungGen total 13824K, used 11525K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
eden space 12288K, 93% used [0x00000000ff100000,0x00000000ffc41760,0x00000000ffd00000)
from space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000)
to space 1536K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000ffe80000)
ParOldGen total 5120K, used 0K [0x00000000fec00000, 0x00000000ff100000, 0x00000000ff100000)
object space 5120K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff100000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

这里没有发生GC,新生代使用了11525K空间,说明新生代空间足够分配,因此没有触发GC。那么将新生代空间调整到7M会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
执行参数:-Xmx20m -Xms20m -Xmn7m  -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 6036K->480K(6656K)] 6036K->1648K(19968K), 0.0012152 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 6656K, used 5782K [0x00000000ff900000, 0x0000000100000000, 0x0000000100000000)
eden space 6144K, 86% used [0x00000000ff900000,0x00000000ffe2d900,0x00000000fff00000)
from space 512K, 93% used [0x00000000fff00000,0x00000000fff78020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 13312K, used 1168K [0x00000000fec00000, 0x00000000ff900000, 0x00000000ff900000)
object space 13312K, 8% used [0x00000000fec00000,0x00000000fed24020,0x00000000ff900000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

这里发生了一次新生代GC,回收了6036K-1648k= 4388k空间,剩下的5782k在新生代。继续调整,这次调整一下新生代中eden和Survivor的比例,设置-XX:SurvivorRatio=2,增大了Survivor的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
执行参数:-Xmx20m -Xms20m -Xmn7m   -XX:SurvivorRatio=2 -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 3922K->1504K(5632K)] 3922K->1624K(18944K), 0.0025991 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 4656K->1504K(5632K)] 4776K->1624K(18944K), 0.0114028 secs] [Times: user=0.08 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 4646K->1504K(5632K)] 4766K->1632K(18944K), 0.0024505 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 5632K, used 2610K [0x00000000ff900000, 0x0000000100000000, 0x0000000100000000)
eden space 4096K, 27% used [0x00000000ff900000,0x00000000ffa14930,0x00000000ffd00000)
from space 1536K, 97% used [0x00000000ffd00000,0x00000000ffe78030,0x00000000ffe80000)
to space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000)
ParOldGen total 13312K, used 128K [0x00000000fec00000, 0x00000000ff900000, 0x00000000ff900000)
object space 13312K, 0% used [0x00000000fec00000,0x00000000fec20010,0x00000000ff900000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

发生了3次新生代的GC,都是新生代的Minor GC,第一次GC回收了3922K-1624K=2298k,第二次GC回收了4776K-1624K=3152K,第三次GC回收了4766K-1632K=3134k,剩下的2610k在新生代中,还有一部分去了From Survivor中。再看一下,-XX:NewRatio=1的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
执行参数:-Xmx20m -Xms20m -XX:NewRatio=1    -XX:SurvivorRatio=2 -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 4932K->1688K(7680K)] 4932K->1696K(17920K), 0.0011783 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 5884K->1592K(7680K)] 5892K->1600K(17920K), 0.0009533 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 7680K, used 3834K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 5120K, 43% used [0x00000000ff600000,0x00000000ff830ad8,0x00000000ffb00000)
from space 2560K, 62% used [0x00000000ffd80000,0x00000000fff0e040,0x0000000100000000)
to space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
ParOldGen total 10240K, used 8K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec02000,0x00000000ff600000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

可以看到,提高新生代空间之后,发生了两次GC,因为新生代能分配更多的空间,避免GC,也没有是用老年代的空间。那么再增加eden空间会怎么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
执行参数:-Xmx20m -Xms20m -XX:NewRatio=1   -XX:SurvivorRatio=3 -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 6036K->1720K(8192K)] 6036K->1728K(18432K), 0.0059791 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 8192K, used 7022K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 6144K, 86% used [0x00000000ff600000,0x00000000ffb2d900,0x00000000ffc00000)
from space 2048K, 83% used [0x00000000ffc00000,0x00000000ffdae040,0x00000000ffe00000)
to space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
ParOldGen total 10240K, used 8K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec02000,0x00000000ff600000)
Metaspace used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K

提高Eden空间之后,只发生了一次GC,GC越少,说明系统执行效率越高。

OOM内存溢出

1
2
3
4
5
6
7
8
9
10
11
-XX:+HeapDumpOnOutOfMemoryError
OOM时导出堆到文件
-XX:+HeapDumpPath
导出OOM的路径
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
当发生OOM时,导出到文件a.dump
-XX:OnOutOfMemoryError
在OOM时,执行一个脚本
"-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p"
当程序OOM时,在D:/a.txt中将会生成线程的dump
可以在OOM时,发送邮件,甚至是重启程序

默认配置

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
Edem : from : to = 8 : 1 : 1

永久区分配参数

1
2
-XX:PermSize  -XX:MaxPermSize
设置永久区的初始空间和最大空间

使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM。

栈空间分配

1
-Xss

通常只有几百K,决定了函数调用的深度;
每个线程都有独立的栈空间,局部变量、参数 分配在栈上.
因为每个线程都需要分配一个栈空间,因此如果想多跑一些线程,就需要将这个值调小,容纳更多线程。

参考资料

1.JAVA 方法区是在堆里面吗
2.Java虚拟机:JVM内存分代策略

注解

什么是注解

注解也叫Annotation,从JDK5.0开始引入。

  • Annotation不是程序,但是可以对程序作出解释
  • 可以被其他程序读取

内置注解

@Override
定义在java.lang.Override中,此注释只适用于修辞方法,表示一个方法声明打算重写超类中的另一个方法声明。

1
2
3
4
   @Override
public String toString (){
return "";
}

在阿里巴巴Java开发手册中强制要求所有重写方法必须加@Override,防止重写方法名称写错。
@Deprecated
定义在java.lang.Deprecated中,此注释可用于修辞方法、属性、类,表示不建议使用这样的元素,通常被遗弃。
@SuppressWarnings
定义在java.lang.SuppressWarnings中,用来抑制编译时的警告信息。

自定义注解

先了解一下元注解:
元注解的作用就是负责注解其他注解。 Java定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。这些类型和它们所支持的类在java.lang.annotation包中可以找到

  • @Target 用于描述注解的使用范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
//其中ElementType是枚举

package java.lang.annotation;
public enum ElementType {
TYPE, //修饰:类、接口、枚举、Annotation类型
FIELD, //用于描述域
METHOD, //用于描述方法
PARAMETER, //用于描述参数
CONSTRUCTOR, //用于描述构造器
LOCAL_VARIABLE, //用于描述局部参数
ANNOTATION_TYPE, //注解A对注解B进行声明,@Target就是用的这个
PACKAGE, //package包
//java8新增两个注解
TYPE_PARAMETER,
TYPE_USE
}

```java
- @Retention 表示需要在什么级别保存该注释信息,用于描述注解的生命周期

```java
package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {

RetentionPolicy value();
}

//RetentionPolicy也是枚举
package java.lang.annotation;


public enum RetentionPolicy {
SOURCE, //在源文件中有效(用于编译器和加载器)
CLASS, //在class文件中有效(用于编译器和加载器)

RUNTIME //在运行时有效。为Runtime可以被反射机制读取
}

下面的两个不常用:

  • @Documented 给类方法添加注释
  • @Inherited 修饰某个被标注的类型是被继承的

1、首先创建一个Annotation,使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package fun.gwt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;



@Target(value={ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "";
int id() default 0;
}

  • 注解元素必须要有值。我们定义注解元素时,经常使用空字符串、0作为默认值。也经常使用负数(比如:-1)表示不存在的含义
  • 如果只有一个参数成员,一般参数名为value

注解实战ORM

对象关系映射(英语:(Object Relational Mapping,简称ORM,或O/RM,或O/Rmapping),是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。这里使用注解和反射技术,来实现ORM,将对象转换成SQL语句
1、首先创建一个Student对象,如下代码所示,我们需要创建两个注解,第一个注解将Student类转换成sql的表名,第二个注解将属性赋值上长度,类型和名称等属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package fun.gwt.annotation;

class Student {
int id;
int age;
String sName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getsName() {
return sName;
}
public void setsName(String sName) {
this.sName = sName;
}
}

2、为对象名注解表名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package fun.gwt.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.TYPE})
public @interface Table {
//如果只有一个参数成员,一般参数名为value
String value();
}

3、给属性赋值上长度,类型和名称等属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package fun.gwt.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.FIELD})
public @interface Field {
String columnName();
String type();
int length();

}

4、接下来需要重新修改Student类,将类名和属性名加上注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package fun.gwt.annotation;

@Table("db_student")
class Student {
@Field(columnName="id",type="int",length=10)
int id;
@Field(columnName="id",type="int",length=3)
int age;
@Field(columnName="sname",type="varchar",length=10)
String sName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getsName() {
return sName;
}
public void setsName(String sName) {
this.sName = sName;
}
}

5、使用反射来获取信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package fun.gwt.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException {
Class cla = Class.forName("fun.gwt.annotation.Student");
//获取类的所有有效注解
Annotation[] annotations = cla.getAnnotations();
for (Annotation a : annotations) {
System.out.println(a);//@fun.gwt.annotation.Table(value=db_student)
}
//获取类的指定注解
Table t = (Table)cla.getAnnotation(Table.class);
System.out.println(t.value()); //db_student
//获取类的属性注解
Field f = cla.getDeclaredField("sName");
GwtField gwtField = f.getAnnotation(GwtField.class);
System.out.println(gwtField.columnName()+"--"+gwtField.type()+"--"+gwtField.length());//sname--varchar--10
//接下来就是拼接sql语句
}
}

简介

Object类在java.lang包中,是所有类的父类,所有类都间接或者直接继承Object类。Object类中主要有registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait()和finalize()等方法。其中
getClass()、notify()、notifyAll()和wait()等方法使用final关键字修饰,因此子类不能覆盖。

源代码

registerNatives()方法

1
2
3
4
5
private static native void registerNatives();
//在类加载时就执行这个方法
static {
registerNatives();
}

registerNatives()方法是注册一些本地方法,具体实现在本地的代码中,所以我也不清楚里面干了什么。学Java还是有必要学习一下JNI的,等我找到工作要好好研究一下。
在Java中,静态代码的调用顺序:父类静态–>子类静态–>父类非静态–>父类构造–>子类非静态–>子类构造

getClass()方法

1
public final native Class<?> getClass();

返回运行时的类

hashCode()方法

1
public native int hashCode();

返回对象的哈希码,在HashMap中,就是通过hashCode来提高查找哈希表的速度。

equals()方法

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

这里判断的是对象,对象的地址相同,返回true,如果两个对象完全一样,但是地址不相同,也会返回false。

在Java中,8种基本数据类型变量存储的是值,可以直接使用==来比较,而引用类型数据变量需要使用equals(),也可以重写该方法。

  • 数值型- byte(1字节)、 short(2字节)、int(4字节)、 long(8字节)、float(4字节)、 double(8字节)
  • 字符型- char (2字节)
  • 布尔型-boolean (1位)
  • 引用数据类型 —— class,interface,数组 (4个字节)

clone()方法

实现拷贝必须实现Cloneable接口,并重写clone()方法。

1
protected native Object clone() throws CloneNotSupportedException;

clone分为浅拷贝和深拷贝:

1
2
浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。
深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。

浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Professor {
private String name;
private int age;
//省略get和set方法
}

public class Student implements Cloneable {
private String name;
private int age;
private Professor professor;
//省略get和set方法
public Professor getProfessor() {
return professor;
}
public void setProfessor(Professor professor) {
this.professor = professor;
}
//对对象Studentclone
@Override
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
1
2
3
4
5
6
7
Professor p1 = new Professor();
//省略赋值
Student s1 = new Student();
//省略赋值
Student s2 = (Student) s1.clone();
Professor p2 = s2.getProfessor();
//省略修改

在这里s2浅拷贝s1,如果现在对p2进行修改,那么s1中的Professor也会变化。因为p2只是引用了p1的地址,对p2的任何操作和操作p1是一样的。

深拷贝

代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Professor {
private String name;
private int age;
//省略get和set方法
@Override
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}

public class Student implements Cloneable {
private String name;
private int age;
private Professor professor;
//省略get和set方法
public Professor getProfessor() {
return professor;
}
public void setProfessor(Professor professor) {
this.professor = professor;
}
//对对象Studentclone
@Override
public Object clone() throws CloneNotSupportedException{
Student newStudent = (Student) super.clone();
newStudent.professor = (Professor) professor.clone();
return newStudent;
}
}

这样就实现了深拷贝,对p2的任何操作都不会影响p1。

toString()方法

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

toString()方法在子类中都会被重写,这里显示的是类名+”@”+对象哈希码的十六进制数

notify()和notifyAll()方法

1
2
public final native void notify();
public final native void notifyAll();

notify()方法会唤醒一个处于等待状态的线程,notifyAll()方法会唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程先调度。

wait()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//timeout是等待时间,毫秒。
public final native void wait(long timeout) throws InterruptedException;
//nanos是纳秒。多加一个nanos参数是为了比wait(long timeout)更好的控制时间,其中1毫秒(ms)=1000000纳秒(ns)
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}

if (nanos > 0) {
timeout++;
}

wait(timeout);
}
//不唤醒就一直等待
public final void wait() throws InterruptedException {
wait(0);
}

finalize()方法

1
protected void finalize() throws Throwable { }

当垃圾回收器将要回收对象所占内存之前被调用。

参考资料

Java的clone():深复制与浅复制

介绍

HashMap是基于哈希表的Map接口的实现,并允许使用 null 值和 null 键。HashMap在JDK1.8之前使用的是哈希表+链表的方式存储数据。在JDK1.8之后,如果链表过长则将链表转成红黑树。 

源码分析

继承

1
2
3
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}

HashMap继承了AbstractMap及实现了Map、Cloneable和Serializable接口。

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static final long serialVersionUID = 362498820763181265L;
// aka 16 默认的初始容量 1*2*2*2*2 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量是2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//填充因子是0.75.如果哈希表中的元素超过了加载因子与当前容量的乘积,就调用rehash方法
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//阈值,当桶上的链表数大于这个值会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//当桶中的立案表述小于这个值则红黑树转成链表
static final int UNTREEIFY_THRESHOLD = 6;
//转成红黑树之前,判断键值对数量大于64才会转换。
static final int MIN_TREEIFY_CAPACITY = 64;
//哈希表数组,长度一直为2的幂次
transient Node<K,V>[] table;
//键值对集合
transient Set<Map.Entry<K,V>> entrySet;
//键值对的数量
transient int size;
//统计操作次数,迭代的时候判断这个值是否变化,fail-fast抛出ConcurrentModificationException
transient int modCount;
//阈值,键值对数量大于这个值将开始扩容。threshold = table.length * loadFactor
int threshold;
//这个才是填充因子,上面DEFAULT_LOAD_FACTOR是默认的
final float loadFactor;

Node 结点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static class Node<K,V> implements Map.Entry<K,V> {
//存的结点的hash值
final int hash;
//K和V的值,这里V不用final修饰。K用final修饰,说明键只能赋值一次,不能改变,但是值可以改变
final K key;
V value;
//指向下一个结点
Node<K,V> next;
//创建一个结点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
//instanceof 判断对象o是否是类Map.Entry的一个实例
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//传入初始容量和填充因子
public HashMap(int initialCapacity, float loadFactor) {
//判断初始容量是否合法
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity)
//如果传入的初始容量大于最大容量,将用最大容量作为初始容量
if (initialCapacity > MAXIMUM_CAPACITY)
//初始容量在这里有啥用???
initialCapacity = MAXIMUM_CAPACITY;
//判断填充因子是否合法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//计算出来threshold阈值
this.threshold = tableSizeFor(initialCapacity);
}
/*将初始化容量转化为大于等于最接近cap的2的整数次幂
|是或运算,>>>是无符号右移,空位0补齐
以n = 011011,
011011 >>> 1 = 001101
011011 | 001101 = 011111
.....
然后继续下去,最后得到最高位和后面的都是1,就能保证结果大于等于n,并且n为奇数,最后再加1.
因为int为32位,所以最后肯定能让所有位都为1
*/
static final int tableSizeFor(int cap) {
//cap减一,是防止传进来的是2的整数次幂,减一后保证最后结果是cap本身
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

1
2
3
4
5
//如果没有传入填充因子,则使用默认的填充因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

1
2
3
4
5
//只默认了填充因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//传入一个Map初始化
public HashMap(Map<? extends K, ? extends V> m) {
//使用默认填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//s是长度
int s = m.size();
if (s > 0) {
//如果哈希表没有初始化
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//计算出来的t大于阈值,则用t初始化阈值
if (t > threshold)
threshold = tableSizeFor(t);
} //m的个数大于阈值,则进行扩容
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

其他方法

(table.length - 1) & hash

HashMap根据key的hashCode计算hash值,知道hash值之后怎么确定key在数组中的位置呢,这里就用到了(table.length - 1) & hash;
首先使用(table.length - 1) 和hash进行与操作,不用担心数组越界。那为什么要数组长度减一呢?假设数组长度是16,假设有两个hashcode是8和9:
8的二进制:1000
9的二进制:1001
16的二进制是:10000
8 & 16 = 10000
9 & 16 = 10000
这样出现了两个不同的hashcode在一个数组中,增加了查找的次数
如果table.length - 1,也就是16-1:
15的二进制:1111
8 & 15 = 1000
9 & 15 = 1001

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//传入Key,返回Value
public V get(Object key) {
Node<K,V> e;
//这里也能看到hashmap可以保存null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//传入Key的hash值和key的值
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否是null,数组长度是否大于0,取出来的结点是否为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//先判断结点的hash值是否相同,再判断key是否相同,都相同就返回这个结点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果数组中还有其他的结点,就继续查找
if ((e = first.next) != null) {
//判断first是不是红黑树
if (first instanceof TreeNode)
//调用TreeNode中的getTreeNode方法,我还没看TreeNode类,一会写
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//不是红黑树,是链表,开始遍历链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

红黑树

红黑树不是严格的平衡二叉树,红黑树比AVL树不平衡最多一层,查询上比AVL最多多一次比较。红黑树在添加和删除结点时比AVL减少旋转次数,旋转三次以内就会解决不平衡,而AVL树追求严格平衡,旋转次数很多。因此大多选用红黑树。

  • 树根:必须是黑色
  • 叶子节点:黑色(NULL)
  • 红色节点的子节点都是黑色(不存在两个红色节点连续)
  • 从任一节点到每个叶子节点路径包含相同数量的黑色节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 前一个节点
boolean red; //颜色
/*
这个构造函数调用了super(),LinkedHashMap.Entry的构造函数中也是调用super();就回到了上面的Node类中:
Node(int hash, K key, V value, Node<K,V> next) {}

*/
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}

/**
* 返回这个节点的根节点,就是不断向上找
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
//根节点的父节点是null
if ((p = r.parent) == null)
return r;
r = p;
}
}

/**
* 确保传进来的root节点是这个二叉树的根节点
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;//n是HashMap的数组长度
//验证传进来的参数是否合法,tab是HashMap的哈希表
if (root != null && tab != null && (n = tab.length) > 0) {
//上面解释过了,index是哈希表数组的索引
int index = (n - 1) & root.hash;
//如果是红黑树的结构,哈希表数组中存储的结点是红黑树的头节点,所以这里直接取tab[index]就是取出来红黑树的头节点,可以看上面的图
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
//如果头节点和传进来的root不相同
if (root != first) {
Node<K,V> rn;
//直接把root放进去
tab[index] = root;
TreeNode<K,V> rp = root.prev;//rp等于root结点的前一个结点
//如果存在下一个结点
/*
这里是这样:rp-->root-->rn 现在把root拿出来当红黑树的根结点了变成了:rp-->rn
因为是双向链表,需要:rp<--rn
*/
if ((rn = root.next) != null)
//下一个结点rn的前驱结点设置为root的前驱rp<--rn
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
//rp-->rn
rp.next = rn;
/*
这里变成了:root-->frist;null<--root<--frist
*/
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
//checkInvariants()方法还没看,这里如果返回false就会抛出AssertionError错误,然后终止执行
assert checkInvariants(root);
}
}

/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
h:hash值 k:key kc:缓存key?
给定hash值和key找到这个节点
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
//从节点p还是查找
TreeNode<K,V> p = this;
do {
//ph:p节点的hash值; pk:节点p的key
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
/*
这里能看出来,红黑树是根据hash值来判断一个节点应该去左边还是右边。这里可能会有个疑问,前面判断在哈希表数组索引也是用的hash值,那红黑树中所有的hash值不应该一样吗?其实,前面哈希表数组中判断的hash值是Node节点的hash值:Objects.hashCode(key) ^ Objects.hashCode(value);也就是key的hash值和value的hash值取异或,而这里用到的hash值是key的hash值,还不知道value是多少。
*/
//h小于p节点的hash值,向左查找
if ((ph = p.hash) > h)
p = pl;
//h大于p节点的hash值,向右查找
else if (ph < h)
p = pr;
//判断key是否相同,相同就查找到了
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//这是出现hash值相同但是key不同的情况?
else if (pl == null)
//左子树是null就去右子树找
p = pr;
else if (pr == null)
//左子树是null去右子树找
p = pl;
//comparableClassFor方法是获取k的运行时类型,compareComparables方法先判断,key与运行时kc是同类型,在通过调用k和kc实现的Comparable接口的compareTo进行比较
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//在右子树里面递归
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}

/**
* 从根节点开始查找
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//parent==null 就说明是根结点,否则就找到根结点再查找
return ((parent != null) ? root() : this).find(h, k, null);
}

/**
比较两个对象的大小,不会返回0
*/
static int tieBreakOrder(Object a, Object b) {
int d;
//比较类名,如果相同,调用本地方法为对象生成hashcode值,再继续比较
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}

/**
* 链表转成红黑树
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
//把x在链表里面取出来,next指向下一个结点
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;//设置左右子树为null
//如果x是第一个结点,也就是root为null的情况,将父结点指向null,颜色是黑色
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//下面的代码和find函数差不多,就是找到k应该去的位置。
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//dir小于等于0去左边,大于0去右边。这里找到了应该去的位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//旋转节点,保持平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
//将根节点放进去
moveRootToFront(tab, root);
}

/**
* 将树转成链表
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}

/**
* 插入元素
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;//标记是否查找一次
TreeNode<K,V> root = (parent != null) ? root() : this; //获取根结点
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//如果K和PK通过COMPARATO比较之后,如果相同就进来,并且只会进来一次
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//从左子树或者右子树中找到就返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
//找到合适的位置,然后插入
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
//创建一个新的节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}

/*
删除结点
1.如果删除的是红色结点则不影响性质
2。如果删除的是黑色结点,那么路径上会少一个黑色结点,破坏了性质
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
//找到根结点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
//succ表示下一个结点,pred表示上一个结点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
//如果要删除根结点,直接将下一个结点提上来
if (pred == null)
tab[index] = first = succ;
//否则就直接将当前结点的上一个结点指向当前结点的下一个结点
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
//根结点赋值到root
if (root.parent != null)
root = root.root();
//红黑树结点太少,转换成链表返回
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
/*
找到结点之后,需要删除结点,红黑树删除结点分为几种情况:
情况1:删除的结点左右子树都非空(按其他二叉树删除的方式处理,转变成其他的下面的三种情况)
情况2:删除的结点左子树为空,右子树非空
情况3:删除的结点右子树为空,左子树非空
情况4:删除的结点左右子树都为空
*/
//情况1
if (pl != null && pr != null) {
//这里s指向的是当前结点p的右节点
TreeNode<K,V> s = pr, sl;
//不断找要删除结点的右子树里面的最左结点,赋值给s
while ((sl = s.left) != null) // find successor
s = sl;
//交换要删除结点右子树的最左叶子节点s和要删除结点p的颜色
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
//特殊情况,如果p结点的右结点s没有左孩子
if (s == pr) { // p was s's direct parent
//直接交换p结点和s结点
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
//还是交换s结点和p结点
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
//如果s存在右结点,就将p设置为sr的父结点
if ((p.right = sr) != null)
sr.parent = p;
//p的左结点给s
if ((s.left = pl) != null)
pl.parent = s;
//p的父结点给s
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
//如果s有右结点,则replacement等于右结点,否则为p
if (sr != null)
replacement = sr;
else
replacement = p;
}
//情况3
else if (pl != null)
replacement = pl;
//情况2
else if (pr != null)
replacement = pr;
else //情况4
replacement = p;
//当p有孩子或者s有孩子,进行删除结点操作
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
//对红黑树进行调整
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//当p结点没有孩子,或者s结点没有孩子,进行删除操作
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}

/**
红黑树扩容时调用拆分方法,将红黑树拆成两个链表
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//两个变量计数,如果很小将变成链表,否则变成红黑树
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}

if (loHead != null) {
//UNTREEIFY_THRESHOLD = 6 ,如果拆分后小于这个值就转成链表,否则就转成红黑树
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}

/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
//下面是左旋和右旋调整结点,在我另外一篇博客中已经将清楚了[算法导论学习笔记--红黑树](https://www.gwt.fun/articles/2017/08/26/1546585401831.html#%E6%97%8B%E8%BD%AC)
//左旋
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
//右旋
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
/*
插入结点后调整方法,插入结点遵循下面的规则:
1、插入的结点重视红色
2、如果插入结点的父结点是黑色,能保证性质
3、如果是红色,则破坏了性质,必须进行重新染色或者旋转
插入过程详解过程:https://www.gwt.fun/articles/2017/08/26/1546585401831.html#%E6%8F%92%E5%85%A5
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true; //先设置插入的结点为红色
//xp:x的父结点;xpp:x的父结点的父结点,也就是爷爷结点;xppl:爷爷结点的左节点;xppr:爷爷结点的右结点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//如果x的父结点是null,表示x是根结点,直接返回x
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果父结点是黑色或者不存在爷爷结点,就直接返回
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//如果父结点是爷爷结点的左结点
if (xp == (xppl = xpp.left)) {
//如果爷爷结点的右结点存在,并且该结点是红色
if ((xppr = xpp.right) != null && xppr.red) {
//这里结合图看的会更明白,把叔叔结点设置成黑色,父结点设置为黑色,爷爷结点设置成红色
xppr.red = false;
xp.red = false;
xpp.red = true;
//爷爷结点赋值给x,继续循环
x = xpp;
}
//爷爷结点的右结点不存在或者叔叔结点是黑色
else {
//判断x是否是父结点的右节点
if (x == xp.right) {
//x上升至父结点,并左旋
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
//x是左孩子,x的父结点设置为黑色,x的爷爷改成红色然后右旋
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else { //将上面的过程反过来
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/*
删除结点后调整规则:
1、如果删除红色结点,不破坏规则
2、如果是黑色,就少了一个黑色

https://www.gwt.fun/articles/2017/08/26/1546585401831.html#%E5%88%A0%E9%99%A4
*/
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
//删除后结点是null或者是根结点,不调整
if (x == null || x == root)
return root;
//x成为根结点,将x设置为黑色
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果x是红的,设置成黑
else if (x.red) {
x.red = false;
return root;
}
//x是父亲的左孩子
else if ((xpl = xp.left) == x) {
//x的兄弟是红色的
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
//没有兄弟,则x到父结点位置
if (xpr == null)
x = xp;
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
//如果x结点的兄弟是黑色的,并且左右结点都是黑色
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
//x的兄弟是黑色,右结点是黑色,左结点是红色
if (sl != null)
sl.red = false;
//将x的兄弟结点变成红色,然后右旋
//下面的不写了,就是那几个过程,看明白就行,代码看不看都可以
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // 反过来的
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}

/**
* 从根结点开始检查红黑树,是否符合红黑树的性质
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
}

put(K,V)

终于看完了TreeNode类,现在继续看HashMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果tab是空或者长度为0,就扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果哈希表数组中是空,就创建一个结点放进去
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//hash 相同,并且key相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
//如果遍历完链表也没找到,就直接加在最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表长度大于等于8就变成红黑树,同理,红黑树小于6就转成链表
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//在链表中找到
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//更新value值
if (e != null) { // existing mapping for key
//
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//threshold是阈值,超过就要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取旧哈希表长度
int oldThr = threshold; //获取旧阈值
int newCap, newThr = 0;
if (oldCap > 0) {
//超过Integer最大值,就不能加了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//调用了无参构造函数,使用默认容量16,阈值为0.75*16
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新阈值为0,则重新计算,如果超过了Integer最大值,就直接赋值最大值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//更新阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧tab里面有数据
if (oldTab != null) {
//遍历每一个哈希表数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//临时将结点赋值给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果e只有一个结点,就重新计算然后存到新tab里面
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果结点多,是红黑树,就拆分
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/*
e.hash & oldCap,这个也非常的巧妙,刚在红黑树的split中看这个式子还没闷过来弯,在这里看明白了。(table.length - 1) & hash是查找索引,而这里没有减一。
这样就得出来两个值,0或者oldCap,这样就能拆成两个链表


*/
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

treeifyBin()

这个方法的作用是将链表转成红黑树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果链表达到了转换成红黑树的阈值,但是tab的数量没到变成红黑树的阈值,也不会变化。MIN_TREEIFY_CAPACITY = 64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

参考资料

1.hashcode()和hash()

介绍

Stack是一种先进后出的数据结构,继承于Vector,所以Stack是通过数组实现。

源码分析

继承

1
2
public
class Stack<E> extends Vector<E> {}

构造函数

Stack只有一个默认的构造函数,创建一个空的栈。

1
2
public Stack() {
}

所有API

1
2
3
4
5
             boolean       empty()
synchronized E peek()
synchronized E pop()
E push(E object)
synchronized int search(Object o)

Stack只是在Vector的基础上添加了几个方法。

1
2
3
4
5
6
//push 将数据存放在数组的最后
public E push(E item) {
addElement(item);

return item;
}
1
2
3
4
5
6
7
8
9
10
//弹出数据操作
public synchronized E pop() {
E obj;
int len = size(); //先获取栈的大小

obj = peek(); //取出来栈中最上面的数据,也就是数组中最后一个数据
removeElementAt(len - 1);//将最后一个删除掉

return obj;
}
1
2
3
4
5
6
7
8
//查看栈顶元素,但是不移除
public synchronized E peek() {
int len = size(); //获取栈的大小

if (len == 0) //判断是否为空栈
throw new EmptyStackException();
return elementAt(len - 1); //elementAt是根据索引查出来数据
}
1
2
3
4
//判断是否为空栈
public boolean empty() {
return size() == 0;
}
1
2
3
4
5
6
7
8
9
//搜索元素0在栈中的位置,从1开始,距离栈底的大小。
public synchronized int search(Object o) {
int i = lastIndexOf(o);

if (i >= 0) {
return size() - i;
}
return -1;
}