类的加载
类加载器分类
类加载器的概述
- 负责将.class文件加载到内存中,并为之生成对应的Class对象。那么有人会问.class文件是哪里来的,它是由javac编译器将java文件编译成.class文件的。
类加载器的分类
- Bootstrap ClassLoader 根类加载器(引导类加载器)
- Extension ClassLoader 扩展类加载器
- System ClassLoader 系统类加载器
- Application ClassLoader 应用程序类加载器
类加载器的作用
- Bootstrap ClassLoader 根类加载器
- 也被称为引导类加载器,负责Java核心类的加载;比如System,String等。在JDK中JRE的lib目录下rt.jar文件中
- 由C++语言实现(针对HotSpot),负责将存放在
\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中,即负责加载Java的核心类。
- Extension ClassLoader 扩展类加载器
- 负责JRE的扩展目录中jar包的加载。在JDK中JRE的lib目录下ext目录
- Sysetm ClassLoader 系统类加载器
- 负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径
- Application ClassLoader
- 负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器,通过ClassLoader.getSystemClassLoader()方法直接获取。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
类的加载机制
类的加载定义
- 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。
- 更加详细一点说,类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
- 在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点来实现的。
触发类加载条件
- ①.遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段的时候(被final修饰,已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。
- 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
- 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
- ②.使用java.lang.reflect包的方法对类进行反射调用的时候。
- ③.当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先出发父类的初始化。
- ④.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- ⑤.当使用JDK1.7的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出发初始化。
类加载的具体过程(*)
加载:
- ①.通过一个类的全限定名来获取定义此类的二进制字节流
- ②.将这个字节流所代表的静态存储结构转换为方法区内的运行时数据结构
- ③.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:
- 是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包含四个阶段的校验动作
- a.文件格式验证
- 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- b.元数据验证
- 对类的元数据信息进行语义校验,是否不存在不符合Java语言规范的元数据信息
- c.字节码验证
- 最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
- d.符号引用验证
- 最后一个阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在连接的第三个阶段——解析阶段中发生。
- 符号验证的目的是确保解析动作能正常进行。
准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中分配。只包括类变量。初始值“通常情况”下是数据类型的零值。而对于 static final 常量而言直接在准备阶段完成赋值,如 public static final a = 10…那么它就是 10。
“特殊情况”下,如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量的值就会被初始化为ConstantValue属性所指定的值。
Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。
1
public static int sector = 3;
但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。
1
public static final int number = 3;
两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。
解析:
- JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。(符号引用替换为直接引用)
- “动态解析”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。
初始化:
- 用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化
- 初始化阶段是执行类构造器
<clinit>()
方法的过程。 <clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。<clinit>()
与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。- 简单地说,初始化就是对类变量进行赋值及执行静态代码块。
使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。
卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。
类的生命周期
什么是类的生命周期
- 类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。严格来说的话那属于类到虚拟机内存这个阶段的生命周期,完整的是:加载——>校验——>准备——>解析——>初始化——>使用——>卸载。
- 对于类变量(static 只能作用域)是在准备阶段完成初值填充,如 int = 0, String = null…,实际的值是在初始化阶段。
- 在这七个阶段中,加载、验证、准备、使用和卸载这四个阶段发生的顺序是固定顺序,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
- 另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
如何理解解析
解析阶段则不确定:它在某些情况下可以在初始化完成后在开始,这是为了支持Java语言的运行时绑定(运行时绑定也叫动态绑定)。
后期绑定,是指在运行时根据对象的类型进行绑定,又叫动态绑定或运行时绑定。实现后期绑定,需要某种机制支持,以便在运行时能判断对象的类型,调用开销比前期绑定大。
运行时(动态)绑定的过程
- 虚拟机提取对象的实际类型的方法表;
- 虚拟机搜索方法签名;
- 调用方法。
注意,这里说的是对象的实际类型。即在多态的情况下,虚拟机可以找到所运行对象的真正类型。
Java中的static方法和final方法(private属于final方法,详细的解释见《Java编程思想》)属于前期绑定,子类无法重写final方法,成员变量(包括静态及非静态)也属于前期绑定。除了static方法和final方法(private属于final方法)之外的其他方法属于后期绑定,运行时能判断对象的类型进行绑定。
其实解析就是符号引用到直接引用的转换过程,因为 Java 使用直接寻址而非句柄的方式。解析分为:类或接口的解析/字段解析/类方法解析/接口方法解析
例子一
1 | public class Book { |
首先根据上面说到的触发初始化的5种情况的第4种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类),我们会进行类的初始化。
Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和 对象初始化方法 。
类初始化方法。编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。类初始化方法就是下面这段代码了
1
2
3
4
5static
{
System.out.println("书的静态代码块");
}
static int amount = 112;对象初始化方法。编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。对象初始化方法就是下面这段代码了
1
2
3
4
5
6{
System.out.println("书的普通代码块");
}
int price = 110;
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);这里没有执行对象的初始化方法,先执行累的初始化方法后,执行输出语句。
例子二
代码如下
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
39class Grandpa {
static {
System.out.println("爷爷在静态代码块");
}
}
class Father extends Grandpa {
static {
System.out.println("爸爸在静态代码块");
}
public static int factor = 25;
public Father() {
System.out.println("我是爸爸~");
}
}
class Son extends Father {
static
{
System.out.println("儿子在静态代码块");
}
public Son()
{
System.out.println("我是儿子~");
}
}
public class InitializationDemo {
public static void main(String[] args) {
System.out.println("爸爸的岁数:" + Son.factor); //入口
}
}
//--------执行结果---------------
最终的输出结果是:
爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25
没有输出「儿子在静态代码块」这个字符串?
这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
首先程序到 main 方法这里,使用标准化输出 Son 类中的 factor 类成员变量,但是 Son 类中并没有定义这个类成员变量。于是往父类去找,我们在 Father 类中找到了对应的类成员变量,于是触发了 Father 的初始化。
但根据我们上面说到的初始化的 5 种情况中的第 3 种(当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)。我们需要先初始化 Father 类的父类,也就是先初始化 Grandpa 类再初始化 Father 类。于是我们先初始化 Grandpa 类输出:
「爷爷在静态代码块」
,再初始化 Father 类输出:「爸爸在静态代码块」
。最后,所有父类都初始化完成之后,Son 类才能调用父类的静态变量,从而输出:
「爸爸的岁数:25」
。
例子三
代码如下
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
50class Grandpa
{
static
{
System.out.println("爷爷在静态代码块");
}
public Grandpa() {
System.out.println("我是爷爷~");
}
}
class Father extends Grandpa
{
static
{
System.out.println("爸爸在静态代码块");
}
public Father()
{
System.out.println("我是爸爸~");
}
}
class Son extends Father
{
static
{
System.out.println("儿子在静态代码块");
}
public Son()
{
System.out.println("我是儿子~");
}
}
public class InitializationDemo
{
public static void main(String[] args)
{
new Son(); //入口
}
}
//输出结果是:
爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~
执行流程:
- 首先在入口这里我们实例化一个 Son 对象,因此会触发 Son 类的初始化,而 Son 类的初始化又会带动 Father 、Grandpa 类的初始化,从而执行对应类中的静态代码块。因此会输出:「爷爷在静态代码块」、「爸爸在静态代码块」、「儿子在静态代码块」。
- 当 Son 类完成初始化之后,便会调用 Son 类的构造方法,而 Son 类构造方法的调用同样会带动 Father、Grandpa 类构造方法的调用,最后会输出:「我是爷爷
」、「我是爸爸」、「我是儿子~」。
例子四
代码如下:
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
37public class Book {
public static void main(String[] args)
{
staticFunction();
}
static Book book = new Book();
static
{
System.out.println("书的静态代码块");
}
{
System.out.println("书的普通代码块");
}
Book()
{
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
}
public static void staticFunction(){
System.out.println("书的静态方法");
}
int price = 110;
static int amount = 112;
}
//-----输出结果是:-------------
书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法
整个执行流程。
当 JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 book 实例变量被初始化为 null,amount 变量被初始化为 0。
当进入初始化阶段后,因为 Book 方法是程序的入口,根据我们上面说到的类初始化的五种情况的第四种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。所以JVM 会初始化 Book 类,即执行类构造器 。简单表示如下:
1
2
3
4
5
6static Book book = new Book();
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;JVM 对 Book 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器 ),后执行对象的构造器(按顺序收集成员变量赋值和普通代码块,最后收集对象构造器,最终组成对象构造器 )。
首先执行
static Book book = new Book();
这一条语句,这条语句又触发了类的实例化。于是 JVM 执行对象构造器 ,收集后的对象构造器 代码:1
2
3
4
5
6
7
8
9{
System.out.println("书的普通代码块");
}
int price = 110;
Book()
{
System.out.println("书的构造方法");
System.out.println("price=" + price +", amount=" + amount);
}于是此时 price 赋予 110 的值,输出:「书的普通代码块」、「书的构造方法」。而此时 price 为 110 的值,而 amount 的赋值语句并未执行,所以只有在准备阶段赋予的零值,所以之后输出「price=110,amount=0」。
当类实例化完成之后,JVM 继续进行类构造器的初始化:即输出:「书的静态代码块」,之后对 amount 赋予 112 的值。
方法论
从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:
- 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
- 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
- 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
- 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。
如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。
生命周期顺序图
这7个阶段发生顺序如下图:
其中加载,验证,准备,解析及初始化是属于类加载机制中的步骤。注意此处的加载不等同于类加载。
双亲委派机制
什么是双亲委派机制
- 主要是表示类加载器之间的层次关系
- 前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承(Inheritance)关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
双亲委派模型工作流程
双亲委派模型的工作流程是:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
为何需要这样
- 这样的好处是不同层次的类加载器具有不同优先级
- 比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,保证安全。即使用户自己编写一个java.lang.Object类并放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。
代码实现案例展示
ClassLoader中loadClass方法实现了双亲委派模型
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
38protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
//检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果该类没有加载,则进入该分支
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
long t1 = System.nanoTime();
c = findClass(name); //用户可通过覆写该方法,来自定义类加载器
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}整个流程大致如下:
- a.首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- b.如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
- c.如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
Java对象
对象的创建的条件
- 虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用;
- 检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有,那必须先执行响应的类加载过程;
- 在类加载检查功通过后,为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。
对象的内存布局
- 分为3个区域:对象头,实例数据,对齐填充。
对象头:
- 包括两部分信息,第一部分:对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32 bit和64 bit,官方称它为“Mark Word”。
- 第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据。
实例数据:
- 是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
对齐填充:
- 对齐填充不是必然存在的。HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对其补充来补全了。
Java对象的创建过程
看下创建类加载过程
Person p = new Person()请写一下类的加载过程?
1
2
3
4
5
6
7
81).因为new用到了Person.class,所以会先找到Person.class文件,并加载到内存中;
2).执行该类中的static代码块(JVM 加载类时执行,仅执行一次。),如果有的话,给Person.class类进行初始化;
3).在堆内存中开辟空间分配内存地址;
4).在堆内存中建立对象的特有属性,并进行默认初始化;
5).对属性进行显示初始化;
6).对对象进行构造代码块化(构造代码块或非静态代码块)初始化; 每一次创建对象时会执行。
7).对对象进行与之对应的构造函数进行初始化;
8).将内存地址付给栈内存中的p变量
对象的创建
Java对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
- 1.类加载检查
- 2.分配内存
- 3.初始化零值
- 4.设置对象头
- 5.执行init方法
①类加载检查:
- 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②分配内存:
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:
- 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(”标记-压缩”),值得注意的是,复制算法内存也是规整的
内存分配并发问题
- 在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试:
CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
TLAB:
- 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
③初始化零值:
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头:
- 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。
- 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
⑤执行 init 方法:
- 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
- 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
对象的内存布局
- 在 Hotspot 虚拟机中,对象在内存中的布局可以分为3快区域:对象头、实例数据和对齐填充。
- Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
- 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
- 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
- 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
测试类的执行
加载类-类名.变量
代码案例如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class A{
public static int value = 134;
static{
System.out.println("A");
}
}
class B extends A{
static{
System.out.println("B");
}
}
public class Demo {
public static void main(String args[]){
int s = B.value;
System.out.println(s);
}
}a.打印错误结果
1
2
3A
B
134b.打印正确结果
1
2A
134- 观察代码,发现B.value中的value变量是A类的。所以,帮主在这里大胆的猜测一下,当遇到 类名.变量 加载时,只加载变量所在类。
如何做才能打印a这种结果呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class A{
public static int valueA = 134;
static{
System.out.println("A");
}
}
class B extends A{
public static int valueB = 245;
static{
System.out.println("B");
}
}
public class Demo {
public static void main(String args[]){
int s = B.valueB;
System.out.println(s);
}
}- 得到数据
1
2
3A
B
245
加载类-new
那么如果是直接使用new创建对象?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class A{
public static int value = 134;
static{
System.out.println("A");
}
A(){
System.out.println("构造A");
}
}
class B extends A{
static{
System.out.println("B");
}
B(){
System.out.println("构造B");
}
}
public class Demo {
public static void main(String args[]){
B b = new B();
}
}- 那么得到打印结构,子类的构造方法中,必须要调用父类的某个构造方法
1
2
3
4A
B
构造A
构造B
代码块和构造执行顺序
代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class C {
{
System.out.println("代码块C");
}
C(){
System.out.println("构造C");
}
void setData(){
{
System.out.println("方法中代码块C");
}
System.out.println("方法C");
}
}
public class Demo {
public static void main(String args[]){
C c = new C();
c.setData();
}
}打印结果
1
2
3
4代码块C
构造C
方法中代码块C
方法C修改代码块和构造块的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class C {
C(){
System.out.println("构造C");
}
{
System.out.println("代码块C");
}
void setData(){
System.out.println("方法C");
{
System.out.println("方法中代码块C");
}
}
}
public class Demo {
public static void main(String args[]){
C c = new C();
c.setData();
}
}打印结果
1
2
3
4代码块C
构造C
方法C
方法中代码块C得出结论
- 当遇到 类名.变量 加载时,只加载变量所在类。
- 当遇到new加载类时,先执行父类,在执行子类。
- 在同一个类中,代码块比构造方法先执行,方法中根据位置顺序执行。
静态,代码块,构造执行顺序
代码如下所示
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
27public class Demo {
static {
System.out.println("静态代码块");
}
{
System.out.println("构造代码块");
}
public Demo() {
System.out.println("构造函数");
}
public void sayHello() {
System.out.println("普通代码块");
}
public static void main(String[] args) {
System.out.println("执行 main 方法");
new Demo().sayHello();
System.out.println("---------------");
new Demo().sayHello();
}
}打印结果
1
2
3
4
5
6
7
8
9静态代码块
执行 main 方法
构造代码块
构造函数
普通代码块
---------------
构造代码块
构造函数
普通代码块结论
普通代码块、静态代码块、构造代码块和构造方法执行顺序优先级:静态块 -> main() -> 构造块 -> 构造方法。
普通代码块:类中的普通方法,只有被调用才执行。
静态代码块:用 staitc 声明,JVM 加载类时执行,仅执行一次。
构造代码块:类中直接用 {} 定义,每一次创建对象时执行。
安全管理器
SecurityManager,在Java应用中,安全管理器是由System类中的方法setSecurityManager设置的。要获得当前的安全管理器,可以使用方法getSecurityManager。
安全管理器是一个允许应用程序实现安全策略的类。它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否是在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。
SecurityManager应用场景
当运行未知的Java程序的时候,该程序可能有恶意代码(删除系统文件、重启系统等),为了防止运行恶意代码对系统产生影响,需要对运行的代码的权限进行控制,这时候就要启用Java安全管理器。
1
2//代码会删除你的C盘windows文件夹。
Runtime.getRuntime().exec("cmd /c rd C:\\Windows /S /Q");
默认配置文件
默认的安全管理器配置文件是
$JAVA_HOME/jre/lib/security/java.policy
,即当未指定配置文件时,将会使用该配置。内容如下: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// Standard extensions get all permissions by default
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
// default permissions granted to all domains
grant {
// Allows any thread to stop itself using the java.lang.Thread.stop()
// method that takes no argument.
// Note that this permission is granted by default only to remain
// backwards compatible.
// It is strongly recommended that you either remove this permission
// from this policy file or further restrict it to code sources
// that you specify, because Thread.stop() is potentially unsafe.
// See the API specification of java.lang.Thread.stop() for more
// information.
permission java.lang.RuntimePermission "stopThread";
// allows anyone to listen on un-privileged ports
permission java.net.SocketPermission "localhost:1024-", "listen";
// "standard" properies that can be read by anyone
permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";
permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";
permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};
配置文件简单解释
在启用安全管理器的时候,配置遵循以下基本原则:
- 没有配置的权限表示没有。
- 只能配置有什么权限,不能配置禁止做什么。
- 同一种权限可多次配置,取并集。
- 统一资源的多种权限可用逗号分割。
第一部分授权:
- 授权基于路径在”file:$/*“的class和jar包,所有权限。
1
2
3grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};第二部分授权:
- 这是细粒度的授权,对某些资源的操作进行授权。具体不再解释,可以查看javadoc。
1
2
3
4grant {
permission java.lang.RuntimePermission "stopThread";
……
}
启动安全管理器
启动安全管理有两种方式,建议使用启动参数方式。
启动参数方式:启动程序的时候通过附加参数启动安全管理器:
1
-Djava.security.manager
若要同时指定配置文件的位置那么示例如下:
1
-Djava.security.manager -Djava.security.policy="E:/java.policy"
编码方式启动,不过不建议:
- 通过参数启动,本质上也是通过编码启动,不过参数启动使用灵活
1
System.setSecurityManager(new SecurityManager());
项目启动源码如下(sun.misc.Launcher):可以发现将会创建一个默认的SecurityManager;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
自定义安全管理器
1 | //自定义的安全管理器 |
小结
类的生命周期
加载: 把字节码文件.class 加载到方法区的内存中,创建一个对应的对象,堆中(Class),通过Class对象可以获得字节码中的所有信息
连接
验证:对字节码文件.class进行验证,保证虚拟机的安全
准备: 为静态变量分配空间,进行默认初始化
解析:把符号引用替换为直接引用(如把方法用指针替换)
初始化:对静态变量声明处或静态块初始化
使用:
卸载
类的初始化
当一个类被主动使用时:
- 当创建某个类的新实例时
- 当调用某个类的静态成员;
- 当初始化某个子类时,该子类的所有父类都会被初始化。
- 当使用反射方法强制创建某个类或接口的对象时
- 当虚拟机java命令运行启动类
static final类型的不能导致初始化的情况,静态常量在编译期就能确定值的情况不会引起初始化
代码中会引起类的初始化的是:
- 访问n的时候,会引起类的初始化
- 访问num,不会引起类的初始化,在编译期期间就能确定了值是33.
- 访问sn2,也不会引起类的初始化,值为88.
- 访问sn的时候,虽然是final static修饰,但是值不能确定,会引起类的初始化
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
29class Demo{
static int n = 55;
final static int num=33;
final static int sn = 33 + n;
final static int sn2 =33+55;
static {
System.out.println("static 初始化");
}
}
class SubDemo extends Demo{
}
public class TestDemo1 {
/*static int sn = 45;
static{
sn = 55;
}
static {
System.out.println("static !");
}*/
public static void main(String[] args) throws ClassNotFoundException {
// Demo demo = new Demo();
System.out.println(Demo.sn);
// SubDemo demo = new SubDemo();
// Class.forName("day18.loader.Demo");
}
}
类加载器
根类加载器:加载
\jre\lib
下的核心类库
Bootstrap ClassLoader扩展类加载器:加载路径
\jre\lib\ext
的文件
Extension ClassLoader系统(应用)类加载器: 加载classPath路径下的文件(默认当前路径)
App ClassLoader
System ClassLoader自定义类加载器:加载自定义位置的文件
加载器的顺序
如果根类加载器不能加载,看扩展类加载器,不能给系统类加载器,给用户自定义类加载器
双亲委派模型
好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
1 | public class TestDemo2 { |
加载类的Class对象
代码如下
1
2
3
4
5
6
7
8public static void main(String[] args) throws ClassNotFoundException {
//加载类 完全限定命名
ClassLoader.getSystemClassLoader().loadClass("day18.Demo1");
//加载类 ,并初始化,Class.forNmae(加载类,初始化 true,系统类加载器 )
Class.forName("day18.Demo1",true,ClassLoader.getSystemClassLoader());
//简写
Class.forName("day18.Demo1");
}
自定义类加载器
由于是双清委派,在项目外D:/data,建立一个类C.java,编译得到C.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
34
35
36
37
38
39
40//自定义类加载器
class MyLoader extends ClassLoader{
String path;
MyLoader(String path){
this.path = path;
}
// 包名.类名
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = null;
// "d:/data/" + "C" + ".class" ->"d:/data/C.class"
//带包情况下,用.替换
// path = path + name.replace(".", "/").concat(".class");
path = path + name.concat(".class");
FileInputStream fin = null;
try {
fin = new FileInputStream(path);
// 创建了一个和文件大小一样的缓冲区
byte [] b = new byte[fin.available()];
int len = fin.read(b);
c = this.defineClass(name, b, 0, len);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fin != null) {
try {
fin.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return c;
}
}使用自定义类加载器
1
2
3
4
5
6
7
8
9
10
11public class TestDemo4 {
public static void main(String[] args) throws ClassNotFoundException {
// 自定义类加载器 加载类 C .class
MyLoader my = new MyLoader("d:/data/");
Class c = Class.forName("C",true,my);
System.out.println(c.getClassLoader());
c = null;
}
}