【Java】深入理解Java虚拟机的读书笔记

  1. 云栖社区>
  2. 博客>
  3. 正文

【Java】深入理解Java虚拟机的读书笔记

雅痞士 2018-09-14 23:07:14 浏览1071
展开阅读全文

java虚拟机所管理的内存包括以下几个运行时数据区域
image
【程序计数器】线程私有,是一块较小的内存空间,当前线程执行的字节码的行号指示器,处理分支、循环、跳转、异常处理、线程恢复等基础功能,每个线程都需要有一个独立的程序计数器
【虚拟机栈】线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每个方法被执行的时候同时创建一个栈帧,存放局部变量表、操作栈、动态链接、方法出口等信息。每个方法的调用到完成对应了栈帧从入栈到出栈的过程,局部变量表所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。
【本地方法栈】为使用到的Native方法服务,与虚拟机栈所发挥的作用是非常相似的。
【Java堆】最大的一块,被所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此被称为GC堆。
Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。
【方法区】各个线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。不需要连续的内存,垃圾收集行为在这个区域是比较少出现的,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
运行时常量池是方法区的一部分,class文件中有一项信息是常量池,存放编译期生成的各种字面量和符号引用
【直接内存】不是虚拟机运行时数据区的一部分

对象访问

例:
Object obj=new Object();
Object obj将会反映到Java栈的本地变量表中,作为一个reference类型数据出现,而new Object()将会反映到Java堆中,这块内存的长度是不固定的,方法区存储对象类型、父类等的地址信息。
主流的访问方式有两种:使用句柄和直接指针
句柄:Java堆中划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄包含对象实例数据和类型数据各自具体的地址信息。
image
直接指针:reference中直接存储的就是对象地址
句柄更稳定,因为对象呗移动只需要改变句柄,不需要改变reference
直接指针的方式的最大好处是速度更快,节省了一次指针定位的时间开销

垃圾收集器与内存分配策略(GC)

垃圾收集GC
需要做的事情:哪些内存需要回收、什么时候回收、如何回收

【1.哪些内存需要回收】

线程私有的程序计数器、虚拟机栈、本地方法栈内存分配和回收具备确定性,线程结束了,内存自然就跟随回收了。
而Java堆和方法区则不一样,一个接口中多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间才知道会创造哪些对象,这部分的内存的分配和回收都是动态的,垃圾回收器关注的就是这部分内存。

堆中几乎存放着所有对象实例,垃圾回收器对堆进行回收前,第一件事情要确定对象还有哪些是存活的,哪些是死去的
【引用计数算法】
给对象添加一个引用计数器,每当有一个地方引用它,就+1,当引用失效,计数器-1;任何时刻计数器为0的对象就不可能再被使用。
优点:实现简单,判定效率高,但是【!Java没有选用这种方法!】
因为它很难解决对象之间相互循环引用的问题-----两个不使用的对象互相引用着对方,导致引用计数不为0,于是引用计数算法无法通知GC收集器回收
【!!根搜索算法!!】
Java使用的,基本思路:通过一系列的名为“GC roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连(用图论的话来说就是从GC roots到这个对象不可达)时,则证明此对象是不可用的。
这样就算不同对象之间有关联但是与GC root是不可达的,也会被判定为可回收的对象
那么GC roots的对象是什么呢
可包括:栈帧中的本地变量表引用的对象、方法区中的类静态属性引用的对象、方法区常量引用的对象、Native方法引用的对象

两种方法和引用都分不开关系,引用有强、软、弱、虚引用,引用强度依次减弱。
1.只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
2.将要发生内存溢出之前,列入回收范围并进行第二次回收
3.弱引用只能生存到下一次垃圾收集发生之前
4.虚引用又称幽灵或者幻影引用,最弱,希望能在这个对象呗收集器回收的时候收到一个系统通知罢了

在根搜索算法中不可达的对象,也并非是非死不可的,这是暂时处于缓刑阶段,要真正宣告一个对象死亡,还至少要经过两次标记过程:如果对象进行根搜索后发现没有引用链,将会被第一次标记并且第一次筛选,筛选的条件是此对象是否有必要执行finalize()方法

   没有必要的情况是对象没覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过
   有必要的话,这个对象会被放置在一个名为F-Queue的队列之中,并在稍后有一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里的执行只是触发方法,并不保证等待运行结束。
   finalize()方法是对象逃脱死亡的最后一次机会,如果对象在finalize()中成功拯救自己——即重新与引用链上的任意一个对象建立关联即可,比如把自己赋值给某个类变量或者对象的成员变量,那么第二次GC在对F-Queue中的对象进行标记时ja将会被移除出“即将回收”的集合。*****任何一个对象的finalize()方法只会被调用一次,如果面临下次回收,将不再执行,自救行动将失败。
  重点:该方法不是运行代价巨大,它的工作,try-finally或者其他方式都可以做到更好更及时,不建议使用

【回收方法区】
个别虚拟机又称永久代,主要回收两个部分:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象类似
判定废弃常量简单,但是判定无用类比较复杂
要满足1.该类的所有实例已被回收2.加载该类的java.lang.Class对象没有任何地方被引用
是否对类回收不是和对象一样不用就回收,虚拟机会提供参数控制

【2.如何回收】

垃圾收集算法
【标记-清除算法】
最基础的收集算法,首先标记所有需要回收的对象,在标记完成后统一回收掉标记过的对象
缺点:效率不高;产生大量的内存碎片
【复制算法】
将可用的内存按容量划分为大小相等的两块,每次只用其中一块,当这个用完了,就将还存活的对象复制到另一块上去,然后一次性清理掉当前这块内存空间,这样效率和碎片问题解决,不过内存缩小为原来的一半,代价不小
现在的商业虚拟机采用这种算法回收新生代,将内存分一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块S,当回收时,将E和S1中还存活的对象一次性拷贝到S2上,然后清理E和S1的空间
【标记-整理算法】
复制算法在对象存活率较高的情况下效率变低
和标记-清除前面类似,后续步骤不是直接对可回收对象进行清理,而是让存活对象移动到一端,然后直接清理端边界以外的内存
【!分代收集算法!】
当前商业虚拟机采用的算法,根据对象的存活周期的不用将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,就选用复制算法,而老年代因为对象存活率高,就是用标记类算法回收

【3.垃圾收集器】

内存回收的具体实现
CMS收集器:
以获取最短回收停顿时间为目标的收集器,能让系统停顿时间最短
基于“标记-清除”算法实现的
四个步骤:初始标记、并发标记、重新标记、并发清除
初始标记只是标记GC roots能直接关联的对象,速度很快,并发标记的过程就是进行GC roots 追踪的过程,而重新标记时修正并发标记期间,程序运作导致的变动。
整个过程耗时最长的并发标记和并发清除过程,收集器的线程和用户线程是一起工作的
优点:并发收集、低停顿
缺点:对CPU资源非常敏感,无法处理浮动垃圾,空间碎片

G1收集器:
基于“标记-整理”算法实现
将整个Java堆划分为大小固定的独立区域,并跟踪这些区域里面的垃圾堆积程序,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域
优点:没有空间碎片;精确地控制停顿,即在M毫秒内消耗在垃圾收集上不超过N毫秒

【4.内存分配与回收策略】

Java的自动内存管理最终可以归结为自动化解决了两大问题:给对象分配内存和回收分配给对象的内存
给对象内存分配,就是在堆上分配,主要在新生代的Eden区上
【一.对象优先在Eden分配】
【二.大对象和长期存活的对象直接进入老年代】
需要大量连续内存空间的Java对象 避免在Eden区和S1S2区发生大量的内存拷贝
【三.动态对象年龄判定】
【四.空间分配担保】

Java内存模型与线程

JMM,足够严谨又要足够宽松,达到充分利用硬件(寄存器、高速缓存等)
内存模型的目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,不包括线程私有的变量。
规定了所有的变量都存储在主内存中(main memory)中
每条线程还有自己的工作内存(working memory),保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作必须在工作内存进行,不能直接读写主内存变量。
不用线程之间无法直接访问对方的工作内存的变量,线程之间变量的传递需要在主内存中完成,三者关系如下图
image
主内存勉强对应为Java堆中对象的实例数据部分
工作内存对应虚拟机栈中的部分区域
更低的层次来看,主内存是硬件的内存,工作内存为了速度,优先存储于寄存器和高速缓存中
【内存之间的交互】
八大操作
lock锁定、unlock解锁、read读取、load载入、use使用、assign赋值、store存储、write写入
把一个变量从主内存复制到工作内存:顺序read load
工作内存-》主内存:顺序store write
【volatile型变量的特殊规则】
当一个变量被定义为volatile之后,具备两大特性
1.保证此变量对所有线程的可见性,即一条线程修改了这个变量,新值对于其他线程是立即可知的,普通变量做不到这点,因为还需要在主内存进行回写,注意,这不能代表基于volatile变量的运算在并发下是安全的,这是因为Java里的运算并非原子操作,volatile变量的运算在并发下并不安全。
2.禁止指令重排序优化,指令重排序会造成代码被提前执行

volatile同步机制性能优于锁,但虚拟机对锁有很多消除和优化,也没有优很多
【原子性、可见性和有序性】
Java的内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的,哪些操作实现了这三大特性
原子性(操作是不可中断的,要么全部执行成功要么全部执行失败):read、load、assign、use、store和write,还有lock和unlock来满足更大范围的原子性保证。
可见性(当一个线程修改了共享变量的值,其他线程立即得知):通过将新值同步回主内存再刷新变量值来实现,volatile、synchronized和final
有序性(本线程所有操作有序,观察另一线程,无序):volatile、synchronized关键字来实现

线程安全

当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
代码本身需要封装所有必要的正确性保障手段,让调用者不要关心多线程的问题
Java中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
不可变:肯定安全
绝对线程安全:所有的方法都被synchronized修饰
相对线程安全:就是我们通常意义上的安全,能保证对这个对象单独的操作时线程安全的,一些特定的连续调用,需要额外的同步手段来确保,例如vector、hashtable等
线程兼容:对象本身不安全,在调用端正确使用同步手段来保证安全,例如ArrayList、hashmap等
线程对立:采取了同步措施也没办法并发使用

【如何实现线程安全】

和代码的编写有大关系,虚拟机提供的同步和锁机制也能起作用
1、互斥同步
最常见,同步是指在多线程并发访问共享的数据时,确保共享数据同一时刻只被一条线程使用
互斥是实现同步的一种手段,包括临界区、互斥量和信号量
互斥为因,同步是果,互斥是方法,同步是目的
具体实现:synchronized关键字
2.非阻塞同步
互斥的最主要问题就是线程阻塞和唤醒所带来的性能问题,一种乐观的并发策略:先进行操作,如果没有其他线程竞争,那么操作就成功,有的话就补偿,最常见的补偿就是重试直到成功,不需要挂起线程,称为非阻塞,这种策略需要操作和冲突检测都带有原子性,怎么保证原子性呢,不能再用加锁了,所以要靠硬件,这里就不展开了
3.无同步方案
要保证线程安全,并不是一定要同步哦。
如果一个方法不涉及共享数据,自然无需同步,天生就是线程安全的。例如可重入代码和线程本地存储

网友评论

作者关闭了评论
雅痞士
+ 关注