jvm总结1

JVM与锁(总结)

JVM

JVM内存结构

在这里插入图片描述
在这里插入图片描述

	JVM类似于计算器内存结构,也是由主内存、工作内存及内存一致性协议等构成。  
	所有的工作基本在每个线程的工作内存中进行处理。变量会从主存中进行复制到工作内存处理,
然后再次刷回主内存。
A:这样就会存在,多个线程对一个共享变量的操作出现不一致现象。也就是多线程编程的一个需要解决的问题。
Q:JVM提供了多种方式去解决此类问题。如voliate 、Synchronized、JDK中的concurrent包中的AQS和atomic包等。

JVM运行期数据区

在这里插入图片描述

JVM运行时数据区如上,JVM规范中分为 java虚拟机栈、本地方法栈、堆、方法区(1.8元空间)、程序计数器五大块。

hotspot JVM厂商将 虚拟机栈与本地方法栈进行合并实现为栈,线程独享;将方法区由1.7之前的永久代变为1.8之后的元空间(又名持久代,主要包括类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已确定的符号引用和虚方法表及JIT优化编译信息),此区域线程共享,常量池已经移动至堆中,堆的大方向特点为线程共享区,当然也会有部分为了线程创建时申请堆内存耗费时间,此时会有私有的一个堆空间即TALB,在读取时是线程共享的,但是在内存分配上是线程独享的。程序计数器为线程独享,会记录栈的出入点,在cpu切换时间片等记录。

hotspot将虚拟机栈及本地方法栈(调用本地方法即native方法)合并实现
栈由栈桢组成,栈是线程独享的,为先进后出的数据结构。
栈帧的结构分为“局部变量表、操作数栈、动态链接、方法出口”几个部分。
通常说的栈内存指的是虚拟机栈的栈帧中的局部变量表,因为这里存放了一个方法的所有局部变量。
方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁。
如果不断压栈,但是却不出栈,会导致栈内存不足从而导致StackOverflowError(单个栈内存不够)、
或者OutOfMemoryError(栈总内存不足)问题。例如没有方法出口的递归等。
JVM相关参数
-Xss:指定单个线程虚拟机栈大小
栈在对象使用synchronize锁的轻量级锁及重量级锁后会存储相关信息。即LockRecord

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-87ciQykH-1591176620747)(evernotecid://004FD3D9-9B16-4F33-9AF9-29EB79581FB9/appyinxiangcom/18866256/ENResource/p253)]

涉及到堆,会有内存分配、垃圾收集、优化参数设置等
内存分配
内存分配:
堆整体会被分为年轻代、老年代
年轻代会被分为eden伊甸园区、fromSurvivor区、toSurvivor区。
垃圾收集
	何为垃圾,如何识别垃圾。
	垃圾即为创造后完成了某种用途后,不再有使用价值,但是还在占用资源。
	例如生活中,你买个伊利火炬,还是原味儿的,肯定是原味儿的。当你吃完后,包装袋还在手里。
这个包装袋就是垃圾,不再有使用价值而且一直会在你手里。你就需要收集处理掉。
	java中,当一个对象使用完毕,并且不再有其他地方引用后,它不经过处理会一直占用内存空间,导致JVM内存不足,也需要进行收集。不过java语言中垃圾的回收会由JVM进行处理,从而减轻开发人员的负担。
对象引用类型
java中有四种引用类型。
强引用  
一般 A a = new A();此类都是强引用,
在拥有强引用时,GC是不会主动进行垃圾收集,除非使用完毕后进行了a=null;操作。
可以看ArrayList的clear方法会将数组直接使用null赋值,等待GC回收。


软引用
在Java中用java.lang.ref.SoftReference类来表示,
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();
	如上所示,sf就会保留一个obj的软引用,当内存充足无需gc时,软引用使用get方法可以获取到,
但是内存不足前时,会对此关联的对象进行回收及开始回收obj,并且此时obj并没有赋值null。
其可以和引用队列进行合并使用,引用队列有两个构造函数,一个带quene一个不带quene,而如果不带的话,就只有不断地轮询reference对象,通过判断里面的get是否返回null( phantomReference对象不能这样作,其get始终返回null,因此它只有带queue的构造函数 )。
	这两种方法均有相应的使用场景,取决于实际的应用。
	如weakHashMap中就选择去查询queue的数据,来判定是否有对象将被回收。
	而ThreadLocalMap,则采用判断get()是否为null来作处理,key是一个弱引用,value是一个强引用。


弱引用
弱引用和软引用的区别在于:弱引用的对象具有更短暂的生命周期。在垃圾回收时,一旦发现了只具有弱引
用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线
程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟
机就会把这个弱引用加入到与之关联的引用队列中。


虚引用
即没有什么实际意义的引用,并不会对对象声名周期产生影响
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,
因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mm6w9ocG-1591176620749)(evernotecid://004FD3D9-9B16-4F33-9AF9-29EB79581FB9/appyinxiangcom/18866256/ENResource/p256)]

如何确认是否为垃圾(无用对象)
一般会有两种做法,一种是引用计数法,一种为可达性分析法。
引用计数法,即每个对象记录自己的引用数,简单但是无法解决循环引用问题。
可达性分析法。即寻找根节点,然后从根节点查找其引用.OopMap结构。可作为根节点的
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
 
	根据java数据特点,大多数对象会在方法结束后失去意义。意味着只有少量对象会需要进行复用。这样就采取了内存的分区。

在内存分区上,根据不同分区的特性及运作流程。例如对象会先进入到eden区,当该区域内存不足时,会进  
行MGC,对还存活的对象规整到survivor区,并且如果from区已经有对象,会也参与GC,最后将from 和to进  
行调换,形成一个空的survivor区。此时会采取复制算法。内存小则造成的STW会少。当然也可以设置一些大
对象直接进入老年代。老年代的垃圾收集一般会进行标记,然后清除或者整理。只进行清除会造成内存碎片
过多,所以优先会使用标记整理算法。
垃圾回收会造成STW,但是不同的垃圾回收器会尽量减少停顿时间,增强收集效率。
常见的垃圾回收器:
serial serialOld 
为单线程年轻代和老年代收集器。串行收集器,jvm运行在client模式下的默认配置。
开启配置 -XX:+SerialGC
Parallel Scavenge + Parallel Old 并行收集器,
年轻代使用复制算法,老年代使用标记-整理算法,jvm运行在server模式下的默认配置。开启配置开启选
项:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)
 ParNew + CMS + Serial Old 并发标记清楚收集器,
 开启选项:-XX:+UseConcMarkSweepGC,cms和serialOld都是针对老年代的收集。
 G1收集器
 开启选项:-XX:+UseG1GC
 参考资料:[https://blog.csdn.net/renfufei/article/details/41897113]
 CMS收集器:cms收集器是jvm堆中老年代的垃圾收集器,其并发执行时,必须在老年代内存未用尽前完成,
 如果无法完成,会降级为串行收集器及serialOld收集器进行垃圾回收。此时就会造成长时间的STW。cms收
 集器是并发清楚算法,在老年代会造成大量内存碎片,此时老年代内存会被耗尽,会触发担保机制,对内存
 进行压缩。其提供了jvm参数支持-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指
 定多少次CMS收集之后,进行一次压缩的Full GC。
 cms收集器过程:
 初始标记、并发标记、预清理、可中断预清理、最终(重新)标记、并发清除、并发重置。
 其中初始标记和重新标记都会导致STW来对根对象进行记录。已经优化为多线程执行。
 
 并发收集问题,即如何在并发收集时,再次产生新的引用链后正确进行回收及对于浮动对象的正确回收。
三色标记法
 在并发标记时,通常使用三色标记法。
 白色:对象尚未被垃圾回收器访问过,
 黑色:对象已经被垃圾回收器访问过,并且所有对象的所有引用都被扫描过,
 灰色:已经被垃圾回收器访问过,但至少存在一个引用没有被扫描过。
 当且以下两个条件同事满足时,才会出现对象消失问题。
 1.赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
 2.同时,赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
增量更新法和原始快照法
 在解决并发标记时,正确清理无效对象中,有增量更新法和原始快照法。
 cms是采用增量更新发进行的标记。当黑色对象插入了新的引用到白色对象时,会将新插入的引用记录下来,并发扫描结束后,重新再次扫描该黑色对象。
 G1采用原始快照法。原始快照不会变动,但是会记录引用删除关系。

参考链接:
头条-why技术,微信公众号-why技术

头条-why技术-微信号why技术-可达性分析法

参数设置:
-Xms JVM启动时分配的堆内存
-Xmm JVM运行过程中最大堆内存
一般两者会设置为一样和最大堆内存一致。当服务启动时,会产生大量对象,可能会导致频繁gc从而启动过慢,所以可以采取预先设置即为最大堆内存,一次性申请完毕。
-Xmn 设置年轻代大小,但是目前一般会采取比例设置,如下
-XX:NewRatio=4 即年轻代大小占总内存大小的五分之一。
-XX:SurvivorRatio=4 设置年轻代中 eden区与survivor的比例,survivor有两个,即eden区占年轻代4/6,单个survivor占1/6。
-XX:MaxTenuringThreshold=15 即在年轻代年龄即经过了多少次回收,再进入老年代。
-XX:MaxPermSize=16m 设置持久代

JDK8 markword实现表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OBdWnMkK-1591176620750)(evernotecid://004FD3D9-9B16-4F33-9AF9-29EB79581FB9/appyinxiangcom/18866256/ENResource/p259)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VGWxeEaE-1591176620751)(evernotecid://004FD3D9-9B16-4F33-9AF9-29EB79581FB9/appyinxiangcom/18866256/ENResource/p260)]

自旋锁什么时候升级为重量级锁?

为什么有自旋锁还需要重量级锁?

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开

new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁

synchronized优化的过程和markword息息相关

用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位

  1. Object o = new Object()
    锁 = 0 01 无锁态
    注意:如果偏向锁打开,默认是匿名偏向状态

  2. o.hashCode()
    001 + hashcode

    00000001 10101101 00110100 00110110
    01011001 00000000 00000000 00000000
    

    little endian big endian

    00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000

  3. 默认synchronized(o)
    00 -> 轻量级锁
    默认情况 偏向锁有个时延,默认是4秒
    why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

    -XX:BiasedLockingStartupDelay=0
    
  4. 如果设定上述参数
    new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock
    打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101

  5. 如果有线程上锁
    上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
    偏向锁不可重偏向 批量偏向 批量撤销

  6. 如果有线程竞争
    撤销偏向锁,升级轻量级锁
    线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁

  7. 如果竞争加剧
    竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
    升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间

(以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)

偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数

如果计算过对象的hashCode,则对象无法进入偏向状态!

轻量级锁重量级锁的hashCode存在与什么地方?

答案:线程栈中,轻量级锁的LR中,或是代表重量级锁的ObjectMonitor的成员中

关于epoch: (不重要)

批量重偏向与批量撤销渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

原理以class为单位,为每个class维护解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

加锁,指的是锁定对象

锁升级的过程

JDK较早的版本 OS的资源 互斥量 用户态 -> 内核态的转换 重量级 效率比较低

现代版本进行了优化

无锁 - 偏向锁 -轻量级锁(自旋锁)-重量级锁

偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁

有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁

自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin

自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。

锁重入

sychronized是可重入锁

重入次数必须记录,因为要解锁几次必须得对应

偏向锁 自旋锁 -> 线程栈 -> LR + 1

重量级锁 -> ? ObjectMonitor字段上

synchronized最底层实现


public class T {
    static volatile int i = 0;
    
    public static void n() { i++; }
    
    public static synchronized void m() {}
    
    publics static void main(String[] args) {
        for(int j=0; j<1000_000; j++) {
            m();
            n();
        }
    }
}

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly T

C1 Compile Level 1 (一级优化)

C2 Compile Level 2 (二级优化)

找到m() n()方法的汇编码,会看到 lock comxchg …指令

synchronized vs Lock (CAS)

 在高争用 高耗时的环境下synchronized效率更高
 在低争用 低耗时的环境下CAS效率更高
 synchronized到重量级之后是等待队列(不消耗CPU)
 CAS(等待期间消耗CPU)
 
 CAS : lock cmpxchg
 Synchronized : lock comxchg
 
 一切以实测为准

锁消除 lock eliminate

public void add(String str1,String str2){
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
}

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

锁粗化 lock coarsening

public String test(String str){
       
       int i = 0;
       StringBuffer sb = new StringBuffer():
       while(i < 100){
           sb.append(str);
           i++;
       }
       return sb.toString():
}

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

锁降级(不重要)

https://www.zhihu.com/question/63859501

其实,只被VMThread访问,降级也就没啥意义了。所以可以简单认为锁降级不存在!

超线程

一个ALU + 两组Registers + PC

参考资料

http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值