前面的两个大体上就是先加载再在队中开辟空间用于存放对象。
首先说加载,分为5个步骤:
加载:
首先是类加载器,一般是由AppClassLoader,系统类加载器加载类,此时使用的是双亲委派来实现加载的,其实一开始系统类加载器是不加载类的,是交由父类加载器即扩展类加载器加载的,但是扩展类加载器一般是加载lib下的etc下的jar文件和class文件,所以如果是用户自定义的类的话那么一定不会被这个加载的,那么扩展类加载器会将其交给父类加载器即启动类加载器去加载,但是启动类加载器一般是加载lib下的jre和jre/lib目录下的核心库。所以一般不会加载用户自定义的类的。所以启动类和扩展类都加载不了那么会回退到系统类加载器上去加载。此时如果类文件存在的话那么系统类加载器一定会加载的,应为他加载的是classpath下的所有的class文件。除非是自定义的额加载器,会使用自定义的加载器来加载的。
并且现在有一个注意的就是一个加载器会将这个类所引用的或是继承的或是实现的都将由这个类的加载器来加载,除非这个类之前已经被同一个加载器或是父类加载器加载过了。所以很少会出现父类类加载器加载的类去调用子类加载器加载的类的,这种情况查下会造成失败即classnotfound。
验证
其实就是验证责怪class文件是否合乎jvm的规范,如第一个检查的就是cafebabe,然后是jdk的主次版本号,然后就是常量池的数量,常量池中的元素,类、接口、方法、属性等等。这个可以选择不认证,但是默认是开启的,需要手动关闭,这里不提供这个参数。
准备阶段
为class的静态的变量分配空间,如果只是单纯的static的那么设置的初始值是0或是这个类型的默认初始值。如果使用了final修饰了,那么就需要直接附给定的值。
解析阶段
这个是一个十分重要的阶段,以前学习的时候总是认为这是一个无足轻重的环节,但是到了后面进一部学习和分析的时候才发现这是多么的重要啊,它是将所有的符号引用转变为直接引用。转为为直接引用那么就可以直接使用了,这里才是构建一个类所具有的各种的功能的基础,这里解析了后都会放在类的常量池中,后续的类的方法中的各种的代码的执行都是需要使用这些的。类、接口、方法、属性,使用字面量+描述符来将具体的符号引用变为直接引用。
初始化
这里就是将上面的已经设置好的东西进新足以的初始化,但是这里的主要是针对静态变量。
此时这个类就已经存在于元空间中了,可以交给jvm惊醒下一步的操作了,如对象的建立,如使用new、反射等等来建立。
现在来说一说对象的创建:
空间分配
有两种主要的分配方式:1.tlab即在线程的内存中建立对象,每一个线程都拥有自己的一段空间,如果这个内存有空闲的话那么就在内存中分配,如果没有的话那么就在堆的内存中分二批局势第二个方法。2.cas失败重试,由于创建对象时对象成的,这个线程在建立对象,其他的线程有可能也在建立对象,所以需要注意的是这里的对象创建的安全性,于是依靠cas来保证安全性,如果失败了,那么再来查找位置分配内存,直到找到空间或是oom溢出。
在这里还有两个小知识:即在什么杨的环境下分配,是在连续的内存空间呢,还是在不连续的内存空间?这个就是依靠gc垃圾器来决定了。如果年轻代(一般的对象是分配在这里,除非是大对象)使用的时copy是算法(如Serial、pc、parnew),那么就是指针碰撞即一个指针前面时已分配的内存,后面时连续的空闲的内存,那么就可以放心的分配内存。如果时使用的标记清除(目前在年轻代没有看到过使用这个算法的垃圾回收器,但是老年代有,为cms),那么此时就时空闲列表,即需要在列表的每一个元素中查找可以存放的下这个对象的空间,每一个元素内部时连续的。
清零,将这个空间值0。
设置对象头
一般是两个部分,如果时数组对象的话时三个部分。即markward(8字节)和指向这个对象的类的指针(4字节,压缩后的,系统为64位)。如果时数组对象还需要一个字段来存放数组的大小为四字节。如下图所示:
public class test {
public void print(){
System.out.println("vbjdfbjdfb");
}
}
public class App
{
public int i = 10;
public int j = 20;
public static void print(int k,String string){
// System.out.println(i+""+j);
}
public static void main( String[] args )
{
test 他= new test();
// test[] t = new test[3];
// int[] t =new int[3];
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}
1-2是指markword的长度8个字节,3是指压缩后的指针的长度4个字节,4是指对其,应为我的是64位系统,所以是8字节对齐,1-3只是12字节,所以需要填充4字节,以保证可以被8字节整除。
1-2是指markword的长度8个字节,3是指压缩后的指针的长度4个字节,4是指数组的长度,5是指数组中元素的类型,可以看到上面的test的长度为12字节,但是在长度里是为3,就表示一个元素为4字节(对象而不是基本类型),如果是基本类型的话那么就是各自的字节长度如double是为8,那么这里就不是12了而是24,所以需要加上这个一共为28字节,但是不能被8字节整除所以填充4字节。
可以使用UseCompressedClassPointers参数来调节是否实现对于指针的压缩,默认时添加的,现在将其去掉即在jvm的参数里添加上-XX:-UseCompressedClassPointers即可。效果如下:
可以看到指针大小由原来的4字节变为了8字节大小。
但是奇怪的是当数组的对象头不满足8字节的整数时会出现填充的现象,但是如果 将test里添加上一个int i 并且将指针压缩打开的话的话,会呈现出一下的情况:
为什么此时没有了像数组对象里的对象头不满8字节的整数倍时的填充呢?我觉得可能是即为了长度的满足8字节的整除和更好的处理下面的数据吧。
更新:
我将test中的int变量换成了long类型的变量后就发现,其实是分情况讨论的:
1.如果使用了压缩指针,且头部 计算出来的值不是8字节的整数被那么如果下面的第一个属性(按照优先级分)如果能够放下,如char(12+2+2(填充)),byte(12+1+3(填充)),short(12+2+2(填充)),boolean(12+1+3(填充)),int(12+4),float(12+4)等等。
2.如果是使用了压缩指针后,且计算出来的头部不是8字节的倍数,且第一个属性放不下那么就会添加4字节的填充,然后再放置具体的数据。我想这个是为了数据的处理的方便吧。
下面就需要重点说一下markword的内容了,这里面的内容很重要,包含了对象的hashcode即唯一标识对象的标识符,对象的gc年龄,对象的锁及其状态。
下面的图为引用了这篇文章的以下内容:
https://blog.csdn.net/qq_26542493/article/details/90938070
分别从无锁到synchronized的偏向锁(只有一个线程)再到cas轻量级锁(多个线程)再到进入到操作系统的重量级锁(自旋此时超过一定次数),最后一个是gc来集会收起的表示,我想这个就是在垃圾回收过程中的的标记吧(自认为,还没有证实过)。
其中重量级锁的指针指向的是一个monitor Object。这个对象里包含有一个引用计数器用于锁的重用,一个等待列表,一个就绪列表。
https://www.linuxidc.com/Linux/2018-02/150798.htm
或它的转发连接:
https://blog.csdn.net/zwjyyy1203/article/details/106217887?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allfirst_rank_v2~rank_v25-1-106217887.nonecase
以下代码的出处
//结构体如下
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0, //等待者的数量
_recursions = 0; //线程的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
所得升级就是依靠对象头的markword来实现的。
如上面的test的一个对象头:
01 00 00 00 00 00 00 00(00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
对于这里并没有看太懂,java是小段的,所以最终的为00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001。即此时的hashcode为0,age为0,biased_lock(是否是片线索,0表示不是偏向锁,1为偏向锁),最后为01,表示为正常。
其中ptr_to_lock_record为cas锁至下关的栈中的方法中的锁,ptr_to_heaveweight_minitor指的是monitor对象的地址。
下面就是初始化了,初始化内部的各种属性等等。
给对象的引用赋值。
这里其实还是有一个小的知识点就是在单例的双检锁那里的instance需要使用volatile来修饰。原因就是在4,5这里会有一个指令的重排序,在cpu执行的时候将5限制性并不影响到代码的执行,相反初始化可能会比较慢,但是将地址付给引用确实很很快的,在cpu看来没什么错,哦我可以限制新5,在执行4.但是在业务逻辑上来看确实不行的,应为对象引用有值了,但是引用指向的地址中却还没有值所以会出现错误。
上面的理解还有一些偏差没有其实在markword和数组对象的对象头为什么还需要对其哪里含糊不清。
下面就是代码的执行了。
其实这里有一个知识点是jvm可以选择懒加载,即用到了采取加载,没用到的后面在加载。
执行代码
public class test {
int i=0;
public void print(){
System.out.println("test "+i);
}
}
public class App
{
public int i = 10;
public int j = 20;
public void print(int k,String string){
System.out.println(k+" "+string);
System.out.println(i+" "+j);
test t = new test();
t.print();
}
public static void main( String[] args )
{
App app = new App();
app.print(123,"123");
}
}
首先jvm先加载的是拥有main函数的类App。
由于在加载阶段就已经完成了各种函数的解析、属性的解析并将其放置到了类的常量池中,所以main函数中执行的代码如下使用javap -v app.class来查看:
这里是App.class的常量池
Constant pool:
#1 = Methodref #19.#44 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#45 // java_common_test/App.i:I
#3 = Fieldref #15.#46 // java_common_test/App.j:I
#4 = Fieldref #47.#48 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Class #49 // java/lang/StringBuilder
#6 = Methodref #5.#44 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#50 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#8 = String #51 //
#9 = Methodref #5.#52 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Methodref #5.#53 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#11 = Methodref #54.#55 // java/io/PrintStream.println:(Ljava/lang/String;)V
#12 = Class #56 // java_common_test/test
#13 = Methodref #12.#44 // java_common_test/test."<init>":()V
#14 = Methodref #12.#57 // java_common_test/test.print:()V
#15 = Class #58 // java_common_test/App
#16 = Methodref #15.#44 // java_common_test/App."<init>":()V
#17 = String #59 // 123
#18 = Methodref #15.#60 // java_common_test/App.print:(ILjava/lang/String;)V
#19 = Class #61 // java/lang/Object
#20 = Utf8 i
#21 = Utf8 I
#22 = Utf8 j
#23 = Utf8 <init>
#24 = Utf8 ()V
#25 = Utf8 Code
#26 = Utf8 LineNumberTable
#27 = Utf8 LocalVariableTable
#28 = Utf8 this
#29 = Utf8 Ljava_common_test/App;
#30 = Utf8 print
#31 = Utf8 (ILjava/lang/String;)V
#32 = Utf8 k
#33 = Utf8 string
#34 = Utf8 Ljava/lang/String;
#35 = Utf8 t
#36 = Utf8 Ljava_common_test/test;
#37 = Utf8 main
#38 = Utf8 ([Ljava/lang/String;)V
#39 = Utf8 args
#40 = Utf8 [Ljava/lang/String;
#41 = Utf8 app
#42 = Utf8 SourceFile
#43 = Utf8 App.java
#44 = NameAndType #23:#24 // "<init>":()V
#45 = NameAndType #20:#21 // i:I
#46 = NameAndType #22:#21 // j:I
#47 = Class #62 // java/lang/System
#48 = NameAndType #63:#64 // out:Ljava/io/PrintStream;
#49 = Utf8 java/lang/StringBuilder
#50 = NameAndType #65:#66 // append:(I)Ljava/lang/StringBuilder;
#51 = Utf8
#52 = NameAndType #65:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#53 = NameAndType #68:#69 // toString:()Ljava/lang/String;
#54 = Class #70 // java/io/PrintStream
#55 = NameAndType #71:#72 // println:(Ljava/lang/String;)V
#56 = Utf8 java_common_test/test
#57 = NameAndType #30:#24 // print:()V
#58 = Utf8 java_common_test/App
#59 = Utf8 123
#60 = NameAndType #30:#31 // print:(ILjava/lang/String;)V
#61 = Utf8 java/lang/Object
#62 = Utf8 java/lang/System
#63 = Utf8 out
#64 = Utf8 Ljava/io/PrintStream;
#65 = Utf8 append
#66 = Utf8 (I)Ljava/lang/StringBuilder;
#67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#68 = Utf8 toString
#69 = Utf8 ()Ljava/lang/String;
#70 = Utf8 java/io/PrintStream
#71 = Utf8 println
#72 = Utf8 (Ljava/lang/String;)
下面是main函数的代码:
0: new #15 // class java_common_test/App
3: dup
4: invokespecial #16 // Method "<init>":()V
7: astore_1
8: aload_1
9: bipush 123
11: ldc #17 // String 123
13: invokevirtual #18 // Method print:(ILjava/lang/String;)V
16: return
可以看见显示进行了类的new操作,从invokespecial #16 // Method “< init >”: ( )V可以看出来。然后是在装填调用函数的形参。调用print函数,可以看见这个函数是依靠常量池才获取到引用的,及之前的类加载过程中的解析阶段做的操作。
让我们再来看一下print函数的代码:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #5 // class java/lang/StringBuilder
6: dup
7: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
10: iload_1
11: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
14: ldc #8 // String
16: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: aload_2
20: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
29: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
32: new #5 // class java/lang/StringBuilder
35: dup
36: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
39: aload_0
40: getfield #2 // Field i:I
43: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
46: ldc #8 // String
48: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
51: aload_0
52: getfield #3 // Field j:I
55: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
58: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
61: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
64: new #12 // class java_common_test/test
67: dup
68: invokespecial #13 // Method java_common_test/test."<init>":()V
71: astore_3
72: aload_3
73: invokevirtual #14 // Method java_common_test/test.print:()V
76: return
可以看见基本上调用的都是常量池中的数据,现在可以进一步的理解了常量池的作用就是类的资源仓库,如果没有这个的话那么类的使用会变得无从下手。
从中也可以看到System.out.println(k+" "+string);内部其实使用的是StringBuilder来实现字符串的凭借的,如第一个是k第二个是” “第三个是string,最后使用toString来传给PrintStream.println。
所以最终所有的函数都会被正常的执行,因为常量池的资源已经准备好了。现在再次回过头来是不是觉得类的加载很重要了,其中验证阶段可以跳过,那么就是加载-准备-解析-初始化。
总的来说本文是为了记录下自己的理解,没有什么硬性的解释,如果错误请不吝指教,谢谢!