类加载:jvm读取.class文件,解析获取类的信息,生成对应的Class对象。
类加载的过程
主要分为加载、连接、初始化3个阶段,连接又可细分为验证、准备、解析3个阶段。
1、加载 Loading
- 通过类的全限定名获取类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成对应的Class对象
2、连接 Linking
加载是获得Class对象,连接是把Class对象放到java运行时环境中,即把Class对象连接到jre。
(1)验证
- 校验被加载的类是否符合jvm要求,内部结构是否正确,和其他类是否协调一致,保证类的正确性
- 主要包括文件格式验证、元数据验证、字节码验证、符号引用验证
(2)准备
- 给类的静态成员在方法区中分配内存空间并设置初始值,如果静态成员还使用了final修饰,会在此阶段显式赋指定的值。
- 静态成员是在方法区中分配内存,实例成员是在堆中分配内存
(3)解析
- 将Class对象中的符号引用替换为直接引用
- 符号引用是用符号表示引用,直接引用是直接指向目标的指针、相对偏移量、句柄等。
- Class对象是第一步加载时生成的,那时还没有分配内存,不知道该引用对应的内存地址,只能用符号引用暂时表示内存地址。连接的准备阶段已经分配了内存,确定了引用的内存地址,在解析阶段可以用直接引用(内存地址)替换掉符号引用。
3、初始化 Initialization
- 执行初始化方法 <clinit> (),对类的静态成员进行初始化
- 静态代码块在此阶段执行,静态成员变量在此阶段被显式赋值
- <clinit> ()是java编译器提取类中的所有静态成员变量声明时的赋值操作和静态代码块中的语句合并而来。
类加载器
类加载器用于完成类的加载,一共4种,jvm提供了前3种,按加载的先后顺序依次为
- Bootstrap ClassLoader :根类加载器,又叫做引导类加载器,负责加载jdk中的核心类库。根类加载器是用C++写的,不继承java.lang.ClassLoader。
- Extension ClassLoader :扩展类加载器,负责加载jdk中的扩展类库(非核心类库),扩展类加载器是用java写的,继承自ClassLoader。
- System ClassLoader:系统类加载器,负责加载第三方jar包、我们自己写的类。系统类加载器是用java写的,继承自ClassLoader。
- 自定义的类加载器:可以继承ClassLoader类来实现自定义的类加载器
jvm如何判断2个Class对象是否是同一个类的
- 全限定类名是否相同
- 使用的类加载器是否相同
类加载机制
jvm提供了3种类加载机制,这3种机制共同作用,一起完成类的加载
- 全盘负责:使用类加载器加载一个类时,这个类中所引用的类也由该类加载器进行加载
- 父类委托:也叫作双亲委派机制,如果要加载的类存在父类,先使用父类的类加载器来加载子类,如果父类的类加载器加载不了,再使用类本身的类加载器进行加载。父类委托可以避免类的重复加载。
- 缓存机制:jvm会缓存加载过的类的class对象,要使用某个类时,先在缓存中搜索是否有对应的class对象,有就直接使用、不再加载,没有才加载。运行程序后,如果修改了某个类,需要重启jvm才会生效,不然使用的是之前缓存的class对象。
类加载器的使用流程如下
- 类加载是按需加载,使用该类时才加载
- 因为存在缓存机制,一个类只加载一次,且一个类在内存中最多只有一个class对象。类加载生成的class对象用全类名唯一标识。
-使用双亲委派机制去加载类:避免重复加载同一个.class文件
类加载时机
虚拟机规范定义了只有在以下5种情况下,才会进行类加载
1、虚拟机启动 执行应用程序 加载主类
2、加载子类时,作为父类被先加载
3、对类进行反射调用,比如Class.forname(“Xxx”)、newInstance()
4、MethodHandle和VarHandle可以看作轻量级的反射调用机制,使用这2个调用时需要先使用findStaticVarHandle初始化要调用的类
5、遇到 new 、 getstatic、putstatic、invokestatic 这4条直接码指令之一
- new:创建对象
- getstatic:访问类的静态的成员变量
- putstatic:给类的静态成员变量赋值
- invokestatic:调用类的静态方法
类加载方式
类加载方式有两种
- 隐式加载:使用new创建对象,隐式调用类加载器,加载对应的类到 JVM 中,这是最常见的类加载方式
- 显式加载:使用 loadClass()、forName() 等方法显式加载需要的类,获取到 Class 对象后,调用 Class 对象的 newInstance() 方法来创建类的实例
两种类加载方式的区别
- 隐式加载能直接获取类的实例,显式加载需要调用 Class 对象的 newInstance() 方法来生成类的实例
- 隐式加载能使用带参的构造函数,而Class对象的 newInstance() 不能传入参数,如果要使用带参的构造函数,可以通过反射获取到该类带参的构造方法,通过反射调用带参的构造方法来创建实例
loadClass() 、 forName() 的区别
loadClass() 只执行类加载的第一步:加载,后续操作均未进行;Class.forName() 执行了类加载的整个过程(3步)。
对象的创建过程
1、检测是否已加载对应的类,如果没加载则先加载对应的类。
2、计算对象占用的空间大小,在堆中分配内存空间。
引用类型的成员变量,只分配引用的空间即可。
分配内存的2种方式
- 指针碰撞:适用于内存规整、连续的情况,所有已使用的内存在一边,空闲的内存在另一边,以一个指针标识分界点,先在空闲的一边开辟一块新内存,再移动指针。
- 空闲列表:适用于已使用的内存、空闲内存相互交错,内存琐碎、不规整的情况,jvm维护一个空闲列表,记录空闲的内存块,先从空闲列表中找一块合适的内存块进行分配,再更新空闲列表。
堆内存是否规整由使用的垃圾收集器是否带有压缩整理功能决定,jvm会根据堆内存是否规整选择合适的方式进行内存分配。
这2种内存分配方式都具有多个(2个)步骤,不是原子性操作,对象创建是非常频繁的行为,需要解决多线程创建对象时的内存分配的并发问题。
解决内存分配并发问题的2种方式
- CAS算法实现乐观锁,搭配失败重试来保证内存分配的成功率
- 本地线程分配缓冲:Thread Local Allocation Buffer,TLAB,在堆中预先给每个线程分配一小块内存作为分配缓冲区,每条线程在各自的分配缓冲区进行分配,分配缓冲区用完了才使用CAS方式。
jvm是否使用TLAB方式,可以通过jvm参数指定
#+是使用,-是不使用
-XX: +UseTLAB
3、初始化分配的内存空间。
是按实例成员变量的类型进行计算、分配内存空间的,此时初始化实例成员变量为默认值。静态成员在类加载时就已初始化。
4、设置对象头的相关信息
eg. GC分代年龄、对象的hashCode、锁状态标识、元数据信息
5、执行init()方法对对象进行初始化。
- init()方法由编译器在编译时提取普通初始化块、构造方法、实例成员变量声明时的赋值操作组成,在创建对象时用于显式初始对象的实例成员变量。
- <clinit> ()由编译器在编译时提取静态初始化块、静态成员变量声明时的赋值操作构成,在类加载时用于显式初始化类的静态成员变量。
对象的内存布局
1、对象头用于存储对象的元数据信息
- Mark Word 部分存储对象自身的运行时数据,比如哈希值、gc分代年龄、锁状态标识
- 类型指针指向对象所属的类的元数据,标识对象所属的类
2、实例数据存储的是对象本身的数据,即各成员变量的值
3、对齐填充部分只是让实例数据占用的内存空间是8的倍数,无实际意义
创建对象(类实例化)的4种方式
1、使用new关键字调用构造方法,可调用任意构造方法。
2、通过反射创建实例,实质也是调用构造方法创建实例
- 可以使用class对象的newInstance()方法,实质是调用无参构造方法创建实例,此种方式要求该类提供无参构造方法
- 也可以先获取要使用的构造方法,借助Constructor类的newInstance方法创建实例,此种方式可以调用任意构造方法
try {
//实质是调用无参构造器创建实例
User user = User.class.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
try {
//通过class对象获取要使用的构造方法,通过构造方法创建实例。可以使用任意的构造方法,参数指定构造方法的形参表类型
Constructor<User> noArgConstructor = User.class.getConstructor();
Constructor<User> allArgConstructor = User.class.getConstructor(Integer.class, String.class, String.class);
User user1 = noArgConstructor.newInstance();
User user2 = allArgConstructor.newInstance(1, "chy", "188xxxxxx");
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
3、使用根类Object的clone()方法,通过克隆创建实例
Object类的clone()是native方法,性能高,但访问权限为protected,只能在子类中使用,需要重写修改访问权限。需要注意的是,Object的clone()是浅拷贝。
@Data
@AllArgsConstructor
@NoArgsConstructor //需要实现标记接口 Cloneable,不然调用clone()会报异常 CloneNotSupportedException
public class User implements Cloneable {
private Integer id;
private String name;
private String tel;
/**
* 修改权限为public,把返回值设置为具体的类型,方法体中调用Object的clone()方法
*
* @return
* @throws CloneNotSupportedException
*/
@Override
public User clone() throws CloneNotSupportedException {
return (User) super.clone();
}
}
User user = new User(1, "chy", "188xxxxx");
User user1 = user.clone();
4、使用反序列化创建对象,前提是该类要是可序列化的
IO流的传输是深拷贝,序列化、反序列化创建对象属于深拷贝
/**
* 深拷贝对象
*
* @param source 源对象
* @param <T> 目标类型
* @return 新创建的对象
* @throws IOException, ClassNotFoundException
*/
public static <T> T clone(T source) throws IOException, ClassNotFoundException {
//序列化源对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(source);
//反序列化得到新对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
T target = (T) ois.readObject();
ois.close();
oos.close();
return target;
}
对象的访问方式
对象创建之后,在java虚拟机栈中进行访问,有2种访问方式
- 直接指针访问:虚拟机栈的局部变量表中存储对象的引用(reference类型),通过引用直接访问对象
- 句柄访问:用句柄存储对象的引用,句柄放在句柄池中,虚拟机栈的局部变量表中存储对象的句柄,相当于二级指针。句柄池是堆中的一块内存。
直接指针访问效率高,但gc回收对象时效率低;句柄访问效率低,但gc回收对象时效率高。HotSpot虚拟机采用的是直接指针访问。