JVM超长篇幅-持续刷新更新

254 阅读11分钟

Java运行时数据区域

链接:附带备注

image.png 运行时数据区域划分.png

对象创建内存布局及访问定位

对象的创建

链接:附带备注 image.png

对象的内存布局

链接:附带备注

image.png

对象访问定位

链接:附带备注

image.png

垃圾收集器与内存分配策略

垃圾收集算法&自动内存管理

1:引用计数算法

在对象中添加一个引用计数器,如果对象有引用,那么这个数字就+1 如果 如果减少一个对象引用这个数字就-1 当引用技术这个数字为0的时候就需要进行

缺点: 当两个对象相互引用的时候,没办法进行回收

2:可达性分析算法

根据GCROOT 的根对象作为起始节点,从这些节点开始根据引用关系向下搜索 ,搜索过程所走过的路径叫做引用链相连,如果某个独享GC ROOT 没有任何引用想关联,那么就证明这个对象不能在被使用了

3:引用的定义

传统定义:当一个reference类型中,存储的数值,代表另一块内存的其实地址,那么就称为该reference数据代表某一块内存,某个对象的引用

强引用:代码之中普遍存在的引用复制 obj a = new Object();这种永远不会被垃圾收集器收集

软引用:还有用,但非必须的对象,在内存溢出前会被清理

弱引用:用来描述一些非必须的对象,比软引用还要若一些,当垃圾收集器开始工作时无论怎样都会被清理

虚引用:幽灵/幻影 引用最若的一种关系,对一个对象毫无影响,并且无法通过虚引用创建对象,目的仅是为了在垃圾收集时得到一个通知

4:死亡的标记次数

两次标记,第一次标记并不会被直接清楚,只有在第二次标记时刚对象没有腹泻finalize方法,或者该方法已经被执行了才会被清除

finalize 确定为有必要执行,那么该对象就会被放在F-Quere的队列中,由虚拟机创建的一条低优先级的线程Finalizer

finalize 只会被执行一次。

5:回收方法区

常量的回收:当前系如中没有任何一个对象值的字符串常量是 “java(随意)”,且虚拟机没有其他的引用这个字面量, 在发生内存回收时,根据垃圾收集器判定是否回收该字面量,如果有必要那么将被移除

类的回收:

该类所有的实例都已经被回收,并且所有的派生子类也被回收

加载该类的类加载器已经被回收,

该类对应的java.lang.Class 没有任何地方被引用 ,无法在任何地方通过反射获取该类对象

6:跨代引用

当发生GC时,一些老年代里面的对应引用这年轻代里面的对象,那么此时为了判断年轻代里面的对象是否被引用,还需要去老年代里进行GCROOT遍历判断,这样的消耗是十分没有必要的,出现了记忆集,是把老年代划分为若干小块,标识出哪一块会被GC所引用,这样做以后当放生Minor GC 时,只会扫描有被引用的老年代,虽然会增加一些运行时开销,但是比扫描老年代还是更划算的

7:标记清除算法

先进行标记,然后在进行清除,效率比较高和 内存里面的垃圾对象成正比

缺点:有碎片化的问题

8:标记复制算法

将垃圾进行标记,然后复制到另外一个半区,同时也整理好了,然后清除原来的整个半区, 这种算法直接把内存容量降低了一般,会频繁发生GC 适用于年轻代,因为年轻代,基本上存活的对象不会到百分之十,

缺点:浪费内存,当垃圾较多时效率低下

9:标记整理算法

先进行标记清除,然后在进行整理, 有较多的复制算法,可以有效的节省内存空间

缺点:复制比较耗费性能,效率低下

HotSpot算法细节实现

1:根节点枚举

为什么垃圾回收时必须要停止用户线程?
我们可以从可达性分析算法中也就是GC Roots集合寻找引用链,来讲述一下HotSpot是如何实现高性能的根节点枚举,迄今为止所有的虚拟机在GC Root阶段都是会暂停用户线程,发生STW机制的,因其根节点枚举时,必须要保证其一致性,也就是说让整个系统看起来时静止的,或者说快照,若这点无法保证,那么也就无法保证其结果的准确性。

因其GC Root影响过大,有什么办法可以减少其停顿吗?
其实HotSpot会有某种方法来获取存在这某个对象的引用的。HotSpot的解决方法是,使用了一组名为OopMap的数据结构来达到整个目的的. 一旦类完成了加载HotSpot就会把什么对象上是什么类型的数据计算出来,在特定的位置上栈里的寄存器里哪些位置是引用,这样就可以做到不需要从方法区等GC Roots开始查找,就可以知道哪些类的实例,是否还有引用,引用是否更新等信息.

伪代码: 这段代码指明:EBX寄存器和栈中偏移量为16的内存区域中各有一个普通对象指针Oop的引用 (有效代码 从mov -> hlt )

0x026eb7a9: call 0x0283e0 : OopMap{ebx=Oop [16] =Oop off = 142}

2:安全点

如果为每一条指令都生成对应的OoMap其代价是更多的额外存储空间?
其实HotSpot并没有为每一条可以产生OopMap的指令生成,而是在特定的位置记录了这些信息,这些位置被称之为安全点. 有了安全点的设定,那么就不能随时随地进行垃圾收集,也就是说只有在到达安全点时才可以进行根节点枚举,因此安全点的全用不能太少以至于让收集器等待的时间过长,也不能太频繁以至于增大运行时内存符合,所以安全点的选用需要以: 是否让程序长时间执行的特征为标准进行选取的. 而长时间的执行的明显特征就是:指令使用,例如方法调用,循环跳转,异常跳转,等都属于指令序列的复用,所以只有这些地方才会产生安全点.

到了安全点我们该如何进行中断?
抢先式中断(preemptive Suspnsion):在发生垃圾收集时,首先把所有的用户线程进行中断,如果发现有线程不在安全点上,就恢复这条线程,让他一会在重新终端,直到他跑到安全点上时,其实现在没有虚拟机采用抢先中断.

主动式中断(Voluntary Suspension):当发生垃圾收集时不对线程进行直接操作,设置一个简单的标识,各个线程执行时去轮询整个标志,一旦发现中断标识为真时,就在离自己最近的安全点上挂起,轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其需要在Java堆上分配内存的地方,这是为了检查当垃圾收集发生时是否有主够的内存,避免没有内存分配新的对象.

3:安全区域

当用户下线程不处理,垃圾收集指令时怎么办?
什么情况下用户会不处理垃圾收集的指令,当用户线程处于Sleep 状态或者 Block状态这是线程无法响应虚拟机的中断请求,不能在走到安全地方中断挂起自己,而对于这种情况就需要要引入(safe Region)安全区域整个概念来进行解决.

什么是安全区域
安全区域是指,确保在某一段代码中,引用关系不会发生变化,因此在整个区域中任意开始垃圾收集都是安全的,其实我们也可以把安全区域看成是被扩展延申的安全点:

当用户线程到安全区域是,第一件事标识自己已经在安全区域了说明已经要开始GC-Root了,当这段时间内如果发生垃圾收集时,那么就不需要管在安全区域的线程了. 线程从安全区域出来时需要进行判断是否完成GC Root(或其他垃圾收集操作需要暂停用户线程的操作) 如果完成则出去当什么事情都没有发生,

4:记忆集和卡表

当年轻代发送Mainger Gc时需要扫描到老年代怎么办?
在新生代创建一个名为记忆集的数据结构,用以避免把整个老年代加入GC Roots的扫描范围,其实也不知是新生代有,例如部分区域收集或者G1 ZGC的收集器也有这种问题,收集器只需要通过记忆集判断非收集区域是否有指向收集区域的指针就可以了,并不需要了解这些指针的细节问题当然这些记忆集也可以选择记忆的精度 比如(字长精度:记录精确到一个机器的字长 对象精度:记录精确到一个对象 卡精度:记录精确到一个区域(第三种卡精度其实就是接下来所要说的卡表))

什么是卡表
卡表其实就是记忆集的一种具体实现,其结构类似Java中的Map结构,记录了记忆精度和堆内存映射的关系,当然卡表最简单的记录精度时字节数组,当然HotSpot也是这样做的,一个卡表对应一个卡页,而卡页的大小就是2的9次幂字节也就是512字节大小,卡页通常来说有(一个)多个对象存在跨代指针,那么就将卡页的标识标记为1,没有就标识为0,所以当发生垃圾收集时,只需要扫描卡页变为1的将他们加入筛选就可以了. image.png

5:写屏障

卡页是什么时候变脏的?
变脏的原则,应该是引用类型字段赋值的那一刻,但问题是如何变脏的. 其实写屏障类似于AOP的切面,是使用,有着可以操作的写前域和写后域,当前卡页是在数据更新后才变脏的,所以卡页是使用到了写后屏障.

应用写屏障,虚拟机就会为所有赋值操作执行生成响应指令,一旦收集器在写屏障中增加更新卡表的操作,无论更新的是老年代或者年轻代或者块都会增加额外的开销.不过这个开销和Minor GC时扫描整个老年代的代价还是低的多的

卡表在高并发下线程安全吗?
答案当然时否定的,卡表在高并发下存在伪共享,现在的中央处理器是以缓存行(Cache Line)为单位的,如果当多个变量修改相互独立的变量时,如果这些变量刚好存在同一个缓存行,就会彼此影响,(写无效,无效化或者同步)导致性能降低,如果解决呢?

解决方法:当该卡表元素未被标记时才将其标记未变脏的数据,即下行代码

if(CARD_TABLE[this address >> 9 ]!=1){
   CARD_TABLE[this address >> 9 ]=1;
}

为什么会存在伪共享
Cache Line 伪共享问题,就是由多个 CPU 上的多个线程同时修改自己的变量引发的。这些变量表面上是不同的变量,但是实际上却存储在同一条 Cache Line 里。 在这种情况下,由于 Cache 一致性协议,两个处理器都存储有相同的 Cache Line 拷贝的前提下,本地 CPU 变量的修改会导致本地 Cache Line 变成 Modified 状态,然后在其它共享此 Cache Line 的 CPU 上,引发 Cache Line 的 Invaidate 操作,导致 Cache Line 变为 Invalidate 状态,从而使 Cache Line 再次被访问时,发生本地 Cache Miss,从而伤害到应用的性能。在此场景下,多个线程在不同的 CPU 上高频反复访问这种 Cache Line 伪共享的变量,则会因 Cache 颠簸引发严重的性能问题。

下图即为两个线程间的 Cache Line 伪共享问题的示意图,