其中类加载机制如下:
对象创建过程解析:
1、类加载检查
类在加载过程中会首先判断该类是否已被加载,即,在方法区(元空间)上的常量池中去查找指向该类的指令,如果存在,再去判断该指令指向的类是否已经被加载、解析、初始化过,如果没有,就加载此类。
2、分配内存
当类被加载完之后,虚拟机就会给对象分配内存,此时对象的大小已经确定,虚拟机会从堆(栈)的内存区域中划分出该对象大小空间的区域供存放该对象。
1、指针碰撞:
指针碰撞时jvm虚拟机默认的给对象分配内存的方式,即:把堆(或栈)上的一部分内存分成两部分,一部分依次放满了对象,此时对象在这部分内存上是整齐排放着,另一部分是空白内存区域,等待放对象。两块区域的间隔点是用一个指针进行标记,如果在这块内存中又创建了对象,则指针会向后移动这个对象的大小的地址,供这个对象存放。
2、空闲列表:
当内存上存放的对象不是整齐规整存放的话,就会出现许多空白区域与对象交叉出现,此时就不能使用指针碰撞的方式去分配内存,空闲列表的方式就是维护一个可以存放对象的内存地址的集合列表,即记录哪些内存区域是可以存放对象的,当创建对象时,在列表中找一个空间合适的一块区域去存放,然后再更新列表。
在以上分配内存的过程中,如果在给对象a分配内存的同时指针还没来得及修改,对象b同时也使用了原来的这个指针来分配内存。就会出现一块内存分配给了两个对象,即并发问题。
解决并发问题的方式:
1、CAS
CAS即Compare and Swap,比较并交换算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。这里进行简述:利用cas方式解决创建对象的并发问题的逻辑是:对象在进行分配内存之前会将该线程期望的指针值与内存上的原有的指针值进行比较,如果相同,则先修改内存上的指针值,即向后移动该对象大小的空间,然后存放对象。如果不相同,则返回现在的指针值,然后重试以上操作。
2、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把将要存放对象的空白内存区域分成若干小块,每个线程只允许在自己的那一小块的内存上进行分配对象内存区域。
3、初始化
即给对象赋默认值。如果不赋默认值,这个对象的该字段如果不set值的话,就无法get该字段的内容,程序会出错。如果赋默认值之后,就可以返回默认值。
4、设置对象头
对象由对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)三部分组成。
对象头:包含了该对象的必要信息,不仅包括该对象的一些类元信息地址指针,还包含其他信息,比如对象的分代年龄,锁信息等。
实例数据:对象中各个实例字段的数据。
对齐填充:其中为了保持对象内存的整齐性,还包括了对齐指针来进行对对象的内存大小的填充。
5、执行init()方法
为对象赋用户自己的值,执行构造方法。
对象头的类元信息解析
如上图所示:
对象在堆中创建,其对象头中存在指向该对象的类元信息的指针,也就是说,如果对象想要找到该对象的成员方法的具体方法内容(代码),就是通过这个对象头内的指针找的。
duixiang.add();
区分对象、类对象、类元信息:
类对象:类加载完成之后,是java虚拟机为方便用户操作该类的类元信息而创建的镜像对象,里面没有该类的信息,但是通过该类对象可以访问该类的信息。存放在堆中。
class<?extends Duixiang> duixiangClass = duixiang.getClass();
类元信息:存放类的相关信息,比如变量与成员方法,在方法区中。
对象头中的指针为压缩指针,作用是节省内存空间
对象内存分配回收流程图
对象的分配与回收策略:
1.对象优先在Eden区分配
2.大对象直接进入老年代
3.长期存活的对象将进入老年代
4.动态年龄判断
5.空间分配担保
流程图解析:
1.对象在栈中分配原理
通过逃逸分析算法,判断这个对象的作用域,如果该对象只在该栈帧中调用,即方法运行完成即回收,则在栈的该线程栈的栈帧上创建,此方法运行完成,即在对应栈帧上出栈,然后立即被回收。对象在栈上创建,出栈就被回收,避免了在堆上创建,然后进行gc后回收的耗时与浪费空间。
上边说是在栈上创建对象,其实是将该对象分解为一个个标量进行创建在栈上,并没有真的在栈上创建对象,因为对象的创建需要一块连续的空间,栈上不一定有。标量即不可被再次分解的量JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
2.对象优先在Eden区分配
大多数对象都是在堆中创建的,对象会优先在Eden区中创建,因为对象大多数情况下都会很快被回收,所以survivor区空间比较小,它们的比例默认为:8:1:1。
3.大对象直接进入老年代
因为大对象在年轻代中,young gc比较频繁,所以该对象也会频繁进行复制,效率会很低,所以,大对象直接进入老年代创建。
4.长期存活的对象将进入老年代
当对象进行多次minor gc后,仍然存活,就会把该对象放入老年代,停止对该对象的频繁的minor gc,等待full gc进行回收。达到提高执行效率的目的。
5.动态年龄判断
对象分代年龄不够也可能会移到老年代。
在Eden区创建的对象,在进行minor gc后,仍然存活,但是对象所占内存比较大,大于S1和S2区总和的内存的一半,此时会直接放到老年代中。
对于一次minor gc时,即便没有单个对象大于S区总和的一半,但是这次gc后存活的对象总和仍然大于S区总和的一半,也会直接进入老年代中。
6.空间分配担保
1.虚拟机每次在进行minor gc之前,都会去判断老年代中剩余的可用连续空间是否大于整个年轻代的对象总和,确保所有的对象在进行minor gc后都能有地方去存放。
2.当1中满足情况后,会直接进行minor gc,如果不满足,虚拟机会查看是否进行了担保失败,如果担保了,就会去判断从新生代中每次进入老年代的平均大小是否小于老年代剩余的可用连续空间。
3.如果没有进行担保或者大于剩余可用连续空间,就会先进行一次full gc后,再进行minor gc。
4.如果满足,则直接进行minor gc。此时可能会遇到特殊情况,即实际这次进入到老年代的对象过多,大于平均值,也大于老年代剩余可用连续空间。此时会进行full gc,再进行minor gc。
5.虽然4中特殊情况出现后饶了一大圈子,但是,这种情况是很少的,所以大都采取这样的方式,进一步避免了频繁的full gc。
总结:何为担保机制:jvm在进行minor gc时,只有一个survivor区中只有一个区域来存放Eden区中仍存活的对象,如果在进行minor gc后,这个对应survivor区没有空间来存放大量的Eden区存活的对象,此时就需要老年代进行分配担保,即survivor区中无法存放的对象直接放入老年代。而老年代要进行此次担保,就必须保证自己有空间进行存放这些未知总大小的对象,此时,就只能与每一次进入老年代的对象的平均大小进行比较。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。