目录
类加载
当我们去new Object()的时候,首先先判断当前的类的父类是否被加载过,如果没有则先加载父类。然后再加载当前类。将类的信息放入元空间
1、先加载父类
2、再加载本类
而类的加载分为以下几步:
- 加载:就是将类的信息加载到元空间,将静态常量池变成运行时常量池,同时在元空间生成c++语言的class对象,同时在堆空间生成一个java语音的class对象
- 链接
- 验证。校验当前类文件是否符合java规范
- 准备。为静态变量开辟空间,并赋予默认值。
- 解析。将符号引用变为地址引用。这块后续再说
- 初始化:开始按照代码声明的顺序为静态变量赋值,同时执行static代码块
内存分配
一般来讲在jvm中,创建对象时,会在堆中分配空间,而分配方式有两种:
- 指针碰撞:在内存非常整洁的时候,采用这种,即有一个指针在记录最后一个地址,当需要分配空间的时候,将指针想后移动。这种分配方式适用于标记整理、标记复制的gc垃圾器。因为这种gc之后,内存基本上都是一块正片的。
- 内存分配表:这种分配方式适合存在垃圾碎片的。即利用空闲列表来记录哪一块内存空闲,那么就可以直接分配。
不管是那种内存分配方式,都会存在并发的问题,例如有多个线程同时new对象,那么就会有并发的问题。那么如何解决呢?
- cas:这个就相当于每次来了就会进去cas分配
- TLAB:在eden区,为每个线程都单独分配一块空间,每个线程都在自己的TLAB空间内分配空间。但是这个TLAB的空间不会太大,默认是eden区的1%,可以通过-XX:TLABSize来配置。如何TLAB的空间放不下了,那么就恢复成CAS的方式,去向公共空间分配。
对象结构
一个java对象的结构大体分为三个部分:
- markword:对象头(32位、64位)。根据当前对象的锁状态不同,markword的每个bit位的标志是不一样的
![]()
- 有上面的图看出来,利用3个bit表示锁信息。其中1个bit表示是否偏向,2个bit表示锁级别。
- 01-无锁。当01-无锁的时候,需要根据是否偏向也确定当前的对象的锁状态,并且记录线程id。
- 00-轻量级。00-轻量级的时候,记录栈中锁记录的地址。
- 10重量级。10-重量级的时候,指向互斥量monitor对象的地址。
- klass point:(4B或者8B)指向当前对象对应的类元信息(在方法区)。之所以有4B和8B主要是因为是否开启了指针压缩。-XX:+UseCompressedOops【开启压缩,默认】 -XX:-UseCompressedOops【禁止压缩】
- 解释一下什么是指针压缩,为什么要压缩。正常我们用4个B去表示klasspointer,那么4个B对应的是32位,32位正好是4Gb,也就是说如果我们jvm的堆空间在4G以内,那么4B正好能够完全覆盖
- 但是如果堆内存大于4G,我们就需要33位或者34位,甚至35位。那么我们用4B就不能完全表示了,那么就需要用8B去表示。
- 假如我们用8B去表示,那么对象的空间就会严重放大,jvm成千上万的对象都会膨胀。因此我们可以采用算法,用4B来表示大于4G的空间,这个就叫压缩。
- 但是压缩一定是有失真的,如果我们强制用4B表示100T的空间,那一定会有问题。因此,如果我们使用指针压缩,那么堆空间不易过大,最好不要超过32G。
- 也就是说当我们堆空间比较大的时候,不要开启指针压缩,因为压缩之后,失真了,会有问题。
- 数组长度:只有数组对象才会有
- 实例数据:主要是存储当前对象中成员变量,例如如果有一个int类型的的成员变量a,那么将会分配4个字节。如果有一个long类型的变量b,那么会分配8个字节。如果有一个object类型的对象,则分配一个4字节
- 补齐空间。如果当前对象的所占空间不是8个的倍数,那么就进行空间补齐。这块和内存页加载页有关系。
如果想要查看当前对象的头数据,可以引入jol包
对象分配
对象的分配是有具体步骤的:
- 首先尝试在栈上分配,这样空间就会随着线程的回收而回收,但是不一定所有的对象都需要再栈上分配,是有前提条件的:
- 逃逸分析。主要判断当前对象是否逃出了函数的作用域,如果没有逃出,那么可以进行栈上分配。jvm默认开了逃逸分析
- 标量替换。所谓标量替换指的是,但是在栈上分配对象的时候,栈上没有足够的整块的空间,而是碎片空间,因为可以将一个对象打散,存放在不同的位置上。
- 判断当前对象是否足够大【可以用一个参数-XX:PretenureSizeThreshold】,如果超过了这个阈值,则直接放入老年代。但是这个参数只有在ParNew回收器有效
- 如果没有超过,则首先在TLAB上进行分配
- 如果TLAB上没有足够的空间则在eden区其他空间进行分配,此时就需要cas来进行控制并发
- 如果eden不够,则发生ygc。当对象的年龄超过15,则需要晋升到old区,当然这个15是可以通过参数-XX:MaxTenuringThreshold来设置,但是最大也就是15,因为对象头的markword中只通过4个bit来存储
- 上述ygc的时候会有一个【动态年龄】的问题。所谓的动态年龄是指,当eden区向s区copy的时候,将这批对象按照年龄由低到高排序,然后从age1+age2+……+agen的空间大于s区的50%,则age-n以及大于age-n的对象将被送往老年代。
- 因此这块有一个问题,比如我们设置s1的空间为100m,如果有60m的对象进入s区,那么可能导致这批对象直接进入老年代,下次ygc就不会被回收,那么久而久之就会导致fullgc。
- 因此如果我们调整一下s1的空间为150m,那么就不会进入老年代,那么效率就十分高了
- 老年代担保。【其实就是提前计算,防止动态年龄导致fullgc,不如提前fullgc】
- 当执行ygc之前,jvm有一个判断,校验当前old区的剩余空间是否能容得下eden的当前所有对象(包含垃圾)。
- 如果说能容得下,那么就正常走ygc。这块主要是怕如果直接ygc,然后触发了动态年龄,但是老年代又不够,那么就fullgc了吗。所以jvm会提前判断 ,只有在old区够用的情况下才正常ygc,
- 如果容不下,会判断历史的平均【eden区ygc之后剩余的对象容量】是否能被old区容下, 如果能容下,则直接ygc,如果容不下,则直接fullgc。
垃圾对象判断
第一种是引用计数,这个基本没啥用,python好像用了。
第二种是可达性分析。这块主要是从gcroot开始进行遍历。但是这快设计到ygc和fullgc,不同的实现方式,里面涉及到card表。
- 栈中本地变量引用的对象
- class的静态变量引用的对象
- 本地方法栈引用的对象
- 还有一些锁对象
finalize的作用
当gc发生的时候,对象如果被标记为了垃圾,则判断当前对象是否复写了finalize,如果没有复写,则直接清除,如果复写了,则将对象放入到一个队列中,然后又gc线程去执行队列中的对象的finalize方法。如果在finalize方法中,当前对象又和非垃圾对象关联上,则当前对象复活。
Klass的回收
klass的回收指的是回收方法区中的klass信息。那么什么情况下一个klass应该被回收呢?
- 首先该klass的所有对象实例都被回收掉。
- 当前klass的classloader也被回收了。
- 当前klass的class对象也有被引用了