原文连接:a870439570.github.io/interview-d… #思惟导图 java
1. 运行时数据区域
Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域有各自的用途,以及建立和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而创建和销毁。
线程私有的:虚拟机栈,本地方法栈,程序计数器
线程共享的 方法区,堆
程序计数器
程序计数器是一块较小的内存空间,它的做用能够看做是当前线程所执行的字节码行号指示器,在虚拟机的概念模型里,字节码解释器工做时 就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都须要这个计数器来完成。(若是正在执行的是本地方法则计数器为空)。
Java虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:每一个方法被执行的时候都会建立一个栈帧用于存储局部变量表,操做栈,动态连接,方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
本地方法栈
本地方法栈与 Java 虚拟机栈相似,它们之间的区别只不过是本地方法栈为本地方法服务。git
Java 堆
Java
堆是整个虚拟机所管理的最大内存区域,全部的对象建立都是在这个区域进行内存分配。
这块区域也是垃圾回收器重点管理的区域,因为大多数垃圾回收器都采用分代回收算法
,全部堆内存也分为 新生代
、老年代
,能够方便垃圾的准确回收。
方法区
方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量
,即时编译器编译后的代码等。和Java堆同样不须要连续的内存,而且能够动态扩展。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,可是通常比较难实现。
运行时常量池
运行时常量池是方法区的一部分。class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各类字面量和符号引用,会在类加载后放入这个区域。
直接内存
直接内存并非虚拟机运行时数据区域的一部分。
在 JDK 1.4 中新加入了 NIO 类,它可使用 Native 函数库直接分配堆外内存,而后经过 Java堆里的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在堆内存和堆外内存来回拷贝数据。
2. Minor GC和Full GC
Minor GC:指发生在新生代的垃圾收集动做,由于 Java 对象大多都具 备朝生夕灭的特性,因此Minor GC
很是频繁,通常回收速度也比较快。
Major GC或Full GC:指发生在老年代的 GC,出现了 Major GC,常常 会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里 就有直接进行 Major GC
的策略选择过程) 。MajorGC
的速度通常会比 Minor GC 慢 10 倍以上。
Minor GC触发机制
当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引起GCgithub
Full GC触发机制:
当年老代满时会引起Full GC,Full GC将会同时回收年轻代、年老代,
当永久代满时也会引起Full GC,会致使Class、Method元信息的卸载
3. Java中的四种引用
强引用,软引用,弱引用,虚引用算法
强引用
就是指在程序代码中广泛存在的,相似Object obj=new Object()这类的引用,只要强引用还存在,垃圾回收期永远不会回收掉被引用的对象数据库
软引用
用来描述一些还有用,但并不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出前,将会把这些对象列进回收范围以内并进行第二次回收,若是这这次回收仍是没有足够的内存,才会抛出内存溢出。数组
弱引用
用来描述非必须的对象,可是它的强度比软引用更弱一下,被弱引用关联的对象,只能生存到下一次垃圾收集发生以前。当垃圾收集器工做时,不管当前内存是否足够,只会回收被弱引用关联的对象安全
虚引用
被称为幽灵引用或幻引用,是最弱的一种引用关系,一个对象是否有虚引用的存在,彻底不会对其它生存时间构成影响,也没法经过虚引用来取得一个实列。为一个对象设置虚引用的目的就是在对象被回收时收到一个系统通知。bash
4. 垃圾收集算法
1.Serial收集器
一个单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工做。在进行垃圾收集时必须暂停其它全部的工做线程,直接到结束。(Stop The Word)这项工做是虚拟机在后台自动发起和完成的。
JDK1.3以前是新生代收集的惟一选择。
它依然是虚拟机运行在Client模式下的默认新手代收集器,简单而高效。
2. ParNew收集器
Serial收集器的多线程版本,使用多条线程收集。其他的和Serial同样,是许多运行在Server模式下的虚拟机首选新生代收集器。且目前除了Serial收集器,只有它能够与CMS收集器配合工做微信
3.Parallel Scavenge收集器
它是一款新生代收集器。使用复制算法收集,又是并行的多线程收集器
特色是达到一个可控制的吞吐量,也被称为“吞吐量优先”收集器。
4.Serial Old收集器
它是Serial收集器的老年代版本,是一个单线程收集器,使用标记-整理算法收集。
主要意义是给Client模式下虚拟机使用。若是是Server模式,则有两种用途,一是在JDK1.5以前与Parallel Scavenge收集器搭配使用。二是做为CMS收集器的后背预案
5.Parallel Old收集器
它是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。JDK1.6才开始提供。网络
6.CMS收集器
是一种以获取最短回收停顿时间的为目标的收集器。基于标记-清楚算法实现。
运做过程分为四个阶段。初始标记,并发标记,从新标记,并发清除。
初始标记和并发标记仍然须要"Stop The Word".初始标记只是记录下GC Roots能直接关联到对象,速度快。并发标记就是进行GC Roots Tracing过程。从新标记修正并发标记期间因程序继续运做致使标记产生变更的一部分对象的标记记录。整个过程耗时最长是并发标记和并发清除过程。
优势是并发收集,低停顿。缺点是:对CPU资源很是敏感,没法处理浮动垃圾。收集结束时会产生大量空间碎片
7.G1收集器
当前收集器技术最前沿成果之一。将整个Java堆分为多个大小相等的独立区域。虽然保留新生代和老年代,但它们再也不是物理隔离,都是一部分不须要连续的集合。
特色是并行与并发充分利用CPU缩短停顿时间。分代收集,空间整合不会产生内存空间碎片,可预测的停顿。有计划的避免回收整个Java堆。
运行大体分为:初始标记,并发标记,最终标记,筛选回收。
标记-清除算法
算法分为标记和清除两个阶段。首先先标记全部要被回收的对象,标记完成后再统一清除被标记的对象。
主要缺点有两个,
一是效率问题,标记和清除的过程效率都不高。二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多,可能会致使,当程序在之后的运行过程当中须要分配较大的对象时没法找到足够的连续内存,而不得不提早出发另外一次垃圾收集动做
复制算法
为了解决效率问题,一种复制收集的算法出现了。它将可用内存按容量划分为大小相等的两块,每次只用其中的一块。当这一块内存用完,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中一块进行内存回收,内存分配时也就不用内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半未免过高了一点。
标记-整理算法
复制手机算法在对象存活率较高的时要执行多的复制操做,效率将会变低。更关键的是,若是不想浪费50%的空间,就须要额外的空间进行分配担保,以应对被使用的内存中对象都100%存货的极端状况,因此在老年代通常不能直接选用这种算法。根据老年代的特色,有人提出了另外一种 标记-整理的算法,标记过程仍然与 标记-清楚算法同样。但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象向一端移动,而后直接清理掉端边界之外的内存
分代收集算法
根据对象的存活周期的不一样将内存划分为几块。通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收
5. 内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,若是启动本地线程分配缓冲,将按线程的优先级在TLAB上分配。少数状况也可能分配在老年代中,分配的规则并非百分之白固定,其细节取决于当前使用的是哪种垃圾回收期组合,还有虚拟机中于内存相关的参数设置。
对象优先在Eden区分配
对象一般在新生代的Eden区进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,与Minor GC对应的是Major GC、Full GC。
Minor GC:指发生在新生代的垃圾收集动做,很是频繁,速度较快。
Major GC:指发生在老年代的GC,出现Major GC,常常会伴随一次Minor GC,同时Minor GC也会引发Major GC,通常在GC日志中统称为GC,不频繁。
Full GC:指发生在老年代和新生代的GC,速度很慢,须要Stop The World。
大对象直接进入老年代
须要大量连续内存空间的Java对象称为大对象,大对象的出现会致使提早触发垃圾收集以获取更大的连续的空间来进行大对象的分配。虚拟机提供了-XX:PretenureSizeThreadshold参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。
长期存活的对象进入老年代
每一个对象有一个对象年龄计数器,与前面的对象的存储布局中的GC分代年龄对应。对象出生在Eden区、通过一次Minor GC后仍然存活,并可以被Survivor容纳,设置年龄为1,对象在Survivor区每次通过一次Minor GC,年龄就加1,当年龄达到必定程度(默认15),就晋升到老年代,虚拟机提供了-XX:MaxTenuringThreshold来进行设置。
动态对象年龄判断
对象的年龄到达了MaxTenuringThreshold能够进入老年代,同时,若是在survivor区中相同年龄全部对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就能够直接进入老年代。无需等到MaxTenuringThreshold中要求的年龄。
具体代码以下:
public class AllocationTest {
private static final int _1MB = 1024 * 1024;
/*
* -Xms20M -Xmx20M -Xmn10M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+UseSerialGC
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution
* */
public static void testTenuringThreshold2 () {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
public static void main(String[] args) {
test PretenureSizeThreshold2();
}
}
复制代码
空间分配担保
发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代全部对象的总和,若成立,则说明Minor GC是安全的,不然,虚拟机须要查看HandlePromotionFailure的值,看是否运行担保失败,若容许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改为一次Full GC,以上是JDK Update 24以前的策略,以后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,不然将进行Full GC。
冒险是指通过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,因此须要老年代进行分配担保,把survivor区没法容纳的对象直接进入老年代。
回收方法区
不少人任务方法区是没有垃圾回收的,Java虚拟机规范中确实说过能够不要求虚拟机在方法区实现垃圾收集,而在方法去进行垃圾收集的性价比通常比较低,在堆中,由其是在新生代中,常规应用进行一次垃圾收集通常能够回收70%~96%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾主要回收两部份内容:废弃常量和无用的类。
回收废弃常量于回收Java堆
中的对象很是类似。以常量池中字面量的回收为列,假如一个字符串“abc
"已经进入常量池中,可是当前系统没有任何一个String对象叫作”abc
"的,换句话就是没有任何Sting对象引用常量池中的"abc",也没有其它地方引用了这个字面变量,若是这时候发生内存回收,并且必要的话,这个“abc
"常量就会被系统请出常量池,常量池中的其它类,接口,方法,字段的符号引用也与此相似。
image.png
Java中对象访问是如何进行的
对象访问在Java中无处不在,即时是最简单的访问也会涉及到Java栈,Java堆,方法区这三个最重要的内存区域之间的关系。
Object obj=new Object();
复制代码
假设这段代码出如今方法体中, 那吗“Object obj
”这部分的语义将会反应到Java栈
的本地变量中,做为一个reference
类型数据出现。而“new Object()
”这部分的语义将会反应到Java堆
中,造成一块存储了Object类型全部实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不一样,这块内存的长度是不固定的。
另外,在Java堆中还必须包含能查找到此对象类型数据(如对象类型,父亲,实现的接口,方法等)的地址消息,这些类型数据则存储在方法区中。
怎样判断对象是否存活
是否使用引用计数法?不少判断对象存活的算法是这样的,给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器减1;
任什么时候刻计数器都为0的对象就是不可能再被使用的。客观的来讲,引用计数法的实现简单,断定效率也很高,在大部分状况下是一个不错的算法,也有一些著名的案例,列如微软的COM技术,可是,在Java语言中没有选用引用技术发来管理内存,其中最主要的缘由是由于它很难解决对象之间的互循环引用问题。
摘抄自<<深刻理解Java虚拟机>>一书中的原话
根搜索算法:Java是使用根搜索算法判断对象是否存活的。
这个算法的思路就是经过一系列的名为“GC roots"的对象做为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象的GC roots没有任何引用链相连时,则证实此对象是不可用的。以下图所示,对象object5,object6,object7虽然相互关联,可是他们的GC roots是不可达到的,因此它们将会被断定是可回收的对象。
image.png
做为GC roots的几种对象
虚拟机栈(栈中的本地变量表)中的引用对象。
方法区中的类静态属性引用对象。
方法区中的常量引用的对象。
本地方法中JNI(即通常说的native方法)的引用的对象。
6. 虚拟机类加载机制
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为链接(Linking)
在这里输入图片标题
加载、验证、准备、初始化和卸载这5个阶段的顺序是肯定的,类的加载过程必须按照这种顺序循序渐进地开始,而解析阶段则不必定:它在某些状况下能够在初始化阶段以后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
什么时候开始类加载的第一个阶段
java虚拟机规范中并无进行强制约束,这点能够交给虚拟机的具体实现来自由把握。可是对于初始阶段,虚拟机规范则是严格规定了有且只有5种状况必须当即对类进行初始化(而加载,验证,准备天然须要再次以前开始)
遇到new,getstatic,pustatic
或invokestatic
这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见Java代码场景是:使用new关键字实例化对象,读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
对类进行反射调用时,若是类没有进行过初始化,则须要先触发其初始化。
当初始化一个类时,若是发现父类尚未初始化,则须要先触发父类初始化。
当虚拟机启动时,用户指定一个执行的主类,虚拟机会先初始化这个主类。
当使用jdk1.7动态语言支持时,若是一个实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。
类的加载过程
1. 加载
在加载阶段(能够参考java.lang.ClassLoader的loadClass()方法),虚拟机须要完成如下3件事情:
经过一个类的全限定名来获取定义此类的二进制字节流(并无指明要从一个Class文件中获取,能够从其余渠道,譬如:网络、动态生成、数据库等);
将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构;
在内存中生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据的访问入口;
加载阶段和链接阶段(Linking)的部份内容(如一部分字节码文件格式验证动做)是交叉进行的,加载阶段还没有完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动做,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的前后顺序。
2. 验证
验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
验证阶段大体会完成4个阶段的检验动做:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object以外。
字节码验证:经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的
符号引用验证:确保解析动做能正确执行。
验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在堆中。其次,这里所说的初始值“一般状况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
复制代码
那变量value在准备阶段事后的初始值为0而不是123.由于这时候还没有开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,因此把value赋值为123的动做将在初始化阶段才会执行。
至于“特殊状况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,因此标注为final以后,value的值在准备阶段初始化为123而非0.
4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
5. 初始化
若是一个类被主动引用,就会触发类的初始化。
在java中,直接引用的状况有,经过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。经过反射方式执行以上三种行为。初始化子类的时候,会触发父类的初始化。做为程序入口直接运行时(也就是直接调用main方法)。除了以上四种状况,其余使用类的方式叫作被动引用,而被动引用不会触发类的初始化
6. 使用
类的使用包括主动引用和被动引用
被动引用:引用父类的静态字段,只会引发父类的初始化,而不会引发子类的初始化。定义类数组,不会引发类的初始化。引用类的常量,不会引发类的初始化。
7. 卸载
知足下面的状况,类就会被卸载:该类全部的实例都已经被回收,也就是java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。该类对应的java.lang.Class对象没有任何地方被引用,没法在任何地方经过反射访问该类的方法。
若是以上三个条件所有知足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
总结
对象基本上都是在jvm的堆区中建立,在建立对象以前,会触发类加载(加载、链接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完以后会在合适的时候被jvm垃圾收集器回收。
对象的生命周期只是类的生命周期中使用阶段的主动引用的一种状况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。
类的生命周期
jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周期中扮演着比较重要的角色:
方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫作方法区。
常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
堆区:用于存放类的对象实例。
栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会建立一个栈帧存放这些数据,当方法调用完成时,栈帧消失,若是方法中调用了其余方法,则继续在栈顶建立新的栈桢。
微信图片_20180704125805.png
类加载器
经过一个类的全限定名来获取描述此类的二进制字节流,这个动做放到java虚拟机外部去实现。以便让应用程序本身决定如何去获取所须要的类。实现各动做的代码模块称为“类加载器”。
比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义,不然即便这两个;诶是来源同一个class文件,但类加载器不一样,他们也不相等。
启动类加载器
这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,而且是虚拟机识别的类库。用户没法直接使用。
扩展类加载器
这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的全部类库。用户能够直接使用。
应用程序类加载器
这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户能够直接使用。若是用户没有本身定义类加载器,默认使用这个
自定义加载器
用户本身定义的类加载器。
双亲委派模型
若是一个类加载器收到类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器完成。每一个类加载器都是如此,只有当父加载器在本身的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试本身去加载。
优势
Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,不管哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,所以Object类在程序的各类类加载器环境中都是同一个类。
相反,若是没有双亲委派模型而是由各个类加载器自行加载的话,若是用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不一样的Object类,程序将混乱。所以,若是开发者尝试编写一个与rt.jar类库中重名的Java类,能够正常编译,可是永远没法被加载运行。
7. happens-before原则
概述
咱们没法就全部场景来规定某个线程修改的变量什么时候对其余线程可见,可是咱们能够指定某些规则,这规则就是happens-before,从JDK 5 开始,JMM就使用happens-before的概念来阐述多线程之间的内存可见性。
在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必须存在happens-before关系。 happens-before原则很是重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,咱们解决在并发环境下两操做之间是否可能存在冲突的全部问题。下面咱们就一个简单的例子稍微了解下happens-before ;
i = 1; //线程A执行
j = i ; //线程B执行
复制代码
j 是否等于1呢?假定线程A的操做(i = 1)happens-before线程B的操做(j = i),那么能够肯定线程B执行后j = 1 必定成立,若是他们不存在happens-before原则,那么j = 1 不必定成立。这就是happens-before原则的威力。
原则定义
若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。
两个操做之间存在happens-before关系,并不意味着必定要按照happens-before原则制定的顺序来执行。若是重排序以后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
规则以下
程序次序规则
一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做;
锁定规则
一个unLock操做先行发生于后面对同一个锁额lock操做;
volatile变量规则
对一个变量的写操做先行发生于后面对这个变量的读操做;
传递规则
若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C;
线程启动规则
Thread对象的start()方法先行发生于此线程的每一个一个动做;
程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则
线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始;
8. 对象
Java中建立对象的5种方式
使用new关键字 → 调用了构造函数
Employee emp1 = new Employee();
复制代码
使用Class类的newInstance方法→ 调用了构造函数
<!--使用Class类的newInstance方法建立对象。这个newInstance方法调用无参的构造函数建立对象。-->
Employee emp2 = (Employee) Class.forName("org.programming.mitra.exercises.Employee" ).newInstance();
复制代码
使用Constructor类的newInstance方法 → 调用了构造函数
<!--和Class类的newInstance方法很像, java.lang.reflect.Constructor类里也有一个newInstance方法能够建立对象-->
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
复制代码
使用clone方法→ 没有调用构造函数
<!--不管什么时候咱们调用一个对象的clone 方法,jvm就会建立一个新的对象,将前面对象的内容所有拷贝进去。用clone 方法建立对象并不会调用任何构造函数。-->
<!--要使用clone 方法,咱们须要先实现Cloneable接口并实现其定义的clone 方法-->
Employee emp4 = (Employee) emp3.clone();
复制代码
使用反序列化→ 没有调用构造函数
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj" ));
Employee emp5 = (Employee) in.readObject();
复制代码
Java 对象生命周期
对象的整个生命周期大体能够分为7个阶段:
建立阶段(Creation)
在建立阶段系统经过下面的几个步骤来完成对象的建立过程
1,为对象分配存储空间
2,开始构造对象
3,从超类到子类对static成员进行初始化
4,超类成员变量按顺序初始化,递归调用超类的构造方法
5,子类成员变量按顺序初始化,子类构造方法调用
一旦对象被建立,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
复制代码
应用阶段(In Use)
对象至少被一个强引用持有着
复制代码
不可视阶段(Invisible)
当一个对象处于不可见阶段时,说明程序自己再也不持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的做用域了。
复制代码
不可到达阶段(Unreachable)
对象处于不可达阶段是指该对象再也不被任何强引用所持有
与“不可见阶段”相比,“不可见阶段”是指程序再也不持有该对象的任何强引用,这种状况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会致使对象的内存泄露状况,没法被回收。
复制代码
可收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”而且垃圾回收器已经对该对象的内存空间从新分配作好准备时,则对象进入了“收集阶段”。
若是该对象已经重写了finalize()方法,则会去执行该方法的终端操做。
复制代码
终结阶段(Finalized)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
复制代码
对象空间从新分配阶段
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象完全消失了,称之为“对象空间从新分配阶段”。
复制代码
对象内存分配
类加载检查经过后,虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后能够彻底肯定,对象内存分配任务就是把一块肯定大小的内存从堆中划分出来。
指针碰撞法
若是堆中内存是绝对规整的。用过的内存放一边,空闲的放一边,中间放着一个指针做为分界点的指示器,那所分配内存就是把指针向空闲一边移动一段与对象大小相等的距离,即为“指针碰撞”
空闲列表法
若是堆中内存不规整,已使用内存和未使用内存相互交错,虚拟机就必须一个列表,记录哪些内存块可用,在分配时从列表中找到一块足够大空间划分给对象,并更新列表上记录,即为“空闲列表”
总结
选择何种分配方式,由堆是否规整决定,而堆是否规整由采用的垃圾收集器是否有压缩整理功能决定。
使用Serial,ParNew等带Compactg过程的收集器时,系统采用指针碰撞法
使用CMS这种基于Mark-Sweep算法的收集器时,系统采用空闲列表法
对象的访问定位
Java程序须要经过栈上的references数据来操做堆上的具体对象。由于referencesz只是指向对象的一个引用,并无定义这个引用经过何种方式去方位堆中对象的具体位置。因此对象访问方式取决于虚拟机实现而定的。
目前主流的访问方式有使用句柄和直接指针两种。
句柄定位
使用句柄访问时,Java堆中会划分出一块内存来做为句柄池,references中存储的就是对象的句柄地址。句柄中包含对象实列数据与类型数据各组的具体地址信息 references->句柄池->java堆
屏幕截图.png
直接指针定位
若是是直接指针访问,Java堆的布局就必须考虑如何放置访问类型数据相关。
屏幕截图.png
各自优势
句柄访问最大好处就是references中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是广泛行为)时只会改变句柄中的实列数据指针,references自己不须要修改。
直接指针访问的最大好处是速度快,节省了一次定位的实时间开销。
9. 常量池总结
全局字符串池
string pool也有叫作string literal pool
全局字符串池里的内容是在类加载完成,通过验证,准备阶段以后在堆中生成字符串对象实例,而后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是咱们常说的用双引号括起来的)的引用(而不是驻留字符串实例自己),也就是说在堆中的某些字符串实例被这个StringTable引用以后就等同被赋予了”驻留字符串”的身份。这个StringTable在每一个HotSpot VM的实例只有一份,被全部的类共享。
class文件常量池
class constant pool
咱们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各类字面量(Literal)和符号引用(Symbolic References)。
字面量就是咱们所说的常量概念,如文本字符串、被声明为final的常量值等。
符号引用是一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可(它与直接引用区分一下,直接引用通常是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。通常包括下面三类常量。类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
常量池的每一项常量都是一个表,一共有以下表所示的11种各不相同的表结构数据,这每一个表开始的第一位都是一个字节的标志位(取值1-12),表明当前这个常量属于哪一种常量类型。
屏幕截图.png
运行时常量池(runtime constant pool)
当java文件被编译成class文件以后,也就是会生成我上面所说的class常量池,那么运行时常量池又是何时产生的呢?
jvm在执行某个类的时候,必须通过加载、链接、初始化,而链接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每一个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并非对象的实例,而是对象的符号引用值。而通过解析(resolve)以后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是咱们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
举个实例来讲明一下:
public class HelloWorld {
public static void main(String []args) {
String str1 = "abc" ;
String str2 = new String("def" );
String str3 = "abc" ;
String str4 = str2.intern();
String str5 = "def" ;
System.out.println(str1 == str3);//true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true
}
}
复制代码
回到上面的那个程序,如今就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值
而后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,而且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象 与上面那个是不一样的实例
当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,因此str3的引用地址与以前的那个已存在的相同
str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,若是没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,因此返回上面在new str2的时候添加到StringTable中的 “def”引用值
上面程序的首先通过编译以后,在该类的class常量池中存放一些符号引用,而后类加载以后,将class常量池中存放的符号引用转存到运行时常量池中,而后通过验证,准备阶段以后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),而后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。
总结
1.全局常量池在每一个VM中只有一份,存放的是字符串常量的引用值。
2.class常量池是在编译的时候每一个class都有的,在编译阶段,存放的是常量的符号引用。
3.运行时常量池是在类加载完成以后,将每一个class常量池中的符号引用值转存到运行时常量池中,也就是说,每一个class都有一个运行时常量池,类在解析以后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
class文件常量池和运行时常量池
最近一直被方法区里面存着什么东西困扰着?
1.方法区里存class文件信息和class文件常量池是个什么关系。
2.class文件常量池和运行时常量池是什么关系。
复制代码
方法区存着类的信息,常量和静态变量,即类被编译后的数据。这个说法实际上是没问题的,只是太笼统了。更加详细一点的说法是方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用。
符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。
屏幕截图.png
屏幕截图.png
能够看到在方法区里的class文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。
屏幕截图.png
屏幕截图.png
class文件常量池和运行时常量池的关系以及区别
class文件常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量。
运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里,在class文件常量池的符号引用有一部分是会被转变为直接引用的,好比说类的静态方法或私有方法,实例构造方法,父类方法,这是由于这些方法不能被重写其余版本,因此能在加载的时候就能够将符号引用转变为直接引用,而其余的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。
总结:
方法区里存储着class文件的信息和运行时常量池,class文件的信息包括类信息和class文件常量池。
运行时常量池里的内容除了是class文件常量池里的内容外,还将class文件常量池里的符号引用转变为直接引用,并且运行时常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中,这里String常量池是包含在运行时常量池里的,但在jdk1.8后,将String常量池放到了堆中。
10. 类文件结构
1.class类文件结构
class 文件结构是一组以8位字节为基础单位的二进制流。存储的内容几乎所有是程序运行的必要数据,无空隙。
若是须要占用8位字节以上空间的数据,则按照高位在前的方式分割成若干个8位字节进行存储。
class文件结构采用一种相似C语言体系的伪结构体系,这种伪结构只有无符号数和表两种数据类型。
魔数与Class文件的版本
class文件的头4个字节称为魔数,惟一做用是肯定这个文件是否为一个能被虚拟机接受的文件。
魔数值能够自由选择,只要未被普遍使用同事不会引发混淆。
紧接着魔数的4个字节是class文件版本号,第5和第6个字节是次版本你好,7和8个字节是class文件版本号(java版本号从45开始。jdk7是51.0)
常量池
主次版本号以后的是常量池,常量池能够理解为class文件中的资源仓库。
class文件结构中只有常量的容量技术是从1开始
常量池主要存放两大类常量:字面量(如文本字符串,finald常量)和符号引用(类和接口的全限定名,字段的名称和描述符,方法的名称和描述符)。
虚拟机运行时,需从常量池获取对应的符号引用,再在类建立时或运行将诶系会,翻译到哪具体的内存地址中。
访问标志
常量池以后的两个字节表明访问标志,用于识别class是类仍是接口,是否为public类型或abstract类型等等。
类索引,父类缩影与接口索引集合
这三项按顺序排列在访问标志以后,class文件中由这三项来肯定整个类的继承关系。
类索引用于肯定类的全限定名,父类索引用于肯定类的父类权限定名。接口索引集合描述类实现了哪些接口
字段表集合
用于描述接口或类中声明的变量。字段包裹类级别的变量和实列变量。不包括方法内部声明的局部变量。
方法表集合
方法表结构依次包括访问标志,名称索引,描述索引,属性集合.
2.字节码指令简介