java中就虚拟机是其他语言编写的(C语言+汇编语言,因此,JVM最常出现的攻击就是buffer overflow),如javac命令等,而java api是java写的,大多开源在openjdk,jdk中有一个src.jar,就是JDk的源码,本文是JVM基础知识的一个汇总,方便查阅,内容较多,以下是内容目录,可以直接跳到感兴趣的章节。
1、JVM的内存模型
JDK7内存模型(图来自于网络):
JDK8内存模型(图来自于网络):
JVM内存模型说明:
JDK7中内存模型包括:方法区,堆区,栈区,本地方法栈,计数器,分为两个类型的区域,一种是线程私有的,一种是线程共享的,其中,方法区和堆区是线程共享的,其他都是线程私有的,堆区是被GC的主要区域,方法区有部分废弃的常量可以被GC,下面详细的说明:
(1)计数器,线程私有,当前线程所执行的字节码的行号指示器,标记当前执行到了哪一行指令
(2)虚拟机栈,线程私有,生命周期和线程相同,存局部变量表、操作数栈、动态链接、方法出口(即方法返回地址)信息等
其中,局部变量表存编译器可知的各种数据类型,包括boolean、char、byte、short、int、float、double,及对象的引用
(3)本地方法栈,和虚拟机栈所发挥的作用非常相似,区别是,虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的 局部变量表、操作数栈、动态链接、出口信息
(4)堆,java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对 象实例以及数组都在这里分配内存
(5)方法区,用于存储已被虚拟机加载的类的信息(字段、方法)、常量、静态变量、即时编辑器编译后的代码等数据,运行时常量池:属于方法区,包含字面量(字符串、final常量)、符号引用
其中,方法区的详细说明:
方法区,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 做 Non-Heap(非堆),目的应该是与 Java 堆区分开来,方法区也被称为永久代,但方法区和永久代并不完全等同,只是为了便于管理,这样,HotSpot虚拟机的垃圾收集器就可以像管理java堆一样管理这部分内存,方法区可被GC回收的那部分内存是废弃的常量,但这样管理并不好,会容易产生内存溢出,所以在JDK8中,移除了方法区,将方法区中的常量池移至堆中(字符串池和类的静态变量放入java堆中),其他移到直接内存中,命名为“元空间”,解决了永久代会出现内存溢出的问题,但是要注意,需要设置元空间的最大大小(-XX:MaxMetaspaceSize设置),否则,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存
(6)直接内存,不属于JVM运行时数据区,JVM的NIO方法可以分配堆外内存如使用 DirectByteBuffer
2、堆栈的异常总结
(1)堆内存溢出OutOfMemoryError:java heap space
产生原因:java堆用于存储对象实例,只要不断的创建对象,并保证GC roots到对象间有可达路径避免这些对象的GC,那么,当对象数量到达堆的最大容量限制后就会产生OOM
解决办法:
- 通过参数 -XX:HeapDumpOnOutOfMemoryError 可以让虚拟机在内存溢出异常时Dump当前内存堆转储快照
- 通过内存映像分析工具(如:Eclipse Memory Analyzer)对Dump出的堆转储快照分析,判断是内存泄露还是内存溢出
- 如果是内存泄露:通过工具查看泄露对象的类型信息和它们到 GC Roots 的引用链信息,分析GC收集器无法自动回收它们的原因,定位内存泄露的代码位置
- 如果是内存溢出:检查堆参数 -Xms和-Xmx,看是否可调大;代码上检查某些对象生命周期过长,持有时间过长的情况,尝试减少程序运行期间内存消耗
(2)除程序计数器外,JVM其他几个运行时区域都可能发生OutOfMemoryError异常
(3)栈的两种异常
1. StackOverFlowError异常:线程请求的栈深度大于虚拟机所允许的最大深度
一个会发生stackOverFlow的场景:无限递归,没有出口
2.OutOfMemoryError异常:虚拟机扩展栈时无法申请足够的内存空间
一个会发生OutOfMemory的场景:list,无限添加元素
解决虚拟机栈两种异常的办法:
1.检查代码中是否有死递归
2.配置 -Xss 增大每个线程的栈内存容量,但会减少工作线程数,需要权衡
3、常用的JVM参数设置
(1)-Xss,设置栈的大小,不熟悉最好使用默认值,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常
(2) 堆内存设置:
① -Xms,初始堆大小,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值
② -Xmx,最大堆大小,默认为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定
默认空余堆内存小于 40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、 -Xmx相等以避 免在每次GC 后调整堆的大小。
③ -Xmn:年轻代大小(young区大小),通常为 Xmx 的 1/3 或 1/4,不熟悉最好保留默认值
④ -XX:SurvivorRatio,设置年轻代Eden和单个S区的比例,默认为8,即Eden为8/10,两个survivor分别为1/10,其中一个survivor闲置,用于复制,所以年轻代实际可用的内存 大小为-Xmn设置值的9/10,Eden设置的太大的话,会导致GC变慢,并且没有足够的survivor幸存者空间,会导致GC直接到达老年代,老年代满的更快,会更早触发FullfGC, Eden设置的过小,则MinorGC频繁,会影响线上程序运行,因为GC会导致应用程序暂停
⑤ -XX:MaxTenuringThreshold,设置年轻代中回收区对象的年龄,默认15,可通过命令指定,如果设置为0,表示Eden回收时,不经过Survivor,直接到达老年代
(3) 持久代设置:
① -XX:PermSize,方法区(永久代,或称非堆区)初始分配的内存大小,其全称为permanent size(持久化内存)
② -XX:MaxPermSize=64m,永久代的最大大小,超过这个值,将会抛出OutOfMemoryError:PermGen
在配置之前一定要慎重的考虑一下自身软件所需要的非堆区内存大小,因为持久代内存是不会被java垃圾回收机制进行处理的地方。
最大堆内存与最大非堆内存的和绝对不能够超出操作系统的可用内存。
(4) 元空间的设置:
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始 了),取而代之是元空间,元空间使用的是直接内存。配置如下:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
与永久代很大的不同就是,元空间解决了永久代易发生内存溢出的问题,但是要注意,如果不指定元空间的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
4、JVM的分代介绍
因为GC垃圾回收的主要区域是堆区,从GC的角度来说,java堆又细分为新生代和老年代,另外有一部分是持久代,来代表方法区,是为了方便管理,是方法区在堆区开辟出来的一块逻辑空间,下图为分代示意图(图片来自于网络)
(1)新生代,新生代又分为三个区域,包括Eden区和两个Survivor幸存者区,Eden区主要存放new出来的对象,Survivor主要存放上一次GC之后的幸存者,作为这一次GC的被扫描者,分别对应图中的S0和S1,两个Survivor区等大,其中一个闲置,所以新生代实际可用的内存大小要减去其中一个幸存者区域的大小
(2)老年代,老年代主要存放大对象,或从MinorGC过来的到达一定年龄仍然幸存的对象
(3)持久代,即方法区,用于存放已被虚拟机加载的类的信息,静态变量,常量,和编译后的代码等信息,持久代能被GC的是废弃的常量,持久代对垃圾回收没有显著影响
5、GC的回收过程
(1) MinorGC的过程:MinorGC采用复制算法,首先,把Eden区域和使用的幸存者区域(SurvivorFrom)中存活的对象复制到另一块空闲的幸存者区(SurvivorTo)中,同时,把这些对象的年龄+1,如果有对象的年龄已经达到了老年代的标准,则赋值到老年代区,如果空闲的幸存者区(SurvivorTo)不够存放Eden和使用的幸存者区(SurvivorFrom)移动过来的数据,则直接放到老年代,然后,清空Eden和使用的幸存者区中(SurvivorFrom)的对象,然后,幸存者区互换,SurvivorTo中的数据换到SurvivorFrom中,SurvivorFrom继续等待下一次GC,Survivor区每熬过一次MinorGC,就将对象的年龄+1,当对象的年龄到达某个值时(默认时15,可用通过参数-XX:MaxTenuringThreshold 来设定),这些对象就会成为老年代
(2) MajorGC的过程:老年代的回收,不会那么频繁,老年代使用的回收算法是标记-清除或标记-整理算法,标记-清除算法会产生内存碎片,即不连续的空间,如果此时,有大的对象进来,内存中没有足够的连续空间时,会提前触发FullGC(这是一个优化点),一次FullGC的时间要比一次MinorGC的时间长,当年老代也装不下,就会抛出OOM(Out Of Memory)异常 (3) Full GC的触发:老年代满了而无法容纳更多的对象,会触发Full GC,Full GC 清理整个内存堆,包括年轻代和老年代。
6、GC的回收算法
(1) 标记-清除算法,缺点是容易产生碎片,且效率不高,标记过程和清除过程效率都不高
标记-清除算法的过程,分为两个过程,标记过程和清除过程
① 标记过程,遍历所有的GC-roots,然后将所有GC-roots可达的对象标记为存活的对象(记为1)
② 清除过程,遍历堆中的所有对象,将没有标记的对象全部清除掉(没有标记过的,记为0)
③ 清除过后,被标记过的对象留下,标志位重新归0
以下是标记-清除算法的图示(图片来自于网络)
(2) 复制算法,用于年轻代,需要额外的空间来进行复制操作
复制算法的过程,就是把内存分为2块等同大小的内存空间(A和B),使用A进行内存的使用,当A内存不足以分配对象而引起内存回收时,就会把存活的对象从A内存块放到B内 存块,然后把A内存块中的对象全部清除掉,然后在B内存块中使用,当B内存不足以分配对象而引起内存回收时,就会把存活的对象从B内存块放到A内存块中,然后把B内存 块中的对象全部清除掉,如此循环
复制算法的好处是,避免的空间碎片(内存中不连续的空间),缺点是浪费了一半的空间,降低空间使用率
以下是复制算法的图示(图片来自于网络)
(3) 标记-整理算法(Mark-Compact),用于老年代
标记-整理算法的过程,标记过程仍然与标记-清除算法一样,但后续步骤不是直接 对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以 外的内存
以下是标记-整理算法的图示(图片来自于网络)
从图中可以看出,标记-整理算法,避免了标记-清除算法产生内存碎片的问题, 也避免了复制算法中内存浪费的问题,存在的问题就是效率问题,比前两者效率低
(4) 分代收集,JVM实际GC中使用的,根据对象存活周期的不同,分新生代和老年代,新生代使用复制算法,因为每次GC只有少量的对象存活,用复制算法只需要付出少量存活对 象的复制成本就可以完成收集,老年代使用标记-整理算法或标记-清除算法,老年代中,对象存活率高,没有额外的担保空间,就必须使用标记-清除或标记-整理算法
以下是分代收集算法的图示(图片来自于网络)
7、GC的回收器
1、收集器
(1) serial收集器,是单线程收集器,用于新生代,使用复制算法,在GC时,会暂停其他所有工作的线程,直到GC结束,常用于client模式下的虚拟机
(2) parNew收集器,是serial的多线程版本,也用于新生代,使用复制算法
parNew和serial都可以且只能和老年代的CMS和serial old组合使用
(3) Parallel Scarenge收集器,用于新生代,使用复制算法,主要关注吞吐量,适合在后台运算且不需要太多交互的任务
(4) Serial old收集器,是serial收集器的老年代版本,是单线程收集器,使用标记-整理算法,也是给client下的虚拟机用
(5) Parallel old,用于老年代,使用标记-整理算法,注重吞吐量,及CPU敏感的场合优先考虑使用parallel + parallel old组合
(6) CMS(concurrent Mark Sweep)收集器,特点是获取最短回收停顿时间为目标的收集器,使用标记-清除算法,支持并发、低停顿,缺点是对CPU资源敏感(在并发阶段,虽然不会导致用户线程停顿,但会因为占用一部分线程(或CPU资源),而导致应用程序变慢),会导致吞吐量降低,且无法收集浮动垃圾(标记-清除算法,会产生大量的碎片),会导致FullGC,可以用serial old临时替代
(7) G1,JDK7中新增的回收器,是JDK9默认的回收器,特点是,面向服务端应用的垃圾收集器,支持并发与并行,充分利用CPU多核,缩短stop时间,且可预测停顿,采用分代收集,整体是标记-整理算法,局部是复制算法,不会产生碎片
2、G1回收器的工作原理
(1)G1的分代收集,如下图(图片来自于网络)
G1的分代收集,将整个堆分成n个大小相等的Region区域,每个Region占用一块连续的虚拟内存地址,新生代和老生代不再是物理隔离,而是一部分Region的集合,Region的大小可以通过-XX:G1HeapRegionSize设定,如果未设置,默认是2048份,G1仍是分代收集,除Eden、Survivor、Old区域外,还包含Humongous,是专门用来存放巨型对象的,即占用空间>50%分区容量的对象,以此减少短期存在的巨型对象对垃圾收集造成的负面影响。
G1的回收过程:
(1)标记过程,G1的标记分为几个阶段,包括全局并发标记,初始标记,并发标记,最终标记
i. 全局并发标记:基于 STAB(snapshot-at-the-beginning)形式的并发标记,标记完成后,G1知道哪个区域是空的,它首先会收集那些产生大量空闲空间的区域
ii.初始标记STW:耗时很短,标记GC-roots能直接关联的对象,压入扫描栈
iii.并发标记:与用户线程并发执行,耗时较长,GC线程不断从扫描栈中取出引用,然后递归标记,直到扫描栈清空
iv.最终标记STW:重新标记并发标记期间因用户程序执行而导致引用发生变动的那部分标记,写入屏障Write Barrier标记的对象
(2)清理过程:统计各个Region被标记存活的对象有多少,如果发现没有存活,就会整体回收到可分配的Region中
(3)拷贝存活对象:将Region中的存活对象拷贝到空Region里去,回收原Region空间,继续使用
G1不存在Full GC,分为Yong GC和Mix GC两种,Yong GC是新生代的回收,Mix GC是老年代的收集
8、JVM的优化
这是很重要的一部分,JVM参数平时不需要频繁的去调整,可以通过观察应用环境的运行,定期的调整,主要有两方面,一个是JVM参数的调整,一个是GC的优化,GC优化的目标是尽量减少GC次数,尽量在Yong区完成GC,尽量减少Full GC的次数,以减少GC对应用程序带来的影响
(1)JVM参数(这里列出所有可调整的JVM参数,可根据各自的环境酌情设置)
-Xms4G 是指: JVM启动时整个堆(包括年轻代,年老代)的初始化大小
-Xmx4G 是指: JVM启动时整个堆的最大值,默认为物理内存的1/4
-Xms和-Xmx的设置,默认空余堆内存小于 40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般 设置-Xms、 -Xmx相等以避免在每次GC 后调整堆的大小
-Xmn2G是指:年轻代的空间大小,通常为 Xmx 的 1/3 或 1/4,剩下的是年老代的空间
-XX:SurvivorRatio=1是指:年轻代Eden区和单个S区的比例,默认是8,即Eden占8/10,两个Survivor分别占1/10,其中一个Survivor闲置,用于复制,所以年轻代实际可用的内存大小未-Xmn设置值的9/10,Eden设置的太答的话,会导致GC变慢,并且没有足够的幸存者空间,会导致GC直接到达老年代,老年代满的更快,会更早触发Full GC,Eden设置的过小,则Minor频繁,会影响线上程序运行,因为GC会导致应用程序暂停
-XX:MaxTenuringThreshold,设置年轻代中回收区对象的年龄,默认15,可通过命令指定,如果设置为0,表示Eden回收时,不经过Survivor,直接到达老年代
-XX:NewRatio,设置新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
(2)GC的参数优化
i.CMS回收器的参数优化
-XX:CMSInitiatingOccupancyFraction=70,该值代表老年代堆空间的使用率,默认值是92,假如设置为70,就表示第一次 CMS 垃圾收集会在老年代占用 70% 时触发。过大会使 STW 时间过长,过小会影响吞吐率
-XX:+UseCMSCompactAtFullCollection,-XX:CMSFullGCsBeforeCompaction=4:执行4次不压缩的 Full GC 后,会执行一次内存压缩的过程,用来消除内存碎片
-XX:+ConcGCThreads,并发 CMS 过程运行时的线程数,CMS 默认回收线程数是(CPU+3) / 4。更多的线程会加快并发垃圾回收过程,但会带来额外的同步开销。
ii.G1回收器的参数优化
-XX:G1HeapRegionSize,指定G1中Rigion的大小,如果未设置,默认将堆内存平均分为 2048 份
-XX:MaxGCPauseMillis=n,设置GC时最大暂停时间,这个目标不一定能满足,JVM会尽最大努力实现它,不建议设置的过小(<50ms)
-XX:InitiatingHeapOccupancyPercent=n,触发G1启动 Mixed GC,表示垃圾对象在整个G1 堆内存空间的占比
避免使用 -Xmn 或 -XX:NewRatio等其他显式设置年轻代大小的选项,固定年 轻代大小,会覆盖暂停时间目标
9、JVM的内存分析工具
JVM自带的内存分析小工具:jconsole、jhat、jmap、jstack、jstat、jstatd、jvisualvm
linux工具:pidstat、vmstat、iostat
eclipse的分析工具:mat
收费工具:Jporfiler,yourkit
(1) jconsole,jvm自带内存分析工具,位于jdk的bin目录下,它提供了图形界面。可以查看到被监控的jvm的内存信息,线程信息,类加载信息,MBean信息。
(2) jhat,jvm自带内存分析工具,位于jdk的bin目录下,jdk6+版本自带,能够分析dump文件,执行 jhat -J -Xmx512m [file] ,file就是dump文件路径。
(3) jmap,jvm自带内存分析工具,位于jdk的bin目录下,倾向于分析jvm内存中对象信息,jmap -histo <pid>在屏幕上显示出指定pid的jvm内存状况,太简单。
jmap -dump:file=c:\dump.txt 340 导出dump文件,用专门的dump分析工具分析。
(4) jstack,jvm自带内存分析工具,位于jdk的bin目录下,会显示线程优先级,线程ID,native线程ID,线程栈起始地址
(5) jstat,jvm自带内存分析工具,位于jdk的bin目录下,倾向于分析jvm内存的gc情况,常用参数-gcutil,这个参数的作用不断的显示当前指定的jvm内存的垃圾收集的信息。 jstat -gcutil 340 10000,这个命令是每个10秒钟输出一次jvm的gc信息,10000指的是间隔时间为10000毫秒。
(6) jstatd,jvm自带内存分析工具,位于jdk的bin目录下,一个RMI的server,它可以监控Hotspot的JVM的启动和结束,同时提供接口可以让远程机器连接到JVM。 比如 jps jstat都可以通过jstatd来远程观察JVM的运行情况。
(7) jvisualvm,jvm自带内存分析工具,位于jdk的bin目录下,JDK6 update 7之后推出,,java可视化虚拟机,它不但提供了jconsole类似的功能,还提供了jvm内存和cpu实时诊断,还有手动dump出jvm内存情况,手动执行gc。和jconsole一样,运行jviusalvm,在jdk的bin目录下执行jvisualvm,windows下是jvisualvm.exe,linux和unix下是jvisualvm.sh。
(8) pidstat,linux系统下使用,需要安装,yum install sysstat,要查看Linux下面进程、进程组、线程的资源消耗的统计信息,可以使用pidstat,它可以收集并报告进程的统计信息。
(9) vmstat,linux系统下使用,需要安装,vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU活动进行监控。是对系统的整体情况进行统计,不足之处是无法对某个进程进行深入分析。
(10) iostat,linux系统下使用,需要安装,yum install sysstat,iostat是I/O statistics(输入/输出统计)的缩写,iostat工具将对系统的磁盘操作活动进行监视。它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况。iostat也有一个弱点,就是它不能对某个进程进行深入分析,仅对系统的整体情况进行分析
(11) MAT,可以通过MAT分析内存泄漏的原因
10、对象的创建
在语言层面,创建对象有四种方式:1) clone 2)反序列化 3)反射 4) New
而在虚拟机中,对象创建的过程是如何呢?
JAVA编译解释的过程:.java文件->javac编译成.class字节码文件->jvm解释执行。
Java很特殊,Java程序需要编译但是没有直接编译成机器语言,即二进制语言,而是编译成字节码(.class)再用解释方式执行。java程序编译以后的class属于中间代码,并不是可执行程序exe,不是二进制文件,所以在执行的时候需要一个中介来解释中间代码,这既是java解释器,也就是所谓的java虚拟机(JVM),也叫JDK。
JVM中,对象创建过程(New)分三步:1) 类加载 2) 为新对象分配内存 3)初始化
在虚拟机遇到new指令时:
1) 类加载:确保常量池中存放的时已解释的类,且对象所属类型已经初始化过,如果没有,则先执行类加载
2) 为新生对象分配内存:对象所需内存大小在类加载时可以确定,将确定大小的内存从Java堆中划分出来
- 分配空闲内存方法:
- 指针碰撞:假如堆是规整的,用过的内存和空闲的内存各一边,中间使用指针作为分界点,分配内存时将指针移动对象大小的距离
- 空闲列表:假如堆是不规整的,虚拟机需要维护哪些内存块是可用的列表,分配时候从列表中找出足够大的空闲内存划分,并更新列表记录
- 对象创建在并发情况下保证线程安全:例如,正在给对象A分配内存,指针还没修改,对象B同时使用了原来的指针来分配内存
- CAS配上失败重试
- 本地线程分配缓冲TLAB(ThreadLocal Allocation Buffer):将内存分配动作按线程划分到不同空间中进行,即每个线程在Java堆中预先分配一块内存
3) 将分配的内存空间初始化为零值:保证对象的实例在Java代码中可以不赋值就可 直接使用,能访问到这些字段的数据类型对应的零值(例如,int类型参数默认为0)
4) 设置对象头:设置对象的类的元数据信息、哈希码、GC分代年龄等
5) 执行<init>方法初始化:将对象按照程序员的意愿初始化
11、对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局分为3个区域,如下图:
(1)对象头(Header):
i. MarkWord:存储对象自身的运行时数据,例如:哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。考虑空间效率,Mark设计为非固定的数据结构,它根据对象的不同状态复用自己的空间,如下表格:
ii. 指向Class的指针:即对象指向它的类的元数据的指针,虚拟机通过这个指针来确定是哪个类的实例
iii. 如果对象是Java数组,对象头中还需要一块记录数组长度的数据
(2) 实例数据(Instance Data):对象真正存储的有效信息,也是程序代码中定义的各种类型字段的内容
(3) 对齐填充(Padding):起占位符的作用,因为HotSpot VM的要求对象起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍,当对象实例数据部分没有对齐时,需要对齐填充来补充
12、对象的监视器
什么是对象监视器,监视器是一种同步结构,它基于互斥锁,允许线程同时互斥(使用 锁)和协作
互斥是,当一个线程访问受保护的数据时,如果没有其他线程在等待, 线程获取锁 并继续执行。当线程完成执行时,它释放锁并退出监视器。但如果此时另一个线程已经拥有监视器时,它必须在entry-set中等待。当前面的线程 执行完毕退出监视器时,新到达的线程必须与在入口集中等待的其他线程竞争。只有一 个线程能赢得竞争并拥有锁。
协作是,当一个线程需要数据在某一个状态下它才能执行,那么另一个线程负责将数据 改变到此状态
对象监视器的一个理解:对象监视器,任意线程对Object的访问,首先要先获得Object 的监视器。如果获取失败了,线程进入同步队列,线程状态变为BLOCKED。当访问Object 的线程(获得了所的线程)释放了锁,则该释放操作唤醒在同步队列中的线程,使其重 新尝试对监视器的获取。Thread类提供一个holdsLock(Object obj)方法,当且仅当对象 obj的监视器被某条线程持有的时候才返回true,注意这是一个static方法,意味着某 条线程指的是当前线程
常见的如生产者/消费者的问题,当读线程需要缓冲区处于“不空”的状态它才可以从 缓冲区中读取任何数据,如果它发现缓冲区为空,则进入wait-set等待。待写线程用数 据填充缓冲区,再通知读线程进行读取。这种机制被称为“Wait and Notify”或“Signal and Continue”
下图描述了对象、对象的监视器、同步队列和执行线程之间的关系。
从上图中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先 要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。 当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队 列中的线程,使其重新尝试对监视器的获取。
那么,对象的监视器到底是做什么的,用在哪里,起什么作用?
为什么使用术语“监视器”而不是“锁定”?严格来说,确实不同,“锁”是指具有获取和释放原语的东西,这些原语和原语保持某些锁属性。例如,独 占使用或单作者/多读者。
“监视程序”是一种机制,可确保在任何给定时间只有一个线程可以执行给定的代码 段。可以使用锁(和“条件变量”,允许线程等待或向其他线程发送满足条件的通知) 来实现此功能,但它不仅仅是锁。实际上,在Java情况下,不能直接访问监视器 使用的实际锁。(您不能说“ Object.lock()”来阻止其他线程获取它,就像使用Java Lock实例一样。)
简而言之,如果要学究的话,“ monitor”实际上是比“ lock”更好的术语,用于描述 Java提供的特性。但是实际上,这两个术语几乎可以互换使用。
13、类加载器、反射、双亲委派
(1)类加载器,其作用就是,负责将class文件加载到内存中
java中,先通过javac将java文件编译为class文件, 然后使用ClassLoader类加载器加载class文件到内存中,当一个类被使用时,就会加载到内存中,类加载的过程包括:加载,验证,准备,初始化
(2) 双亲委派模型
每一个类都有一个相对应的类加载器,系统中的ClassLoader在协同工作会默认使用双亲委派模型,类加载的过程,是从父类到子类,类验证的过程,是从子类到父类,也就是说,加载的时候,首先把该类委派给该父类加载器的loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader中,然后验证,即在类加载的时候,系统就会首先判断当前类是否被加载过,如果已经加载的类会直接返回,否则都会尝试加载,即当父类加载器无法处理时,都由自己来处理,当父类加载器为null时,会使用启动类加载器BoostrapClassLoader作为父类加载器。
双亲委派保证了java程序的稳定运行,避免了类的重复加载。
除了BootstrapClassLoaderader其他类加载器均由Java实现且全部继承于java.lang.ClassLoader,如果要自定义类加载器,就要继承ClassLoader。
(3)反射
反射是把.class文件加载进内存,把.class中的所有内容,封装成 一个一个的对象的过程,在 程序中可以通过这些对象动态的完成方法的调用,成员变量的赋值,对象的创建!
反射获取Class对象的三种方式:
i、Class.forName(全类名)
ii、类名.class
iii、对象名.getClass();