学习笔记知识点专项(JVM)
1.JVM有哪些分区/java虚拟机运行时数据区有哪些?
堆,方法区,JVM栈,本地方法栈,程序计数器。其中程序计数器,本地方法栈和虚拟机栈随线程而生,随线程而灭。
(1)程序计数器:一块较小的内存空间,作用可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要以来这个计数器完成。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序的计数器,各条线程之间的计数器不影响,独立存储,称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java方法,这个计数器记录的是在执行的虚拟机字节码指令地址;如果正在执行的是native方法,这个计数器值则为空,此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
(2)Java虚拟机栈:线程私有,生命周期与线程相同。每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至完成的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全正确的,在方法运行期间不会改变局部变量表的大小。在Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机规定所允许的最大深度,则会跑出StackOverFlow异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemory异常。
(3)本地方法栈:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务,虚拟机规范中对恩地方法栈中的方法使用的语言、使用的方法与数据结构并没有强制规定,具体的虚拟机可以自由实现它。
(4)Java堆:Java堆是被所有线程共享的一块内存区域。在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”。可以分为新生代和老年代。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以使可扩展的。如果在堆中没有内存完成实例分配,并且堆也无法扩展,则会跑出OutOfMemoryError异常。
(5)方法区:所有线程共享的一块内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将跑出OOM异常。运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口的描述文件等,还有一项是常量池。用于存放编译器生成的各种字面量和符号引用,这部分内容将放在类加载后存放到方法区的运行时常量池中。
程序计数器、虚拟机栈、本地方法栈的内存分配和回收都具备确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束后内存自然的就跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类可能需要的内存不一样,一个房中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
2.对象访问如何进行
以Object obj = new Object()为例,假设这句代码出现在方法体中,那Object obj这部分的语义会反映到Java栈的本地变量表中,作为一个reference类型数据出现。而new Object()这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查到此对象类型数据的地址信息(如对象类型,父类,实现的接口,方法等),这些类型数据都存储在方法区中。(这些类型数据的地址存放在堆中,数据存储在方法区中)。
3.对象访问方式有什么
主流的对象访问方式有两种:使用句柄和直接指针
如果使用句柄访问方式,Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的地址存放在堆中的实例池和方法区中。
如果使用直接指针访问方式,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。
使用句柄访问的最大好处就是reference 中存储的是最稳定的句柄地址,在对象被移动时只改变句柄中的实例数据指针,而reference本身不需要修改,使用直接指针访问最大好处就是速度快,它节省了一次指针定位时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是有一项很可观的执行成本。
4.内存泄漏和内存溢出的区别和联系
内存泄漏:程序向系统申请分配内存进行使用,可是使用完了以后去不归还,结果申请到的这块内存我自己不能再访问,系统也不能再次将它分配给需要的程序,内存泄漏的堆积最终会导致内存溢出。(可用的内存越来越少,申请不到足够的内存 溢出)
内存溢出:你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
5.内存泄露的分类
(1)常发性:发生内存泄露的代码会被多次执行到,每次被执行的时候都会导致一块内存执行
(2)偶发性:发生内存泄露的代码只有只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的,对于特定的韩静,偶发性的也许就变成了常发性。所以测试环境和测试方法对检测内存泄露至关重要。
(3)一次性:发生内存泄漏的代码只会被执行一次,或由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分派内存,在析构函数中却没有释放该内存。
(4)隐式 :程序在运行过程中不停地分配内存,但是直到结束的才释放。严格来说没有内存泄漏,因为最终程序释放了,但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。
6.内存溢出的原因以及解决办法
原因:
(1)内存中加载的数据量过于庞大,如一次从数据库中取出过多数据
(2)集合类中有对对象的引用,使用完后未清空,使得JVM不能回首
(3)代码中存在死循环或循环产生过多重复的对象实体
(4)使用的第三方软件中的BUG
(5)启动参数内存值设定的过小
出现OOM异常解决方案
java堆内存的OOM异常是实际应用中最常见的内存溢出的异常情况
(1)通过内存映像分析工具(Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,分清楚到底是内存泄漏还是内存溢出
(2)如果是内存泄漏,查看泄露对象到GCRoots的引用链,找到垃圾收集器无法自动回收的原因
(3)如果不存在内存泄漏,检查虚拟机的堆参数(最大值-xmx与最小值-xms),与机器物理内存对比看是否还可以调大。另外可以从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
7.java虚拟机规范中描述的两种异常
(1)StackOverFlowError异常:线程请求的栈深度大于虚拟机所允许的最大深度,抛出此异常
(2)OutOfMemoryError异常:虚拟机在扩展栈时无法申请到足够的内存空间,抛出此异常
8.判断对象是否存活的方法
(1)引用计数法:给对象中添加一个引用计数器,每当一个地方引用它时,计数器的值就加1;当引用失效时,计数器的值就减1,任何时刻计数器都为0的对象就是不可能再被使用的、引用计数算法的实现简单,效率也高,但是java语言中没有选用引用计数算法来管理内存,因为它很难解决对象之间的相互循环引用问题。如果两个对象互相引用,除此之外再无引用,实际上这两个对象已经不可能再被访问,但是它们因为相互引用着对方,导致它们的引用计数器也不能为0,所以无法通知GC收集器回收。
(2)根搜索算法:通过一系列的名为“gc roots”的对象作为起始点,从这些节点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连(图论说是不可达),则证明这个对象是不可用的。
在java中,可作为gc roots的对象包括下面几种:
①虚拟机栈中的引用的对象(本地变量表)
②方法区中的类静态属性引用的变量
③方法区中的常量引用的对象
④本地方法栈中JNI的引用的对象
9.java对引用的分类
(1)强引用:只要还存在,GC收集器就永远不会回收掉被引用的对象
(2)软引用:用来描述一些还有用但不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之内并进行第二次回收。如果这次回收还是没有足够的内存则抛出OOM。SoftReference类来实现软引用
(3)弱引用:也是用来描述非必需对象的,但是强度比软引用更弱,被弱引用的关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类来实现弱引用。
(4)虚引用:只是一个标记,希望这个对象被收集器回收时能收到一个系统通知。(phantomReference)
10.finalize()方法
任何一个对象的finalize()方法都只会被系统调用一次,不是C/C++中的析构函数,运行代价高,不确定性大,无法保证各个对象的调用顺序。
若对象在进行根搜索之后发现没有与GC roots相连接的引用链,那么他将会被第一次标记并进行一次筛选,筛选的条件是该对象是否有必要要执行finalize()方法。若对象没有重写finalize方法或者已经被JVM调用过了,则将这两种情况视为没必要。
若该对象被判定为有必要执行,则这个对象会被放在一个F-queue队列,finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-queue中的对象进行第二次小规模的标记,若对象在finalize中成功拯救自己(只要重新与引用链上的任何一个对象简历关联即可),那么在第二次标记时他们将会被移除“即将回收”集合。
11.java的垃圾回收机制
在Java中,如果没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。而负责编译的Java虚拟机JVM的一个系统级线程则自动释放该内存块。垃圾的回收意味着程序不再需要的对象是垃圾信息,会被丢弃。这一设计很好的提高了资源利用,方便了空间被后来的新对象使用。
12.垃圾收集算法有哪些
(1)标记-清除算法:分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
缺点:效率不高,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
(2)复制算法:将可用内存按容量划分为大小相等的两块,每次只是用其中一块。当这一块的内存用完了,就将还活着的对象复制到另一块上面,然后把已经使用过的那一半的内存空间一次清理掉
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价是内存缩小为原来的一半,有可能在对象存活率较高时就要执行较多的复制操作,效率变低。如果不想浪费百分之五十的内存空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都百分之百存活的极端情况,所以在老年代一般不能直接选用这种方法。
(3)标记-整理方法:根据老年代的特点,提出了标记-整理算法,标记过程与标记-清除算法案一样,但后续步骤不是直接对可回收对象进行清除,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
(4)分代收集:根据对象的存货周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用赋值算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对他进行分配担保,就必须使用标记-清理或是标记-整理算法来进行回收。
13.垃圾收集器
新生代:Serial,ParNew, Parallel Scavenge
老年代:CMS,Serial old,parallel old
都可以:G1
(1)Serial:单线程收集器,垃圾收集时必须暂停其他所有的工作线程,是虚拟机运行在Client模式下的默认新生代收集器。新生代采用复制算法,老年代采用标记-整理算法
(2)ParNew:是Serial的多线程版本,是虚拟机运行在Server模式下的首选新生代收集器。新生代采用复制算法,老年代采用标记-整理算法。除serial收集器以外,目前只有它能与CMS收集器配合工作
(3)Parallel Scavenge:并行的多线程收集器。目标是达到一个可控制的吞吐量。吞吐量= 运行用户代码时间/(运行用户代码时间+垃圾收集时间),停顿时间和吞吐量不能同时调优。
(4)Serial Old:Serial的老年版本,单线程收集器,采用标记-整理算法
(5)Parallel Old:parallel scarvenge的老年版本,使用多线程和标记-整理算法。在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scarvenge加parallel old收集器
(6)CMS收集器:一种以获取最短回收停顿时间为目标的收集器,适用于互联网站或B/S系统的服务器上。基于标记-清除算法,运作过程分为四步:①初始标记(stop the world):只是标记一下GC roots能直接关联到的对象;②并发表及(与用户线程一样):gc roots tracing的过程;③重新标记(stop the world):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;④并发清除(与用户线程一起):基于标记结果,直接清理对象。有点:并发收集,低停顿。缺点:对CPU资源非常敏感、无法处理浮动垃圾、收集结束后会长产生碎片
(7)g1:最前沿成果,基于标记-整理算法,不会产生空间碎片,可以非常精确地控制停顿。避免全区域的垃圾收集,将整个java堆分区,并根据这些区域的垃圾堆积程度维护优先列表。区域划分及优先级的区域回收,保证g1收集器的收集效率。
14.内存分配规则
(1)对象优先在Eden分配
(2)大对象直接进入老年代()
(3)长期存活的对象将进入老年代(MaxTenuringThreshold)
(4)动态对象年龄判定(有一半一样的年龄,则大于这个年龄的就直接进入老年代)
(5)空间分配担保
15.用什么工具调试程序(以下在第一遍打完没保存,重新打的,醉了。。。哭了。。)
JConsole,能够监视JVM内存的使用情况、线程堆栈跟踪、已装入的类和VM信息以及CE MBean;
16.JVM调优思路
用VisualVM及其扩展插件VisualGc采集从Eclipse启动开始,到所有插件加载完成为止的总耗时及运行状态数据。
(1)升级JDK版本
(2)优化编译时间和类加载时间(如禁止字节码验证过程)
(3)调整内存设置控制垃圾手机频率(新生代太小,老年代空间耗尽持续扩容)通过设置参数强制虚拟机在启动的时候就把老年代和永久代容量固定下来,避免运行时自动扩展
(4)选择合适的收集器
17.虚拟机的类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括了:加载,验证,准备,解析,初始化,使用和卸载七个阶段。其中验证 准备 解析三个阶段统称为连接。
18.JVM加载一个类的过程
包括加载 验证 准备 解析 初始化 五个阶段
加载阶段,虚拟机需要完成三件事情:
a、通过一个类的全限定名来获取定义此类的二进制字节流
b、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
c、在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java lang VerifyError异常或其子类异常。不同的虚拟机对类验证的实现可能会有所不同,但大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。文件格式验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个java类型信息的要求,这阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所有后面的三个阶段全部是基于方法区的存储结构进行的。元数据验证的目的是对类的元数据进行语义校验,保证不存在不符合java语言规范的元数据信息。字节码验证主要是进行数据流和控制流分析,这阶段将对类的方法进行校验分析,任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段-解析中发生。符号引用验证可以看作是对类自身以外的信息进行匹配性的校验。
准备阶段是正式为类前两分配内存并设置类变量初始值的阶段,这些内存都在方法去中分配。这个时候 内存分配的仅把偶偶类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次,这里说的初始值通常情况下是数据类型的零值。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口,字段,类方法,接口方法四类符号引用进行。
初始化阶段实质性类构造器<client>()方法的过程。
19.什么情况必须立即对类进行初始化
(1)遇到new,getstattic,putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
(3)当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化,(接口不一定)
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
20.类加载器
(1)启动类加载器:使用c++语言实现,是虚拟机的一部分,负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用。
(2)扩展类加载器:(extClassLoader)负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
(3)应用程序类加载器:appClassLoader,由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
21.类加载器的双亲委派模型
要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会是继承而是组合。
双亲委派魔性的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载器请求都会传给顶层的启动类加载器,只有当父类加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
22.java内存模型
java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存中,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的:
(1)原子性:由java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store、write六个,大致可以认为基本数据类型的访问读写是具备原子性的
(2)可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
(3)有序性:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指线程内表现为串行的语义,后半句是指指令重排序现象和工作内存与主内存同步延迟的现象。
23.内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之类的实现细节,java内存模型中定义了八种操作来完成:
(1)lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
(2)unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便以后的load动作使用
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
(7)store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
(8)write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
如果要把一个变量从主内存粗知道工作内存,那就要按照顺序执行read和load,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作,java内存模型只要求上述两个操作必须按顺序执行,而没有保证必须是连续执行。也就是说read、load之间,store、write之间是可插入其他指令的,入队主内存中的变量a,b进行访问时,一种可能出现的顺序是read a, read b, load b,load a。除此之外,java内存模型还规定了在执行上述八种基本操作时必须满足如下规则:
(1)不允许read和load。store和write之一单独出现,不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写但主内存不接收的情况出现
(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
(3)不允许一个线程无原因地(没发生任何assign)把数据从线程的工作内存同步回主内存
(4)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,换一句话就是说对一变量实施use和store之前必须先执行了assign和load
(5)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock之后必须执行相同次数的unlock操作,变量才会被解锁
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化变量的值
(7)如果一个变量事先没被lock锁定,则不允许它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量
(8)对一个变量执行unlock之前,必须先把此变量同步回主内存中(store write)
24.volatile型变量
在某些情况下,volatile同步机制的性能要优于锁,但是由于虚拟机对锁实行的许多消除和优化,使得我们很难量化地说volatile就会比synchronized快上多少,如果让volatile自己与自己比较,则可以确定一个原则:volatile变量读操作的性能消耗与普通变量几乎没有什么区别,但是写操作可能慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下volatile的总开销仍然要比锁来的低,我们在volatile与锁中选择的唯一判断依据仅仅是volatile的语义能否满足使用场景的需求。
volatile的语义有两个:一是保证被定义成volatile的变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,心智对于其他线程来说是可以立即得知的,而普通变量不能做到这一点,普通的变量在线程间传递均需要通过主线程来完成,如线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存中进行读取操作,新变量的值才会对线程B可见。第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖复制结果的地方都能获取到正确的结果,但不能保证变量赋值操作的顺序与程序代码中的执行顺序一致,因为在一个线程的方法执行过程中无法感知这一点,这也就是说java内存模型中描述的所谓的“线程表现为串行的语义”
25.用什么工具可以查出内存泄露
(1)MemoryAnalyzer:一个功能丰富的java堆转储文件分析工具,可以帮助你发现内存漏洞和减少内存消耗
(2)EclipseMAT:是一款开源的java内存分析软件,查找内存泄漏,能容易找到大块内存并验证谁在一直占用它,基于eclipse RCP。可以下载Rcp的独立版本或eclipse的插件
(3)JProbe:分析java的内存泄漏
26.Student s = new Student()在内存里做了哪些事情
- 加载Student.class文件进内存
- 在栈内存为s开辟空间
- 在堆内存为学生对象开辟空间
- 对学生对象的成员变量进行默认初始化
- 对学生对象的成员变量进行显示初始化
- 通过构造方法对学生对象的成员变量赋值
- 学生对象初始化完毕,把对象地址赋值给s变量
(因为整整写了一天,中间后半部分写了两遍,后来情绪逐渐暴躁失控,可能有错别字 大家懂意思就好)