Java对象刨根问底

如何创建一个对象

Java创建一个对象,最简单的方式就是

1
Object a = new Object();

可以,大家有没有想过,在JVM中,创建一个对象的流程是如何呢?其实具体流程入下图所示。

img

###分配内存

对象所需的内存大小,在类加载完成后便可以完全确认,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。那么如何划分,有两种方式:

  • 指针碰撞(Bump The Pointer):假设Java堆中的内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就是朝着空闲的那一边“挪动”与对象大小一样的距离。
  • 空闲列表(Free List):如果Java堆中的内存不规整,已使用的内存和未使用的内存相互交错,就没办法简单进行指针碰撞了,需要维护一个列表,记录哪些内存块是可用的,在分配的时候,找一块足够大的空间分配给对象,并更新列表的记录。

因此,在使用Serial,ParNew等带Compact过程的收集器时,系统采用的分配方式是指针碰撞法,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的是空闲列表法。

除了分配方法外,还需要考虑同步的问题。实际上虚拟机采用了CAS配上失败重试的方式保证更新的原子性,另一种是把内存分配的动作按照线程划分在不同的空间之中,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),可以通过-XX:+UseTLAB参数来设定。

对象内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

HotSpot虚拟机对象头包括两部分内容:

  • 第一部分

用于存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分长度在32bit和64bit虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID,偏向时间戳,对象分代年龄 01 可偏向
  • 第二部分

类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

对象访问定位

有两种方式,句柄访问直接指针访问。这两种访问方式各有优点,使用句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收移动对象是很常见的事情)时只会改变句柄中的实例数据指针,而reference本身不需要更新。使用直接指针访问方式最大好处就是速度更快,它节省一次指针定位的时间开销,由于对象访问在Java中非常频繁,这类开销积少成多,将是一个比较可观的提升。HotSpot就是采用直接指针法。

img

​ 通过句柄访问对象

img

​ 通过直接指针访问对象

java类加载内部流程

Java类加载机制的七个阶段

而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

##加载

下面是对于加载过程最为官方的描述。

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。

其实加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:

  • JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
  • 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。

当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程。

##准备(重点)

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • 内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

1
2
public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
  • 初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

1
public static int sector = 3;

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

1
public static final int number = 3;

之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

##初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。

##使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。

##卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。

总结

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

引用

https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html

Java动态代理

代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

举一个例子:老师要批作业,可以找学习委员代收,学一委员整理好之后,再送到老师的办公室。这个场景中,学习委员就是一个代理。

##为什么要用代理模式

  • 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
  • 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

静态代理

静态代理,就是采用代理模式思想,通过硬编码定义一个代理类,来实现代理的功能。由于静态代理灵活性较弱,在实际应用中,大多数采用动态代理模式。

动态代理模式

jdk动态代理

Jdk动态代理是基于接口的。

  • Step1 定义一个interface Hello
1
2
3
public interface Hello {
void sayHello(String str);
}
  • Step2 定义HelloImpl
1
2
3
4
5
6
7
8
9
10
11
12
public final class HelloImpl implements Hello {
private int[] array;

public HelloImpl() {
this.array = new int[1024*10];
}

@Override
public void sayHello(String str) {
System.out.println("hello " + str);
}
}
  • Step3 实现InvocationHandler的invoke方法
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
public class LogInvocationHandler implements InvocationHandler {
private Hello hello;

public LogInvocationHandler(Hello hello) {
this.hello = hello;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("sayHello".equals(method.getName())) {
System.out.println("You said: " + Arrays.toString(args));
}
return method.invoke(hello, args);
}


public static void main(String[] args) {
Object proxy = Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), // 1. 类加载器
new Class<?>[]{Hello.class}, // 2. 代理需要实现的接口,可以有多个
new LogInvocationHandler(new HelloImpl()));// 3. 方法调用的实际处理者

Hello hello = (Hello) proxy;
hello.sayHello("world");
System.out.println(hello);
}
}

但是Jdk动态代理使用上,有局限性。不能所有的类,都要实现一个interface,这会大大增加代码的冗余。如果使用继承的话,就能够解决这个问题。

CGLIB

cglib通过继承来实现动态代理,cglib的底层实现,是基于节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。如下是class文件的格式。

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
ClassFile { 
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

magic: 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE
minor_versionmajor_version:分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。
constant_pool_count:常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。
constant_pool:常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。
access_flagsaccess_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLICACC_FINALACC_SUPER等等。
this_class:类索引,指向常量池表中项的一个索引。
super_class:父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。
interfaces_count:接口计算器,表示当前类或者接口的直接父接口数量。
interfaces[]:接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。
fields_count:字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info
fields:字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。
methods_count:方法计数器,表示当前class文件methos表的成员个数。
methods:方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。
attributes_count:属性表,其中是每一个attribute_info,包含以下这些属性,InnerClassesEnclosingMethodSyntheticSignatureAnnonation等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 动态代理不能是final类
* 如果是final类,会抛出异常:java.lang.IllegalArgumentException: Cannot subclass final class com.wsy.learn.jvm.autoproxy.cglib.HelloConcrete
*/
public class HelloConcrete {
/**
* 注意,此处如果定义的是final方法,则不会进入intercept方法进行方法增强
* @param str
* @return
*/
public final String sayHello(String str) {
return "HelloConcrete: " + str;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("You said: " + Arrays.toString(args));
return proxy.invokeSuper(obj, args);
}

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
}
}

动态代理最佳实践 - SpringAOP

关于什么是SpringAOP,在此不继续阐述。

springAOP既采用了jdk proxy,也采用了cglib。
spring会根据情况,来选择jdk动态代理还是cglib动态代理。

  • 如果要代理的对象实现了接口,则默认采用jdk proxy。
  • 如果要代理的对象实现了接口,也可以强制使用cglib。需要配置<aop:aspectj-autoproxy proxy-target-class=”true”/>
  • 如果要代理的对象没有实现接口,则采用cglib动态代理。
  • 但是注意,敲黑板!敲黑板!敲黑板!
    • 采用cglib的前提下,final类不能进行动态代理会抛出异常java.lang.IllegalArgumentException: Cannot subclass final class xxx(final类不能被继承,所以你懂得)。
    • 采用cglib的前提下,final方法也不能进行动态代理,不过不会抛出异常,而是直接跳过功能增强。
    • 采用jdk proxy,final类和final方法都能正常工作。

JVM内存区域

JVM内存区域总体介绍​

其中VM Stack(虚拟机栈),Native Method Stack(本地方法栈),Program Counter Register(程序计数器),是线程私有的。Method Area(方法区),Heap(堆)是线程共享的。

注意:没有特殊说明,本文中所讲的都是基于HotSpot虚拟机。

img

##程序计数器

程序计数器是线程私有的,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中(也仅是概念模型,各个虚拟机可能通过更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

多线程场景下,为了线程切换后能恢复到正确的位置,所以每个线程需要一个独立的程序计数器。

如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空(Undefined)。次内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

Java虚拟机栈,也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,它不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是代表对象的句柄)。局部变量表所需的内存空间在编译期间完成分配,方法在帧中分配多大的空间,也是完全确定的。

这个区域存在两种异常情况:

  • 当栈的深度超过虚拟机所允许的深度的时候,抛出StackOverflowError。
  • 如果虚拟机栈允许动态扩展,如果无法申请到足够的内存,则会抛出OutOfMemoryError。

本地方法栈

本地方法栈的功能和Java虚拟机栈很接近,它们的区别是Java虚拟机栈是为虚拟机执行Java代码(字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈,也存在StackOverflowError和OutOfMemoryError。

Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有对象都分配到堆上也变得不那么绝对了。

Java堆是垃圾收集器管理的主要区域,从内存回收角度来看,由于现在收集器基本都采用分代收集算法,Java堆中还分为了新生代和老年代。当堆无法继续扩展的时候,将会抛出OutOfMemoryError。

方法区

方法区(Method Area)也是线程共享区域,它用于存储被虚拟机加载的类元信息、常量、静态变量、及时编译器编译后的代码等数据。JDK8之前,虚拟机采用永久代实现方法区,类元信息、常量、静态变量等都存在堆中,和年轻代、老年代是连续的。

1
2
3
4
5
-XX:PermSize
方法区初始大小
-XX:MaxPermSize
方法区最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

Jdk8之后,官方废弃了永久代,在本地内存中开辟了一块空间,叫做元空间(MetaSpace),专门存储类的元数据信息。常量、静态变量存储到了堆上。

直接内存

很多人忽略了直接内存,举一个例子,JDK NIO中,Buffer调用的Native方法直接分配是堆外内存,也就是直接占用系统内存。所以在调整JVM参数的时候,要给系统留一些Buffer。避免内存动态扩展时出现问题。

ThreadLocal原理分析

ThreadLocal总体介绍

​ ThreadLocal类在并发编程中,每个线程实例,都有一份独立的副本,采用以空间换时间的方式,处理并发。

在Thread类中,ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap就是线程变量的容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}

​ 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key),key就是指ThreadLocal引用本身this。跟WeakHashMap不同的是,ThreadLocal的构造函数,没有指定ReferenceQueue。ThreadLocal用一种不同的方式,来避免内存泄漏,在下面的章节中,会详细介绍ThreadLocal的方式。

​ ThreadLocal的API,就不在此阐述,很简单。

每个线程独立备份

​ 为了达到这个目的,有两个方案。

## 方案A

 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。增加线程与减少线程均需要写 Map,故需保证该 Map 线程安全。线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。
  • 结论:加锁势必会降低ThreadLocal的性能。猜测JDK为了性能考虑,没有采用此方案

方案B

Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。
  • 结论:JDK采用了B方案,也有对应的方案来解决内存泄漏问题

#JDK如何解决内存泄漏问题

WeakHashMap原理分析

WeakHashMap总体介绍

​ WeakHashMap继承自AbstractMap,实现了Map接口,拥有了Map的基础功能。但是与常用的Map(比如HashMap)不同的是,WeakHashMap底层的存储单元Entry采用WeakReference,在GC的时候,会回收K,V。所以WeakHashMap天然比较适合用作缓存,即使K,V丢失,也不会对业务造成影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;

/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}

...
}

​ 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key, queue)传入了key和ReferenceQueue。

​ 在调用get(),put(),size()等方法的时候,会执行一个私有方法expungeStaleEntries,如下所示。

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
private void expungeStaleEntries() {
//迭代弱引用队列中要被回收的对象
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
//操作链表
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
//敲黑板,这行代码很重要,如果不执行e.value = null,map的value会泄漏。
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

#图解WeakHashMap原理

img

​ 图1

​ 如图1所示,key1,key2,key3虚引用指向Entry1,Entry2,Entry3,每个entry又分别指向v1,v2,v3。GC之后,key2虚引用被垃圾回收。通过执行expungeStaleEntries方法,使得Entry2变成游离态,Entry2也和v2脱离了引用关系。所以Entry2,v2都变成了可被垃圾回收的状态。

img

​ 图2

java引用详解

一般情况下,java开发中用的都是强引用。例如如下代码所示。

1
Object ref = new Object();

实际上在java中,还有一种不太常用的“弱引用”类型。弱引用中分为了软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference)。所谓的强引用,其实就是FinalReference。

问题随之而来,为什么引用要分为强弱?弱引用又适用于何种场景?本文结合源码,带大家了解java引用的世界。

引用强弱之分

如果大家对JVM内存回收(GC)有一定了解,一定知道内存回收的前提是对象引用“不可达‘’,才能在回收阶段真正的释放掉对象占用的内存。强引用下,如果对象的引用一直处于可达状态,这块内存是没有办法回收的。在内存资源充沛的情况下还好,但是如果内存资源吃紧,为了服务的可用性,一些“不必要”,或者说“不那么重要的”内存,是不是可以释放掉?这个时候,弱引用就派上了用场。各个引用类的对比,请参考下标

引用类型 回收方式 必须配合ReferenceQueue 说明
FinalReference 不回收 默认情况下使用强引用
SoftReference 内存不充足情况下,GC之后回收,如果使用了queue同WeakReference一样的处理 非必要对象,为了内存空间
WeakReference GC立刻回收,如果使用了queue,需手动把要回收的对象置为null 非必要对象,为了内存空间
PhantomReference(幽灵引用) GC立刻回收 get默认返回null,使用虚引用的目的是通过queue监听对象回收

弱引用典型应用

WeakHashMap,ThreadLocal,JVM缓存实现。

源码分析

下图是java.lang.ref包下的类图,四种引用全部继承自Reference。

img

JDK把引用分为了四个状态。

状态 判定条件 说明 源码注释
Active (queue==ReferenceQueue\ \ ReferenceQueue.NULL) && next == null 新创建的引用对象是这个状态。在GC检测到引用对象已经到达合适的Reachability(可达性)时,GC会根据引用对象在创建时是否指定ReferenceQueue参数进行状态转移,如果指定则转移到pending,否则直接转移到Inactive。 Active: Subject to special treatment by the garbage collector. Some time after the collector detects that the reachability of the referent has changed to the appropriate state, it changes the instance’s state to either Pending or Inactive, depending upon whether or not the instance was registered with a queue when it was created. In the former case it also adds the instance to the pending-Reference list. Newly-created instances are Active.
Pending queue == RefrenceQueue && next == this (jvm设置) pending-Refence链表中引用都是这个状态,它们等着被内部线程ReferenceHandler处理入队(调用RefenceQueue.enqueue方法),没有注册的实例不会进入此状态。 An element of the pending-Reference list, waiting to be enqueued by the Reference-handler thread. Unregistered instances are never in this state.
Enqueued queue == ReferenceQueue.ENQUEUED && next == 下一个要处理的Reference对象,或者链表中最后一个next == this 相应的对象已经为待回收并放到queue中,准备由外部线程来询问queue获取相应的数据。调用ReferenceQueue.enqued方法后的Reference对象处于这个状态,当Reference实例从队列中移除之后,它的状态改为Inactive,没有注册的实例不会进入该状态。 Enqueued: An element of the queue with which the instance was registered when it was created. When an instance is removed from its ReferenceQueue, it is made Inactive. Unregistered instances are never in this state.
Inactive queue == ReferenceQueue.NULL && next == this 即此Reference对象已由外部queue中获取到,并且已经处理掉了。即意味着此对象是可以被回收的,并且对内部封装的对象也可以被回收掉了(具体的回收运行,取决于Clear动作是否被调用,可以理解为进入此状态的Reference对象是应该被回收掉的)一旦Reference对象变成Inactive,它的状态就不会再变化。 Nothing more to do. Once an instance becomes Inactive its state will never change again.

基于上表的讲解,画出了如下引用状态流程图。

img

java类加载之ClassLoader

何为类加载

大家知道Java是一门编译型语言,源代码文件是以.java结尾的。通过编译器编译之后,会形成一个字节码文件,也就是class文件。在一个应用中,不同的class文件中封装着不同的功能,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

类加载器

BootstrapClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:

1
2
3
4
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}

如下是BootstrapClassLoader加载的类:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/classes

ExtClassLoader

称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

AppClassLoader

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。可以说应用程序自己定义的类,都是由AppClassLoader负责加载。

CustomClassLoader

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

ClassLoader加载类原理

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

##为何需要双亲委派

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

JDK如何判定Class相同

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

如何定义CustomClassLoader

如下代码,展示了本地目录的类加载器。

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
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}

Spring初始化源码分析

spring配置文件,需要关注如下两个类。

1
2
org.springframework.beans.factory.config.PropertiesFactoryBean
org.springframework.context.support.PropertySourcesPlaceholderConfigurer

img

​ PropertiesFactoryBean类图

img

​ PropertySourcesPlaceholderConfigurer

如下是配置方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--使用PropertySourcesPlaceholderConfigurer的配置方式-->
<context:property-placeholder location="classpath*:config/*.properties"/>
<!--使用PropertiesFactoryBean的配置方式-->
<util:properties id="appConfig1" location="classpath*:config/*.properties"/>
<!--使用PropertySourcesPlaceholderConfigurer的配置方式-->
<bean id="spc" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:config/data.properties</value>
<value>classpath:config/data1.properties</value>
</list>
</property>
<property name="ignoreResourceNotFound" value="false"/>
</bean>
<!--使用PropertiesFactoryBean的配置方式-->
<bean id="appConfig" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:config/data.properties</value>
<value>classpath:config/data1.properties</value>
</list>
</property>
</bean>

​ 使用方式上,用@Value注解

PropertiesFactoryBean用#{}的方式

PropertySourcesPlaceholderConfigurer用${}的方式

1
2
3
4
5
6
7
8
9
10
@Value(value = "${driveClass}")
private String driveClass;
@Value("${url}")
private String url;

@Value("#{appConfig['userName']}")
private String userName;

@Value("#{appConfig['config1']}")
private String password;

Spring初始化源码分析

本文主要介绍下Spring的初始化过程。

img

​ XML方式Spring应用入口

首先,在web容器启动之后,会加载org.springframework.web.servlet.DispatcherServlet,作为容器的一个Servlet实例,该Servlet也是Spring WEB应用的唯一一个Servlet实例。Servlet初始化,会调用org.springframework.web.servlet.HttpServletBean#init方法,初始化Servlet,见下面源码。

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 config parameters onto bean properties of this servlet, and
* invoke subclass initialization.
* @throws ServletException if bean properties are invalid (or required
* properties are missing), or if subclass initialization fails.
*/
@Override
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
}
// Let subclasses do whatever initialization they like.
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}

注意代码initServletBean,该方法被FrameworkServlet重写。如下代码。

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
/**

- Overridden method of {@link HttpServletBean}, invoked after any bean properties
- have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();

try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}

if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}​

initWebApplicationContext()方法,最终会调用createWebApplicationContext()方法,来初始化Spring的上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Servlet with name '" + getServletName() +
"' will try to create custom WebApplicationContext context of class '" +
contextClass.getName() + "'" + ", using parent context [" + parent + "]");
}
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

wac.setEnvironment(getEnvironment());
wac.setParent(parent);
wac.setConfigLocation(getContextConfigLocation());

configureAndRefreshWebApplicationContext(wac);

return wac;
}

img

​ WEB容器的方式Spring应用入口

img

​ WEB容器的方式Spring Context初始化
最后引出最重要的代码,org.springframework.context.support.AbstractApplicationContext#refresh

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
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
}
}

这个代码的作用,是初始化整个容器,创建Bean.