Java第五篇:结合jdk版本学习和理解关于java内存管理机制,java虚拟机,java内存模型,垃圾回收机制和回收算法、内存调优等java基础

java内存管理机制就是指java如何对内存空间进行分配和回收管理的一套规则。

首先要明白java虚拟机的构成。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分成为若干个不同的数据区域,如下图所示(图片来源于网络):

1 方法区(Method Area)

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息比如版本信息,方法描述,字段描述、final常量、静态变量、即时编译器编译后的代码等数据

方法区在1.7之前有一个运行时常量池(为什么叫运行时常量池,就是因为在class文件被加载进了内存之后,常量池才会存在方法区中,也有种叫它--“永久代”,因为方法区是用永久代实现的)存在于class中的常量池并非固定不变的,可以用intern方法加入新的。

设置的参数-XX:PermSize=10M -XX:MaxPermSize=10M可以调节方法区的大小。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

jdk1.7之后开始逐步去“永久代”,但是并没有完全移除,而是将常量池移到堆中,其中符号引用转移到了native heap,字面量和类的静态变量转移到了java heap,直到JDK8才完全将永久代移除,取而代之的是元空间(Metaspace);

如果你还想了解多一点,参考大佬的解释:

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

https://blog.csdn.net/q5706503/article/details/84640762

https://blog.csdn.net/weixin_39460458/article/details/79982765

2.Java堆(Java Heap)

       Java堆是虚拟机所管理的内存最大的一块。同样是被所有线程共享的一块内存区域,在虚拟机启动时创建。

      Java虚拟机规范描述是:所有的对象实例以及数组都在堆上分配,但是随着JIT编译器的发展与逃逸技术分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所以所有对象在堆上也渐渐变得不那么“绝对”了。即此区域用来存放对象的实例,几乎所有的对象实例都在这里分配内存。在堆中分配一定的内存来保存对象实例,实际上只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。

       Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。如果堆没有完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常,如果不够可以通过配置-Xmx和-Xms来扩展大小 

      Java堆和方法区都是垃圾收集器(GC)管理的区域(而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内),因此很多时候java堆也被称作“GC堆”(Garbage Collected Heap)。讲到这里就得提一下GC是如何进行垃圾回收的。GC根据按代的垃圾回收机制将需要进行垃圾回收的区域分为三个部分--新生代、老年代和永久代。

       新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC 。

      针对新生代,java对其有专门的垃圾收集算法,叫复制算法。复制算法的原型中将内存按容量划分为大小相等的两块,每次只使用其中一块。当此块内存用完之后,将还存活着的对象复制到另一块,然后把已使用过的内存空间一次清理掉。使得每次只对其中一块内存进行回收,内存分配时不用考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。但是这样做的代价就是将内存缩小为原来的一半,比较浪费空间。

       通过上面介绍的复制算法,只要需要两个内存区域,而新生代中被划分为一个eden区和两个survivor区。绝大多数对象被创建时都都会被分配到eden区。两个survivor区同一个时刻只会使用一个,另一个保持为空。这样的设计是为了在垃圾收集过程中具有更高的效率。当新生代被充满时,会引发Minor GC ,回收时,将EdenSurvivor中还存活的对象一次性拷贝到另外一块Survivor空间,最后清理掉EdenSurvivor中空间。Minor GC 发生时JVM中所有的线程都会停止至到该事件结束(这种行为被称为stop-the-world 。它会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行)。这是为了保证垃圾收集的过程中保证数据一致性。

      虚拟机默认EdenSurvivor比例为8:1,只有10%空间被“浪费”。当Survivor空间不够用时,需依赖其他内存(此处指老年代)进行分配担保。

       老年代(Old generation):对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC。

      老年代用来存储长时间存活的对象,JVM中会设定一个阈值,以使用-XX:MacTenuringThreshold来设置该阈值。当新生代中的对象经过Minor GC而存活的次数达到该阈值后,这个对象就会被移动到老年代。同样老年代也会需要垃圾收集工作,引发Major GC 或者 Full GC。当然old GC也是Stop the World事件,但是要比新生代的垃圾回收要慢很多,因为老年代中对象的存活率很高,要保存的对象很多。

     老年代的垃圾收集使用的是标记—整理算法。复制算法在对象存活率较高时要执行较多的复制操作,效率会变低,因此不适合在老年区使用。标记—整理算法的核心思想是标记出需要回收的对象之后,不是直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 这样可以保证老年区有连续空间用以存储较大的数据。

      大多数情况下,Full GC是由Minor GC 引起,这是因为JVM的空间分配担保机制的存在。发生Minor GC 时,JVM会检测之前每次晋升到老年代对象的平均大小是否大于老年代的剩余空间大小,若大于,说明对象晋级到老年代有内存不够的危险,则直接进行一次Full GC,也就是Full GC和Minor GC 都执行,同时回收老年代和新生代的内存资源。若小于,则只会进行Minor GC,在新生代中回收内存。

      通过对新生代和老年代的采用不同的垃圾收集算法, 就可以实现JVM内存管理的高效化。
 

      永久代(Permanent generation)也称之为 方法区(Method area)(PS:上面我们说了方法区是通过永久代实现的,但是在jdk8中已被移除,而增加了元空间的概念):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:

1、所有实例被回收

2、加载该类的ClassLoader 被回收

3、Class 对象无法通过任何途径访问(包括反射)

     满足以上3个条件的无用类“可以”(不是一定会)被回收。是否对类进行回收,虚拟机提供了 -Xnoclassgc参数进行控制,还可使用-verbose:class-XX:+TraceClassLoading-XX:+TraceClassUnLoading查看类的加载和卸载信息。

      在大量使用反射、动态代理等框架的场景中,及动态生成JSP这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,保证永久代不会溢出。java虚拟机规范也没有对该部分内存的垃圾收集做规定,只是还要有垃圾回收的功能。

上面是关于在内存中对象所处的3个生命周期的回收机制,但是最重要的虚拟机如何判断一个对象是否能够被回收呢?

答案是:  依据引用来判断对象是否在使用中

PS:{

这里增加下java中引用类型

  • 1)强引用: 在程序代码中普遍存在的,类似Object obj = new Object( )这类的引用。只要强引用还存在,则垃圾收集器永远不会回收掉被引用的对象。

  • 2)软引用 SoftReference: 一些还有用,但并非必须的对象。对软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围中并进行第二次回收。若这次回收还是没有足够的内存,才会抛出内存溢出异常。

  • 3)弱引用 WeakReference: 非必须对象,强度比软引用更弱一些,被若引用关联的对象只能生存到下一次垃圾回收之前。

  • 4)虚引用 PhantomReference: 最弱的一种引用关系。无法通过虚引用来取得一个对象实例。为对象设置虚引用的唯一目的是希望在对象被垃圾收集器回收时收到一个系统通知。

}

ok,既然是通过引用来判断一个对象是否被使用,所以可以记录一个对象被引用的次数,通过引用计数算法来管理对象的引用次数,其算法原理为:给对象添加一个引用计数器,每有一个地方引用他时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器值都为0的对象就是不可能再被使用的,也就是无用的。引用计数算法实现简单,效率很高,Python等一些语言都使用了引用计数算法进行内存管理。

       但是引用计数算法对于对象之间相互循环引用问题难以解决,因此Java并没有使用引用计数算法。(虽然java不是用这种,但是还是提一下)

       那Java使用什么算法来判断对象是否被引用呢?是一种可达性分析算法,也称根搜索算法。其原理是:通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,表示该对象是不可使用的,垃圾收集器将回收其所占的内存。

      主流的商用程序语言C#、java和Lisp都使用根搜索算法进行内存管理。

      能作为GC Roots对象是JVM一定在使用的对象,在其引用链上对象也就是在使用中的对象。 Java中可作为GC Roots的对象包括两大类:

  • 栈中引用的对象

    • 虚拟机栈(栈帧中的本地变量表)中的引用的对象;

    • 本地方法栈中JNI(Native方法)的引用的对象。

  • 方法区中的对象

    • 方法区中的类静态属性引用的对象;

    • 方法区中的常量引用的对象;

       ok,上面说了java是通过跟搜索法确定一个对象是否被引用而确定是否要回收,但是它并不是简单的标记。根搜索算法中不可达对象在回收之前,要进行二次标记。第一次标记时会进行一次筛选:筛选的条件是是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或者finalize()被虚拟机调用过,则虚拟机认为没有必要执行finalize()方法。如果这个对象有必要执行finalize(),则会放在一个F-Queue的队列中,稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法进行二次标记,如果在finalize()方法中,对象重新回到引用链上(比如this赋值给其他引用链上的对象,这种情况上的对象状态叫可复活),则该对象不被回收,而移出该队列。
       注意:finalize()方法只被调用一次,如果这个对象在GC时被调用过一次finalize()方法,则第二次GC的时候,就会被判断为没有必要执行finalize()而被直接回收。
       finalize()方法是Java刚诞生时的一种妥协产物,已经不推荐使用了。finalize()的功能都可以通过try-finally更好、更及时的解决。

     好的,讲到这里垃圾回收的原理和回收机制都讲完了,但是还有一个Java收集器,这是干什么的呢?垃圾收集器,可以自动确定哪个对象不再被利用,它可以自动将它删除,能帮助开发人员减少对具体算法时间的应用,不需要深入细致的考虑实现,只需要在特定情况下选择合适的垃圾收集器帮你完成垃圾收集的工作。

     垃圾收集器的种类有很多,我在网上找了一个比较清晰明了的说明,并加以解释说明:

     新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

     老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

1.Serial收集器(复制算法)

  Serial收集器是Java SE 5 /6中默认的客户端虚拟机的收集器。在Serial收集器中新生代和老年代都是通过线性过程(使用单线程)来收集垃圾。垃圾收集的过程是Stop the World事件,所有其他的进程都必须暂停,至到其结束。简单高效的优点让它适用于对于效率要求不高的客户端虚拟机上。

      使用Serial收集器: -XX:+UseSerialGC

2.Serial Old收集器(标记-整理算法)

  老年代单线程收集器,Serial收集器的老年代版本。这个跟上面的一样,只不过这个针对的是老年代。

ParNew收集器(停止-复制算法) 

  新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。新生代复制算法,老年代复制整理算法。

      使用ParNew收集器:-XX:+UserParNewGC

3.Parallel Scavenge收集器(停止-复制算法)

  Parallel收集器是一种并行收集器,也可以认为是Serial收集器的多线程版本,在新生代收集垃圾,它的目标是追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间),而其他则是尽可能缩短用户线程的停顿时间,即有良好的相应速度提升用户体验。默认情况下,对于N核的处理器,Parallel收集器会使用N个线程来进行垃圾收集。如果是单核的处理器,即使配置为Parallel收集器,其实也是在采用单线程模式,通常双核以上的处理器才会使用Parallel收集器,以达到更好的性能,因此其适用于服务器端的虚拟机。

      收集器使用线程的数论可以通过如下命令配置:-XX:ParallelGCThreads=<desired number>

      使用Parallel Scavenge收集器:-XX:+UseParallelGC
      使用该命令,JVM将为为新生代设置Parallel收集器来收集垃圾,而老年代会任然使用单线程模式。如果希望新生代和老年代都使用多线程模式请使用如下命令:-XX:+UseParallelOldGC

4.Parallel Old收集器(停止-复制算法)

  Parallel Scavenge收集器的老年代版本

5.CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

  是一种用在老年代的收集器。为了减少回收停顿事时间,CMS收集器没有复制或压缩对象数据,而是基于“标记-清除”算法实现。 运作过程可分为4步:

  1. 初始标记,仅标记一下GC Roots能直接关联到的对象,;
  2. 并发标记,进行GC Roots Tracing的过程,在引用链上查找对象;
  3. 重新标记,修正并发标记期间,因用户程序继续运行而导致标记产出变动的那一部分对象的标记记录;
  4. 并发清除,清除无用对象数据。

      初始标记和重新标记两个步骤仍需Stop The World,且初始标较快速度很快,重新标记这阶段停顿时间一般比初始标记阶段长,但远比并发标记阶段时间短。整个过程耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

     在新生代,一般选择也选择并发式的收集器来配合CMS收集器的工作,但只有Serial和ParNew收集器能和CMS结合同时使用

CMS收集器的缺点如下:

  1. 对CPU资源非常敏感,这是并发设计通病,虽然能贴和应用程序并发执行,但是会抢占资源;

  2. 无法处理浮动垃圾,因为是多次并发标记,所以会有一些无效对象不能被及时标记出来,从而无法回收;

  3. 会产出内存碎片,因为只是标记和清理,并不压缩整理存活对象,所以会出现很多内存碎片。如果没有足够大的连续空间分配对象,就不得不暂停执行 full GC,来压缩整理内存,效率就会降低。

使用场景
CMS收集器高并发、低停顿,追求最短GC回收停顿时间的特点适用于对性能有高要求,要处理并发事件,交互性要求高的场景,如桌面UI应用,网站服务器等。

启动CMS收集器:-XX:+UseConcMarkSweepGC

设置CMS收集器所使用的线程数:-XX:ParallelCMSThreads=<n>

6.G1 收集器

Garbage First or G1收集器作为CMS收集器的替代者而被设计,并在Java7中正式登场。G1收集器取消了新生代和老年代的物理空间划分,但事实上还是一个分代收集器,G1将Java堆划分为多个大小固定的独立区域,并跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间优先回收垃圾最多的区域。 区域划分以及有优先级的区域回收,保证了G1收集器在有限时间内可获得最高收集效率。

与CMS收集器相比的两个显著改进:

  1. G1收集器基于“标记-整理”算法,不会产生空间碎片;

  2. 可非常精确的控制停顿,能让使用者明确指定在一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间不超过N毫秒,几乎是实时Java(RTSJ)的垃圾收集器特征。

由于能极力避免全区域垃圾收集,它可实现基本不牺牲吞吐量前提下完成低停顿的内存回收。更加详细的介绍还是去百度一下。

使用G1收集器:-XX:+UseG1GC

ok,最后聊一下jvm的调优和垃圾回收机制的调优

首先,是调优的思路,调优的方向总体是:低停顿、高吞吐量、少用内存资源;

低停顿:这个停顿时间指的就是前文的Stop the World事件,GC发生垃圾回收停顿的时间越短,用户体验就越好,这个在web项目或者B/S系统应用中一般都有要求。

高吞吐量:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。高吞吐量可以高效率地利用CPU时间,尽快完成运算的任务,主要适合在后台计算而不需要太多交互的任务;应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求;程序主要在后台进行计算,而不需要与用户进行太多交互;例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

少用内存资源:就是在满足前两个条件的情况下,设置最少的堆内存。堆内存越大,可以获得更大的吞吐量,但是相应的响应时间就会增加,这需要根据你项目的实际情况去调整。

jvm的调优,一般默认情况下是最好的,因为jvm有自适应调节,但是一些特殊情况下的应用还是需要手动去调节,一般调节的手段是调整jvm堆参数,选择合适的垃圾收集器。

(1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。

(2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

(3)如何平衡年轻代和老年代的大小?这个不确定,完全看你项目的需求,但是得说清楚谁大谁小的区别和会照成的影响。

  • 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
  • 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
  • 具体的如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。
  • 在抉择时应该根据以下两点:(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。

(4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集。

(5)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

(6)可以通过下面的参数打Heap Dump信息

  • -XX:HeapDumpPath

  • -XX:+PrintGCDetails

  • -XX:+PrintGCTimeStamps

  • -Xloggc:/usr/aaa/dump/heap_trace.txt

    通过下面参数可以控制OutOfMemoryError时打印堆的信息

  • -XX:+HeapDumpOnOutOfMemoryError

(7)在提供一些调优策略和准则:

  • 有些收集器能够结合使用,一般是年轻代和老年代都要有一个收集器: Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  • 当选择使用某种并行垃圾收集器时,应该指定期望的具体目标而不是指定堆的大小;让垃圾收集器自动地、动态的调整堆的大小来满足期望的行为;即堆的大小将随着垃圾收集器试图满足竞争目标而振荡;  
  • 当然有时发现问题,堆的大小、划分也是需要进行一些调整的,一般规则:除非应用程序无法接受长时间的暂停,否则可以将堆调的尽可能大一些;除非发现问题的原因在于老年代的垃圾收集或应用程序暂停次数过多,否则你应该将堆的较大部分分给年轻代;
  • 设置调整后,应该通过在产生环境下进行不断测试,来分析是否达到我们的目标;首先就是要打印GC日志,分析出现GC的频率,时间等。

(7)Linux下面查看Jvm性能信息的命令

  1. jstat: 用于查看Jvm的堆栈信息,能够查看eden,survivor,old,perm等堆区的的容量,利用率信息,对于查看系统是不是有内存泄漏以及参数设置是否合理有不错的意义。例如’’’ jstat -gc 12538 5000 —- 即会每5秒一次显示进程号为12538的java进成的GC情况 ‘’’
  2. jstack:用来查看Jvm当前的线程dump的,可以看到当前Jvm里面的线程状况,对于查找blocked线程比较有意义
  3. jmap:用来查看Jvm当前的heap dump的,可以看出当前Jvm中各种对象的数量,所占空间等等;尤其值得一提的是这个命令可以导出一份binary heap dump的bin文件,这个文件能够直接用Eclipse Memory Anayliser来分析,并找出潜在的内存泄漏的地方。
  4. 非jvm命令—netstat:通过这个命令可以看到Linux系统当前在各个端口的链接状态,比如查看数据库连接数等

关于java垃圾回收机制就讲到这里。

下面讲一下java的内存模型:(以下内容来源于简书-_fan凡-https://www.jianshu.com/p/15106e9c4bf3,个人只做记录和理解)

       java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。Java内存模型的主要目标是定义程序中变量的访问规则。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。需要注意的是这里的变量跟我们写java程序中的变量不是完全等同的。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括局部变量和方法参数(因为这是线程私有的)。这里可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。但是在堆中的变量如果在多线程中都使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了,这个后面会讲到。

 Java内存模型中涉及两个概念:

(1)主内存:java虚拟机规定所有的变量(不是程序中的变h g量)都必须在主内存中产生,可以简单认为是堆区。与物理机的主内存相比,物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。

(2)工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

PS:read和load,store和write必须成对出现。 线程在工作内存中修改了变量的值必须同步回主内存,也就是要执行assign操作,但是如果变量没有修改,不能同步回主内存。变量只能在主内存中产生。一个变量只能被一个线程加锁,但是能被同一个线程加锁多次,但是释放的是否必须同样释放多次。

        针对voliate修饰的变量,有一些特殊性。

      valatile类型的变量保证对所有线程的可见性:volatile类型的变量每次值被修改了就立即同步回主内存,每次使用时就需要从主内存重新读取值。

      volatile变量禁止指令重排序优化:在单线程中,有可能代码的执行顺序和代码顺序是不一致的,这就导致再多线程中会出现问题,比如先运行某个flag变量导致运行某个函数,然而flag变量前需要初始化信息,这就导致初始化信息为空。而volatile修饰的变量能保证变量前的代码按顺序实现。

      volatile变量的读操作和普通变量的读操作几乎没有差异,但是写操作会性能差一些,慢一些,因为要在本地代码中插入许多内存屏障指令来禁止指令重排序,保证处理器不发生代码乱序执行行为。

以上部分内容源自一下链接:

https://www.cnblogs.com/yuxiang1/archive/2019/09/10/11503159.html

https://www.cnblogs.com/alsf/p/9484826.html

https://www.jianshu.com/p/8ef543fafde1

https://www.cnblogs.com/KingIceMou/p/6967129.html

https://www.jianshu.com/p/5c3feb163d9a

https://www.imooc.com/article/34544

http://blog.itpub.net/69904796/viewspace-2565255/

https://www.jianshu.com/p/15106e9c4bf3

发布了65 篇原创文章 · 获赞 31 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/zhangtao0417/article/details/103584988