dengchengchao / notes-jvm Goto Github PK
View Code? Open in Web Editor NEW《深入理解JVM》读书笔记
《深入理解JVM》读书笔记
栈帧(Stack Frame
)是用于支持虚拟机进行方法调用和执行的数据结构,栈帧主要存储了:
每一个方法的调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面入栈到出栈的过程。
方法调用分为一下步骤:
解析: 将方法的符号引用解析为内存地址
分派:由于Java
是一门面向对象的语言,因此需要多态调用方法。重载和重写的区别就在于一个是静态调用,一个是动态调用。
JVM
实现重载的过程如下:
C
中找到与该方法特征完全符合的方法,并且权限校验能通过,则返回这个方法的引用AbstractMethodError
异常。可以看出来,由于方法的每次动态调用都会涉及到依次搜索过程,但是由于动态调用是一个非常频繁的操作,因此为了节约性能,JVM
会在方法区中建立一个虚方法表,虚方法表存放了各个方法的入口地址,如果某个方法子类没有被重写,那么子类方法与父类方法入口地址一致,如果重写了方法,则替换子方法的方法地址即可。
虚方法表一般在类的加载的连接阶段进行初始化,在准备了类的变量初始值后,虚拟机会将方发表也初始化完毕。
GC应该思考的3件事:
首先需要明白的一点是,引用都是在栈上的,栈上的引用指向了堆中的对象,而方法执行都是依靠操作这些栈的引用进行,随着方法执行的完成,栈上引用也会随之回收。当没有引用指向某个对象的时候,这么对象则为无用对象,此时即可回收这个对象。
那么如何确定是否有引用指向某个对象呢?
引用计数法
给一个对象添加一个引用计数器,每当有一个引用引用这个对象的时候,则计数器加一,引用失效的时候,计数器减一。当计数器为0的时候,则可以进行GC.
但是这样无法解决的一个问题便是循环引用。
可达性分析算法
类似链表的遍历,算法通过一个称为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,所走过的路径称为引用链。当某个对象到GC Roots节点没有任何的引用链存在的时候,则证明此对象是不可用的。
在Java中,会作为GC Roots的对象包括以下几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JNI引用的对象
注意:GC Roots 是可作为索引的引用的集合,也就说GC Roots里面存储的是引用
2.2 对于引用的种类:
2.3 finalize
在经过可达性算法后,不可达的对象需要经过两次标记才会被真正的回收
finalize()
方法,当对象没有覆盖 finalize()
方法或者已经执行过 finalize()
方法,那么虚拟机会视作没必要执行 finalize()
方法 finalize()
方法的对象,虚拟机会将其放在一个由虚拟机自动建立的,低优先的线程执行的 F-Queue
队列中。虚拟机并不保证等待方法执行完成以防止过久等待。执行完该方法后,将该对象标记为没必要执行任何对象的
finalize()
方法都只会被调用一次。不建议在
finalize()
方法中做任何工作,对于资源回收,应该使用try-finally.finalize()
方法具有不确定性,并且finalize()
方法具有不确定性,并且运行代价昂贵。
2.4 回收方法区
卸载/回收一个类的条件有3个
对类的回收并不是无用即回收。HotSpot提供了参数进行配置什么时候卸载类。
可达性分析具体实现的问题
有可达性分析知道,GC Roots
主要在全局性引用(常量或静态属性)和执行上下文(栈帧的本地变量表),而这些数据量都比较大,因此要逐个检查引用会消耗很多的时间
GC Roots
需要GC 停顿可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化
就好想妈妈到扫卫生的时候,你不能再往地上扔垃圾,否则你就会挨骂~
这种现象称为Stop The World
Stop The World 是JVM 在后台自动发起和自动完成的,在用户不可见的情况下,把用户的正常工作线程全部停掉
HotSpot JVM
对GC Roots查找的优化
查找GC Roots
的时候,一般主流的JVM都是准确式GC,可以直接得知哪些地方存放着对象的引用,所以在Stop The World的时候,并不需要全部、逐个检查完全局性和执行上下文的引用位置。
在HotSpot
中,是使用的一组称为OopMap
的数据结构来实现准确式GC:
在类加载的时候,计算对象内什么偏移量上是什么类型的数据
在JIT编译时,也会记录栈和寄存器中哪些位置是引用
OopMap
带来的问题
HotSpot在OopMap
的帮助下可以快速且准确的完成GC Roots枚举,但是有这样一个问题
:运行过程中,非常多的指令会导致引用关系变化,如果为这些执行都生成对应的OopMap
,那么需要的空间成本太高
问题解决:
:只在特定的位置记录OopMap
的引用关系,这些位置,则称为安全点(Safepoint)
特定的位置指的是程序只能运行到安全点才能停下来进行GC
安全点的选定
安全点不能太少,否则GC等待的时间太长;也不能太多,空间开销过大,因此安全点基本上是以是否具有让程序长时间执行为特征为标准选定,”长时间执行“最明显的便是指令序列复用:方法调用,循环跳转,循环的末尾,异常跳转等;
使用安全点的规则,解决了OopMap对象过多的问题
如何在安全点上停顿
虽然规定的安全点规则,但是如何在需要GC的时候让所有的线程到达安全点的时候再停顿呢?
不需要线程配合,实现如下:
现在几乎没有JVM使用这种方式
主动式中断
在GC发生的时候,不直接操作线程中断,而是简单的设置一个标志,让各个线程主动去轮询这个标志,发现中断标志为真的时候就自己中断挂起,而轮询标志的地方和安全点是重合的,因此可以直接挂起
安全区(Safe Region)
对于安全点(Safe Point)还有一个问题:对于程序没有CPU时间的时候(Sleep 或 Block 状态),无法运行到Safe Point 上中断再挂起,于是这个时候需要安全区。
什么是安全区
安全区指的是在一段代码片段中,引用关系都不会再发生变化,在这个区域中任意地方开始GC都是安全的
如何利用安全区
线程进入安全区的时候,首先标志自己已经进入了安全区。
线程被唤醒离开安全区的时候,检查系统是否已经完成的根节点枚举(或者整个GC),如果已经完成,则继续执行,否则必须等待,直到收到可以安全离开安全区的信号通知。
关于OopMap,安全点等,详细可以参考R大--找出栈上的指针/引用
标记-清除算法
最基础的标记清除算法分为两个阶段,第一个阶段为标记出所有需要回收的对象,在标记完成后回收所有被标记的对象。
标记清除算法不足之处在于两点:
复制算法
为了解决效率问题,在标记清除的基础上,产生了复制算法,复制算法将可用内存按照容量大小分为两个大小相等的块,每次都值使用其中的一块,当这一块使用完了以后,再复制存活的对象到另外一块中,然后再统一清理,这样的回收算法实现简单,运行高效。
HotSpot
中并未按照1:1划分空间,而是将空间分为一块较大的Eden
空间和两块较小的Survivor
空间,每次使用只使用Eden
和其中一块Survivor
空间,当回收的时候,将剩余对象赋值到另外一块Survivor
空间中,最后清理刚刚的控件。同时Eden
和Survivor
的比例为8:1:1
当1/10
的内存不够使用的时候,存活的对象会直接进入老年代。
标记-整理算法
复制算法有一个缺点便是在对象存活率较高的时候紧要进行较多的复制,这时效率便会降低,更加关键的是如果不是使用50%
的空间,就需要进行空间担保。所以在老年代一般不使用这种算法。
标记整理算法是基于标记-清除算法的一种改进,对于标记过程仍然不变,不过清除的时候并不是直接清除,而是将存活的对象都向同一个方向移动,然后直接清理不其余的控件。
实际使用:分代收集算法
在实际使用的时候,结合各个收集算法的特点,一般商用虚拟机都会根据对象的存活周期将对象分为几块。一般把Java堆分为新生代和老年代。一般新生代中每次收集都会有大批对象死去,这时使用复制算法比较好,而在老年代中,由于对象的存活率比较高,因此一般使用标记-清理算法或标记整理算法。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。HotSpot
主要提供了以下几种垃圾收集器,用于应对不同的场景。
区域:新生代
类型:单线程收集器(只使用一个线程完成收集工作,并且会Stop the world)
特点:使用复制算法
Serial
收集器是最基本,发展历史最久的收集器。
Serial
主要应用场景为:Client
模式,相对于其他收集器而言,它的有点在于:简单而高效,因为是单线程运行,没有线程交互的开销,因此效率比较高。
区域:新生代
类型:并行多线程收集器(多线程进行垃圾回收,并且会Stop the world)
特点:使用复制算法
ParNew
收集器作为Serial
的多线程版本,除了使用多线程进行垃圾回收以外,其他的行为基本和Serial
收集器一样。ParNew
一般作为Server
模式的虚拟机的首选新生代收集器,主要原因是只用它能和CMS
收集器配合工作。
注意:ParNew
由于是多线程收集器,因此它在单CPU下工作效率由于线程开销,一般不如Serial
收集器。
Parallel Scavenge
的特点是它关注的是控制吞吐量,也就是CPU
用于运行用户代码的时间和CPU
总消耗的时间的比值。加入虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99%
Parallel Scavenge
收集器提供了两个参数用于精确控制吞吐量:
-XX:MaxGCPauseMillis
: 控制最大垃圾收集停顿时间-XX:GCTimeRatio
: 设置希望的吞吐量大小Serial Old
是Serial
收集器的老年代版本,它的主要意义在于给Client
模式下虚拟机使用,如果在Server
模式中,它还有两个用途:
Parrallel Scavenge
收集器搭配使用CMS
收集器的后备方案Parallel Old
是Parallel Scavenge
收集器的老年代版本,它是在JDK 1.6
中才开始提供,在此之前只能使用Serial Old
收集器
CMS
(Concurrent Mark Sweep
)是一种以获取最短回收停顿时间为目标的收集器。其运行主要包含以下4个步骤:
其中,初始标记和重新标记这两个步骤仍然需要Stop The World
,初始标记仅仅是标记一下GC Roots
能直接关联到的对象,速度很快。
CMS
是一个比较优秀的收集器,并发收集,低停顿。但是其也有比较明显的3个缺点:
CMS
对CPU
比较敏感,由于是并发的,因此CMS
需要占用一部分CPU
资源CMS
收集器无法处理浮动垃圾,并且可能出现Concurrent Mode Failure
失败而导致Full GC
的产生。CMS
收集器由于是标记-清除算法,因此会产生内存碎片G1 (Garbage-First)
收集器是当今收集器技术发展的前沿成果之一,G1 具有如下特点:
CPU
能缩短Stop The World
的停顿时间,G1
不会产生内存碎片G1运行可以大致分为以下几个步骤:
初始标记和最终标记的时候,需要Stop The World
打印GC
日志参数: -XX:+PrintGCDetails
开启GC
日志后,可以看到:
[GC (System.gc()) [PSYoungGen: 5243K->874K(76288K)] 5243K->882K(251392K), 0.0013200 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 874K->0K(76288K)] [ParOldGen: 8K->800K(175104K)] 882K->800K(251392K), [Metaspace: 3441K->3441K(1056768K)], 0.0050647 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 76288K, used 655K [0x000000076af00000, 0x0000000770400000, 0x00000007c0000000)
eden space 65536K, 1% used [0x000000076af00000,0x000000076afa3ee8,0x000000076ef00000)
from space 10752K, 0% used [0x000000076ef00000,0x000000076ef00000,0x000000076f980000)
to space 10752K, 0% used [0x000000076f980000,0x000000076f980000,0x0000000770400000)
ParOldGen total 175104K, used 800K [0x00000006c0c00000, 0x00000006cb700000, 0x000000076af00000)
object space 175104K, 0% used [0x00000006c0c00000,0x00000006c0cc8158,0x00000006cb700000)
Metaspace used 3448K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
上面的日志一共两次GC
第一段:
[GC(System.gc())]
: 说明GC
的类型是System.gc()
触发。 同时如果还带有[Full GC]
的话说明此次GC
是Stop The World
的。
[PSYoungGen
] :表示GC
发生的区域
对于GC
的区域,不同的收集器的命名不同。
Serial
: DefNew
全称为:Defaulr New Generation
ParNew
: ParNew
全称为: Default new Generation
Parallel Svavenge
: PSYongGen
方括号内5243K->874K(76288K)
: GC
前已使用内存容量->GC
后已使用容量(总容量)
方括号外5243K->882K(251392K)
: GC
前Java
堆已使用容量->GC
后Java
堆已使用容量(Java堆总容量)
GC
:(Minor GC
):指发生在新生代的垃圾收集动作。GC
:(Major GC
/Full GC
): 指发生在老年代的GC
,出现了Major GC
经常会伴随着至少一次Minor GC
,Major GC
一般比Minor GC
慢10倍以上内存分配策略:
新生对象在Eden
区中分配,当Eden
区没有足够的控件的时候,虚拟机将发起一次Minor GC
大对象直接进入老年代,JVM
提供了一个-XX:PretenureSizeThreshold
参数,当对象大小大于这个设置值的时候,直接将大对象放入老年代,避免导致新生代内存不足频繁Minor GC
长期存活对象进入老年代,对象每经过一个Minor GC
依然存活的,年龄就加一,当年龄增加到一定程度(默认15,可以通过-XX:MaxTenuringThreshold
设置),将会晋升到老年代中
动态年龄判定,如果在Survivor
空间中相同年龄的所有对象大小达到Survivor
大小的一半,则年龄大于或等于该年龄的对象会直接进入老年代
空间分配担保,在发生Minor GC
之前,虚拟机需要使用老年代担保新生代空间足够使用,如果老年代最大可用连续空间小于新生代所有对象的空间,如果允许担保失败,则此时会尝试触发一次Minor GC
,如果不允许担保失败,则此时会进行一次Full GC
实际判断并不是使用老年代最大可用连续空间是否大于新生代所有空间,而是判断老年代最大可用来连续空间是否大于每一次晋升到老年代的大小的平均值。如果大于,则尝试
Minor GC
,如果GC
失败,再进行Full GC
类生命周期:加载-验证-准备-解析-初始化-使用-卸载,其中验证-准备-解析统称为解析,因此又可以称加载-连接-初始化
对于类的连接阶段,在某些情况下可以在初始化阶段之后再开始,这是为了支持Java
语言的运行时绑定。
注意,虽然其他阶段,例如加载,虽然一定是按照顺序开始的,但是仅仅是开始,当加载开始后,可能加载还没完成就会执行其他操作
JVM
规定必须初始化的情况:
new , getstatic,invokestaic,putstatic
这4条字节码指令的时候,如果类没有初始化,必须先触发初始化,这4条指令分别对应:
new
一个对象reflect
包的方法对类进行反射调用的时候,如果类没有初始化,则先触发初始化main
的类JDK1.7
的动态语言支持的时候,如果最后解析结果包含REF_getStatic
等。JVM
规范规定有且仅有以上5类情况的时候,才会触发类初始化,类似下面的情况,不会触发类初始化:
对于接口来说,也有接口初始化的过程,这过程大多数和类初始化差不多,不过唯一不同的是接口的类初始化不会导致其父类接口的初始化。
加载是类加载的过程的一个阶段。加载主要需要完成3件事:
java.lang.class
对象,作为方法区这个类的各种数据的访问入口加载与连接部分内容是交叉进行的,加载尚未完成,连接阶段可能就已经开始了
连接包括验证,准备,解析三个阶段。
验证主要是为了确保Class
文件的字节流中包含信息符合当前虚拟机的要求,其主要包括:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中分配。由于此阶段是类的加载阶段,和实例无关,因此这里分配内存和设置类变量都是针对的类,也就是static
。主要进行以下两个工作:
ConstantValue
属性(final static
),在准备阶段会被初始化为初始值。解析是虚拟机将常量池内的符号引用替换为直接引用的过程,其中符号引用指的是用符号来描述所引用的目标,符号可以是任意形式的字面量,
直接引用指的是直接指向目标的指针,相对偏移量或者一个能直接定位到目标的句柄。
虚拟机规范并没有规定解析阶段发生的具体时间。
在准备阶段中,分配了类变量的内存空间和初始值,在初始化阶段,会执行<clinit>()
方法进行类初始化
<clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作以及静态语句块中的语句合并产生的,其收集的顺序是由源文件中出现的顺序决定<clinit>()
方法与类的构造函数不同,它不需要显示的调用父类构造函数,虚拟机在执行<clinit>()
方法之前,会保证其父类已经执行完毕<clinit>()
方法在多线程环境中被正确的加锁,同步。如果多个线程同时初始化一个类,那么只有一个线程会执行<clinit>()
方法,其他线程必须阻塞等待在Java
中,程序员可以自己定义实现类加载动作。因此对于任意一个类,都需要加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
类和类加载器有着很大的关联性,回想回收方法区的时候,判断类是否能回收的条件之一便是加载这个类的类加载器已经被回收
如果两个类的类加载器不同,那么这个类对象的equals()
,isAssignableFrom()
,isInstance()
方法的返回结果必定是false
,其中也包括instanceof
关键字
Java
中类加载器分为以下3种:
Boostrap ClassLoader
)Extension ClassLoader
)Application ClassLoader
)双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是将其委派给父类加载器去完成。
有些时候,双亲委派模型可能需要被破坏
Application ClassLoader
加载。因此,Java
设计者提供了Thread.getContextLoader()
获取上下文类加载器,而这个类加载器默认便是Application ClassLoader
JVM规范中规定,Java虚拟机所管理的内存包括以下几个运行时数据区域:
程序计数器是一块比较小的内存空间,它可以看作是当前线程所执行字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器是唯一一个没有规定任何OutOfMemoryError
的区域,并且它是线程私有的。
程序计数器的线程私有:为了在多线程情况下,线程切换后能恢复现场,每个线程都会有个自己的程序计数器
程序计数器只会记录虚拟机字节码执行的指令地址,对于执行本地方法,程序计数器只会显示Undefined
虚拟机栈 描述的是Java方法执行的内存模型:在每个方法执行的时候都会创建一个栈帧(Stack Frame),用来存放:局部变量表,操作数栈,动态链接,方法出口等信息。
值得注意的是,局部变量表存放了编译器可知的各种基本数据类型和引用类型。其中64位长度的long和double类型会占用2个局部变量控件(Slot),其余的类型各占用一个。
其中一个Slot为“字宽”,对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。 这与我们的认知:byte占用一个字节,short,char两个字节等是相违背的。具体原因见:Java基本类型的物理存储大小---R大的回答
当线程的栈的深度大于虚拟机所允许的深度的时候(比如无限递归)虚拟机栈会抛出 StackOverflowError
异常。如果虚拟机栈可以动态扩展,而当扩展时无法申请到足够的内存的时候,虚拟机栈会抛出OutOfMemoryError
异常
虚拟机栈也是线程私有的,每个线程都独享一个虚拟机栈以保证每个线程都正常执行。
本地方法栈和虚拟机栈非常类似,不过本地方法栈是用来执行本地方法。由于非常相似,因此 HotSpot JVM 已经将这两个栈的实现合二为一。
本地方法栈和虚拟机栈的特性非常相似
Java虚拟机规范规定,所有实例对象以及数组都要在堆上分配,因此Java堆一般用来存储实例对象。
Java虚拟机规范规定,Java堆可以处于物理上不连续的物理空间,只要逻辑上连续即可,在实现的时候,可以将堆实现成固定大小,也可以实现成可扩展的。不过一般主流虚拟机都将Java堆实现为可扩展的。
Java堆是线程共享的,每个在对象的对象都可以被不同的线程访问并改变
如果在堆中没有足够的内存完成实例分配,并且堆也无法扩展,则会抛出OutOfMemoryError
方法区是用来存储已被虚拟机加载的Class的相关信息比如:类名,访问修饰符,字段描述,方法描述,常量池、静态变量、即时编译器编译后的代码等数据。虽然JVM规范将方法区描述为堆的一个逻辑部分,但是他却有一个别名:Non-Heap,目的是为了与Java堆区分开来。
在HotSpot JVM中,jdk 7以前是用永久代(Permanent Generation)来实现方法区,这样实现是为了便于使得方法区能像Java 堆一样收集垃圾。
但是由于永久代有-XX:MaxPermSize上限,而
String
的intered
对象同样是放在永久代中(方法区外),在使用大量类似String.inter()
功能的方法后,容易引起永久代内存溢出异常进而影响到方法区。而其他方式实现的方法区却没有此问题。因此在JDK1.7 的HotSpot中,已经把原本放在永久代的字符串对象移至Java堆中。
JDK1.7改动汇总:
interned String => Java heap
Symbols => native memory
静态变量 => java.lang.Class对象的末尾(位于普通的Java heap中)
注意这些东西,按照JVM规范中,它们还是在方法区中,只是HotSpot将方法区的具体实现,修改到了各个不同的地方而已。
注意:“常量池”如果说的是
SymbolTable
/StringTable
,这俩table自身原本就一直在native memory里,是它们所引用的东西在哪里更有意思。上面说了,7是把SymbolTable
引用的Symbol
移动到了native memory,而StringTable
引用的java.lang.String
实例则从PermGen
移动到了普通Java heap。相关连接:JDK7,HotSpot的String常量池放到了native memory,native memory是什么? - RednaxelaFX的回答 - 知乎
同时,方法区也是线程共享的内存,在运行时如果无法分配足够的内存,则会抛出OutOfMemoryError
异常
扩展:jvm为每个加载的类型(译者:包括类和接口)都创建一个
java.lang.Class
的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。
运行时常量池(Runtime Constant Pool)是方法区的一部分,它用于存放在编译期生成的各种字面量和符号引用,运行时常量池是可动态扩展的。
这里需要总结下运行时常量池的特性:
运行时常量池是用于存放编译期间生成的各种字面量和符号引用。
字面量包括:
有些比较小的基本数据类型会直接嵌入指令中,比如int a=1;会直接编译为 iconst_1,这种值不会放入常量池,也就说常量池找不到1,但是如果是具有final修饰的final int a=1;这个时候1便会放入常量池中,而比较大的基本数据类型是会放在运行时常量池的(比如int a=1234567)。
符号(Symbol)引用包括:
还有一种常量池,叫做常量池技术。比如Integer
,Short
等,在[-128,127],(127可以通过配置扩大)之间能够缓存对象,这种常量池应该和运行时常量池区分开理解,基本类型包装类的常量池技术是通过包装类本身通过在static
代码块中初始化256个对象实现的,而静态变量又是放在方法区中,所以包装类的常量池是放在方法区中的。
Double
和Float
并没有实现常量池,因为不确定[-128,127]中一共应该缓存多少个数
区域名称 | 作用 | 是否线程共享 |
---|---|---|
程序计数器 | 作为当前线程所执行的字节码行号的指示器 | 否 |
虚拟机栈 | 存放局部标量表,操作数栈,...等,用来描述Java方法执行内存模型 | 否 |
本地方法栈 | 用来描述执行本地方法内存模型 | 否 |
Java 堆 | 存放Java对象实例以及数组 | 是 |
方法区 | 存放已加载的内存信息,静态变量,常量等 | 是 |
值得一提的还有直接内存,直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,这块内存称为直接内存,也叫堆外内存。不过它并不搜GC的管理,也不受Java堆大小的限制,只受本机总内存大小以及处理器寻址空间的限制。
对象的创建包含有内存划分等步骤,其中内存划分是通过CAS加上失败重试的方式保证线程安全。另外一种方式是TLAB,TLAB是为每个线程分配单独的内存空间。
在HotSpot虚拟机中,对象在内存中可以分为3块区域:对象头(Header)、实例数据(instance Data)和对齐填充
对象头
包含一个对象运行时的本身数据,比如哈希码。GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等
类型指针(非必须),虚拟机通过类型指针来确定对象是那个类的实例
如果对象是数组,还包括一个记录数组长度的数据
对齐填充
对象访问定位:前面说的类型指针(非必须)是因为对象访问定位包含两种方式
类信息即虚拟机已加载的类型信息,存储在方法区中
上面两种方法各有优势,句柄访问的好处是对象在GC时被移动后,不用修改引用的值,而直接访问的优势是速度更快。HotSpot VM使用的是直接指针访问
Java
内存模型规定所有变量都存储在主内存中,同时每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中变量。不同线程之间无法直接访问对象工作内存中的变量。线程间的变量值通过主内存来完成。
线程对变量的操作主要包括:
- lock 锁定
- unlock 解锁
- read 读取
- load 载入
- use 使用
- assign 复制
- store 存储
- write 写入
其中,Java内存模型规定了以上操作的必须执行的顺序。
返回为happends-before
来看,主要如下:
对于volatile
变量,它具有两个特性:
保证此变量对所有线程的可见性
虽然保证了可见性,但是并不保证要原子性,注意Java大多数操作都不是原子的
禁止指令重排序优化
保证对long/double
的操作是原子的
JVM
保证对于final
修饰的变量,在访问的时候一定初始化完毕了。
happends-before
书写在后面的程序unlock
操作happends-before
锁的lock
操作volatile
变量规则: 对于一个volatile
变量的写操作happends-before
对这个变量的读操作start()
操作happends-before
其他线程在start()
这个线程之前的操作happends-before
这个线程结束后的操作,比如thead.join()
等happends-before
这个线程中断后的操作happends-before
它的finalize()
的开始happends-before
是可传递的上面所说的happends-before
,指的是只要存在happends-before
关系,那么前面的操作对后面的操作是具有可见性和有序性的。
JVM
魔数: Class
文件的头4个字节称为魔数,它的唯一作用是确定这个文件能否成为一个能被虚拟机接受的Class
文件,JVM
魔数为0xCAFEBABE
Class
前4个字节存储为魔数,后4个字节存储为Class
文件的版本号。其中第5和第6个字节是次版本号,第7和第8个字节为主版本号。主版本号从45开始
Class
文件前面依次为魔数,版本号,常量池。
常量池可以理解为Class
文件的资源仓库,主要存放两大类常量:
final
的常量值常量池的作用:Java
在编译的时候,并不想C/C++
一样有link
这一步骤,而是在虚拟机加载Class
文件的时候动态加载,也就是说在Class
文件中不会保存各个方法,字段的最终内存布局信息,因此这些字段,方法的符号引用需要在运行期转换为对应的内存入口地址。
当虚拟机运行的时候,需要从常量池获取对应的符号引用,再在类创建的时候或者运行时解析,翻译到具体的内存地址之中。
常量池之后,紧接着便是一个类访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:Class
是类还是接口,是否定义为public
类型,是否定义为abstract
类型,是否被声明为final
类型。
类索引,父类索引,接口索引用于确定这个Class
的继承关系。
字段表集合用于描述接口或者类中声明的变量
对于字段表,都使用简单的标识字符描述字段类型:
- B: byte
- C: char
- D: double
- F: float
- I : int
- J: long
- S : short
- Z: boolean
- V: void
- L: 引用,如Ljava/lang/Object
数组:[ 如[B 标识byte数组
这里值得记忆的就是冲突了的类型,如long 为 J,boolean 为Z
其中,字段的字符符号,被保存在常量池中,Class
文件通过描述信息只保存索引。
方法表用于描述接口或类中定义的方法
JVM
中,方法特征名包括参数,方法名以及返回值也就是说,在JVM
中,返回值不同的方法是可以重载的
JVM
对finally
字段的处理:在JDK1.5
之前,finally
是通过指令跳转实现,JDK1.5
之后优化为了异常表
JVM
对finally
的处理是通过将finally
中代码拼接到try
和catch
中,也就是说对于下面的代码:
public static int test(){
int x=0;
try {
x=1;
return x;
} catch (Exception e) {
x=2;
return x;
} finally {
x=3;
}
}
但是JVM
执行的效果是类似如下:
public static int test(){
int x=0;
try {
x=1;
return x;
x=3;
} catch (Exception e) {
x=2;
return x;
x=3;
}
}
JVM
没有单独对byte,short,char,boolean
的指令,他们都是使用的int
的指令类型,在使用的时候,byte,short
会被带符号扩展为int
类型,char,boolean
会被零位扩展为int
类型iload,iload_<n>,lload,lload_<n>,fload,fload_<n>,dload,dload_<n>,aload,aload_<n>
store
push,ldc,ldc_w,aconse_null,iconst_m1,
wide
add
sub
mul
div
rem
neg
invokevirtual
: 调用对象的实例方法invokeinterface
: 调用接口方法invokespecial
:调用需要特殊处理的方法invokestatic
:调用静态方法invokedynamic
:调用动态方法A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.