一)类加载过程:
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制
一)行为步骤如下:
1:装载
入口有一下几种:
- 通过一个类的全限定名定义此类的二进制流 ,比如load 一个class文件、jar包、网络读取、或者实时计算生成的一个二进制流(如proxy代理,dubbo呢种写bean 完后load的方式)
- 将这个字节流所代表的静态存储结构转换为方法去的运行时数据结构,如Spring Bean 进行标签引导,最终在jvm中得到了与配置相符的类。
- 在内存中生成一个代表这个类的Class对象,做为这个类的各种数据结构入口(Class 对象存在于方法区)
2:链接
连接一共包含三个阶段 ,验证、准备、解析 ;
验证:验证是连接的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会损害虚拟机自身的安全。存在文件格式校验
、元数据校验
、字节码校验
、符号引用校验
。具体的校验规则不做表述,也记不住。
准备:准备阶段正式为类变量分配内存(allocate)并设置初始值(0值,init),这些变量所使用的内存都将在方法区进行分配。
case1
: 按照上述逻辑,你觉的以下demo会挂掉,还是会输出0呢?
public class Demo1 {
private static int i;
public static void main(String[] args) {
System.out.println(i);
}
}
解析:解析阶段是虚拟机将常量池中的符号引用替换为直接应用的过程(符号引用暂且就记忆成全限定类名),具体又可以分为类或者接口的解析
、字段解析
、类方法解析
、接口方法解析
。
3:初始化
一个类被加载到方法区后,呢么啥情景下会初始化呢? 加载就必须必初始化吗?
初始化触发条件:
遇到
new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,如果类没有初始化,则需要先做初始化工作。
这条字节码指令最常见的java代码是 :
使用new关键字实例化的时候
读或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)
以及调用一个类的静态方法
呢么哪些情况是你不太好想到不做初始化
的情景呢?
- 通过子类引用父类的静态字段,子类则不会初始化。
- 通过数组来引用类。
- 调用类的常量。
初始化到底做什么 :在这个阶段主要执行类的构造方法。并且为静态变量赋值为初始值,执行静态块代码。初始化过程会以单线程执行,不会执行并发。
4:类的使用
//假装写了点什么
5:类的卸载
Class对象也可以视为一个普通的bean ,呢么他有什么特权
做到不像普通对象呢样被GC呢?
由java虚拟机自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载 。
原因是 java虚拟机自带的类加载器包含根类加载器
、扩展类加载器
、系统类加载器
,Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用他们所加载类的Class对象。
因此这些Class对象始终是可触及的,既不会被回收。
呢么用户自定义的类加载器Load的class , 当classLoader 与 相关的obj 都不存在指向的时候,它就会被卸载了。
方便记忆
: “家宴准备了西式菜”,即家(加载)宴(验证)准备(准备)了西(解析)式(初始化)菜
二)类加载器:
通过一个类的全限定名来获取描述此类的二进制字节流,实现这个逻辑的代码就叫做类加载器,对应上述篇幅中load过程。
分类:
Bootstrap ClassLoader
负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。App ClassLoader
负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。Custom ClassLoader
通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
自定义类加载器 :自定义类加载器灵活度大,可以基于自定义类加载器定义热部署、代码加解密之类的行为,但除了jetty、tomcat等容器,能见到的地方不多,贴个case2
方便理解:
public class ClassLoaderDemo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader mycl = new ClassLoader() {
//name - > 类的全限定名
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//com.roocon.dsa.dsadsaDemo
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
//进行加载
InputStream ins = getClass().getResourceAsStream(fileName);
if (null == ins) {
return super.loadClass(name);
}
try {
byte[] buff = new byte[ins.available()];
ins.read(buff);
return defineClass(name, buff, 0, buff.length);
} catch (IOException e) {
throw new RuntimeException("");
}
}
};
Object o = mycl.loadClass("com.mmall.classLoaderDemo.ClassLoaderDemo").newInstance();
System.out.println(o.getClass());
//false - > 因为不是同一个
System.out.println(o instanceof ClassLoaderDemo);
}
}
为什么要有这么几个类加载器?
其实我一直很好奇为什么要有这么多类加载器,就不能代码里面写好“if else“,对外表现成一个类加载器不香吗?有这么一个回复可能会说服你我。
Each class loader is designed to load classes from different locations. For instance, you can actually create a class loader that will load a class file from a networked server or download the binary of a class from a remote web server, etc. The logic that performs this operation is baked into the class loader itself and provides a consistent interface so that clients can load classes regardless of how the class loader actually performs the loading. The BootstrapClassLoader is capable of loading classes from the JVM_HOME/lib directory…but what if you need to load them from a different location??
In short, because there as an infinite (well, not quite) number of ways to load classes and there needs to be a flexible system to allow developers to load them however they want.
大义是:每个类加载器主要为了加载不同路径的class。比如,你可以从联网服务器上加载一个class文件,也可以从远程web服务器下载二进制类。这么设计是因为我们需要类加载器提供一致的接口,这样客户端就可以加载类但是却不用管类加载器到底是怎么实现的。启动类加载器能够加载JVM_HOME/lib 下的类,但如果我们需要在其他的情况下加载类呢?简单来说,加载类的方法有无数种,我们需要一个灵活的加载器系统去在特定的情况下按照我们的想法来加载类。
加载原则:
自底向上,从Custom ClassLoader、BootStrap、ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。
但是这个原则并不是不能打破的,呢让我们聊聊为什么要打破
这个原则。
case3
:Tomcat 如果使用默认的类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离 。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
- web容器也有自己依赖的类库,不可于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的
,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份(还记得全面说的加载是依靠全限定类名吗?呢么不同版本的class实际上全限定类名是一致的)。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
第四个问题,我们想我们要怎么实现jsp文件的热修改,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
tomcat具体是怎么实现上述逻辑的 ?
//todo
二)对象创建、分配、寻址过程:
/
1): 当一个对象被创建的时候,需要先检查指令参数是否在方法区中定位到一个类的符号引用
- 如果能定位到,检查这个符号引用代表的类是否被加载、解析以及初始化过;
- 如果不能定位到或没有检查到,就先执行相应的类加载过程;
2): 为对象分配内存
对象所需内存的大小在类加载完成后,便完全确定了(JVM可以通过普通Java对象的类元数据信息确定对象大小),随后把一块确定大小的内存从Java对里面划分出来;
分配方式:
分配方式是看堆是否规整,而堆是否规整是有垃圾回收期确定的。
这点比较好理解,毕竟创建跟回收也是一组行为。
- 当使用Serial、ParNew、等待Compact过程的收集器的时候,jvm将会采用
指针碰撞
的方式进行分配; - 使用CMS这种基于标记-清除(Mark-Sweep)算法的收集器时,则采用
空闲列表
。//todo (G1好像也是,GC复习完回来修改这里成定论)
指针碰撞
:如果Java堆是绝对规整的,既一边是用过的内存,一遍是空闲的内存,中间有一个指针作为边间指示器,分配内存只需要向空闲呢边移动指针。
空闲列表
:如果Java堆不是规整的,既用过的和空间的内存相互叫错,呢么就需要维护一个列表,记录哪些内存是可以使用的。进而分配内存时,先查该列表,找到一个足够大的内存,随后更新列表。
3): 线程安全
用于避免正在给对象A分配内存,但是指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。
- 同步处理:对分配内存的动作进行同步处理,JVM采用CAS机制加上失败重试的方式,保证更新操作的原子性。CAS是采用带版本的方式避免ABA问题。
- 本地线程分配缓冲区(TLAB) :把分配内存的动作按照线程划分在不同的空间中执行,每个线程在堆预先分配一小块内存,称为本地线程分配缓冲区(
Thread Local Allocation Buffer , TLAB
)。哪个线程需要分配内存就从哪个线程的TLAB上进行分配,只有TLAB用完需要重新分配新的TLAB时,才需要同步处理。
Thread Local Allocation Buffer , TLAB
- TLAB :TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
- TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。
- TLAB空间不够时,会直接向Eden申请。
4): 初始化
对象内存初始化为零值,如果使用TLAB,则提前分配至TLAB,这样保证了程序中对象以及实例变量不显示初始赋零值,程序也能访问到零值。
随后对对象头信息进行设置,包括类元数据引用、对象的哈希吗值、对象的GC分代年龄等。
最终执行实例对象的init,按照程序语音进行初始化赋值。
对象创建流程如下:
对象分配流程如下:
新对象申请时,理论上应该是考虑是否分配到栈,若不在栈上且tlab不够放下对象的时候,才会到下述流程。 //todo 该图内容需要修改。
对象的内存布局:
对象头:
- Mark Word : 一系列的标志位 ,如哈希吗、分代年龄、偏向锁Id、偏向锁时间戳 ,于64位系统中占用8字节,锁标志位具体如下.
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码/对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | 10 | 执行重量级锁定的锁指针 |
GC标记 | 11 | 空 |
可偏向 | 01 | 偏向线程id 、偏向时间戳、对象分代年龄 |
- Class Pointer : 指向对象对应的类元数据的内存地址 ,与64位操作系统中同样占用8字节。
- Length数组对象持有 :数组长度 ,四字节。
实例数据:
包含了对象的所有成员变量,大小由各个变量类型决定 ,如 int 4、long 8等。
对齐填充:
为了保证对象的大小为8字节的整数倍。
对象的访问:
对象的存储空间主要是在堆上面,对象的引用却是在堆栈中分配的。
对象创建过程我们简单理解为三步:
- memory = allocate() 分配对象的内存空间;
- ctorInstance() 初始化对象
- instance = memory 设置instance指向刚分配的内存
(这里的2跟3有可能会指令重拍,后续在聊)
Java程序访问对象需要通过栈上的reference数据操作堆上的具体对象,reference要么通过指向句柄再指向对象实例
,要么直接指向对象实例
。
以句柄的方式访问:
使用句柄方式访问对象实例,需要在堆中划分出一块句柄池,句柄与指针类似,记录了具体的实例所存放的地址。 这种方式的好处是 reference 变动少,较为稳定。 当对象实例地址改变时,只需要改变句柄中的对象实际的指针。
以指针直接访问:
hotspot 采用该方式,通过reference直接指向对象实例,有点是速度快,因为句柄访问方式少了一次寻址的过程。
结束语: 类的加载与对象的创建,平行思考下来大家要搞的事情都是创建为主,各有各的创建原则。
类的加载更多的是考虑 class from 、check class,class load。
对象的创建更多考虑的是 from which class 、how to allocate 、how to gc 。