java类加载
开始先简要介绍一下class文件结构
- class文件的开头4个字节16进制为:
CA FE BA BE 称之为magic number
接下来4个字节与java版本号相关
04 05两个字节放置的是小版本号
06 07两个字节放置的是大版本号
08 09放置的是常量池大小 - 1.一般常量池索引从1开始
类加载
类加载的过程
loading -> linking -> initializing
1. loading过程就是将class文件的二进制bit流装入内存
2. linking细分为三部:verification -> preparation -> resolution
1. verification:验证字节码的magic number,等信息
2. prepartion:虚拟机为**class对象**分配内存,并且jvm会将刚分配到的内存所有位置为0(在堆上创建其他对象也是同理,因此会有int默认为0,boolean默认为false等情况),因为静态变量是属于类的不是属于某个对象的,因此类的静态变量是跟class对象绑定的,该clss对象被创建后被放置在meta space 而静态变量,常量放置在堆中常量池中。
3. resolution 就是将符号引用转化为具体的地址
3. initializing调用静态代码块赋初值
class类相当于class文件与程序之间的桥梁使得,程序中其他类更容易使用与定位到class字节码中的文件
常见的类加载器
- bootStrap:加载核心jar包,比如rt.jar,lang,charset的具体有c++代码实现
- extension:加载ext下扩展的jar包
- AppClassLoader:加载一般我们自己写的类的类加载器
- 自定义的类加载器
一般可以通过调用class类的getClassLoader()拿到加载该class类的类加载器 bootstrap类加载器加载的class类返回为null
什么是双亲委派
假如现在Appclassloader要加载一个class文件,他没有在自己的缓存中找到这个类,向上级迭代,直到找到加载这个class文件的loader或者找到到最上层,若是找到class文件则返回,否则就要从最上层也就是rootstrap开始往下一个个询问,是否愿意加载,如果愿意就加载并返回,否则,就迭代向下询问。
双亲委派的目的主要是为了安全,其次是资源利用
我们现在就来考虑一种类加载的安全问题,如果我们自定义了一个java\lang\String.java并把它打包成一个jar包,我们把它加载进内存替代原来的JDK中的String.java那么使用这个被替换的String类的危险就非常高。从资源这个角度看,如果上级类加载器已经加载过class字节码文件我们就没必要再加载一遍了。
什么是父加载器?
appclassloader加载器的类内部拥有一个名为parent的field,该值是谁那么该类加载器的父加载器是谁,他们之间没有类结构后上的继承关系,也并不是父类加载器是加载子类加载器的加载器。
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
系统的classLoader的源码与实现放在sun包下的Launcher中,classLoader类是一个抽象类
public class Launcher{
private static String bootClassPath = System.getProperty("sun.boot.class.path");
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
}
private static class BootClassPathHolder{
static{ File[] var1 = Launcher.getClassPath(Launcher.bootClassPath);}
}
static class ExtClassLoader extends URLClassLoader{
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
}
}
创建一个对象的过程
理论上我们了解当jvm创建对象的时候总是在堆上创建对象示例,
- jvm先分配大小合适的内存给示例,
2.再将分配的内存所有位置为0,
3.然后按照代码初始化对象实例。
我们通过观察一下java字节码来观察一下这个过程。
// 源代码
public class TTT {
public static void main(String[] args) {
Object o = new Object();
TTT ttt = new TTT();
}
}
// java字节码
0 new #2 <java/lang/Object>
********* 跟c++中的new同一个含义,申请一段内存:就是上述的1,2过程
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
********** 调用代码中的构造方法进行初始化,对应上述的过程3
7 astore_1
********** 将堆上的示例与栈上的变量建立联系,使得栈上的变量可以找到堆上的对象实例
// 从下边字节码可以看出 new()创建的对象步骤相同
8 new #3 <TTT>
11 dup
12 invokespecial #4 <TTT.<init>>
15 astore_2
16 return
为什么要用dup指令
一开始是new指令在堆上分配了内存并向操作数栈压入了指向这段内存的引用,之后dup指令又备份了一份,那么操作数栈顶就有两个,再后是调用invokespecial #18指令进行初始化,此时会消耗一个引用作为传给构造器的“this”参数,那么还剩下一个引用,会被astore_1指令存储到局部变量表中。
我们来深入看一下java调用init函数的过程
public class TTT {
Object o = new Object();
public TTT(){
new String("asd");
}
public static void main(String[] args) {
Object o = new Object();
TTT ttt = new TTT();
}
}
// TTT类init方法的字节码
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
/// 在一个类的构造方法中 先调用 父类的构造方法
5 new #2 <java/lang/Object>
8 dup
9 invokespecial #1 <java/lang/Object.<init>>
12 putfield #3 <TTT.o>
/// 初始化实例域
15 new #4 <java/lang/String>
18 dup
19 ldc #5 <asd> //// ldc 将常量“asd”压入栈
21 invokespecial #6 <java/lang/String.<init>>
24 pop
25 return
init函数调用过程:进入到构造函数-》调用super函数-》初始化实例域-》调用构造函数中的其他代码-》返回对象
volatile 的作用
- 禁止重排序
- 线程间可见
CAS : compare and swap 自旋锁(乐观锁)
保障原子性的一般操作:
c = c + 2
当我们进行以下操作的时候平常情况是将c的值读入到寄存器,并加上2再回写到c的内存地址。
而加上自旋锁以后的操作是:我先读c到寄存器,并记录这个原始值当作副本,然后将他的值+2,我们回写到内存的时候先使用副本与内存中该值比较是否该值发生了变化,如果没有发生变化就直接将+2后的值会写到内存,否则重新做+2操作。一直循环直到成功操作。
CAS存在的问题(ABA问题):假设c的值为0,线程A1读了c的初始值0,这个时候线程A2把c的值改为了3,紧接着线程A3又把c的值改为了0,这个时候线程A1回写发现这个值没有发生变化就重新写入c的值,但如果c处放置的是引用类型。
我们来考虑一下为什么DCL中单例fileld要加上volatile关键字
static Single volatile instance;
public static Single getInstance(){
if(instance == null){ // 2
synchronized(Single.class){
if(instance == null){
instance = new instance(); //1
}
}
}
return instance;
}
当我们不加volatile时cpu可能会进行指令重排序,我们往上寻找看构造一个对象的过程
- 申请一段全是0值的内存
- 调用构造函数,初始化
- 对象与变量之间建立引用关系
如果2,3调换了顺序,可能导致变量的引用值不为空了,但是这时的对象还没有初始化。也就是说当线程A1执行到了1代码段这个时候发生了重排序,执行了1,3指令,但是cpu的控制权发生了转移交给了A2线程,此时A2线程执行代码段2,发现不为null直接将对象返回,结果A2线程的后续操作都在一个还没有完全初始化的对象上执行,会造成未知错误。因此DCL中给instance加上volatile关键字的作用是防止指令重排序导致程序异常的发生。