赞
踩
上面写了Java运行时内存的各个区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性(当方法结束或者线程结束时,内存自然随着线程回收了)
垃圾回收出现在堆和方法区
垃圾: 没有任何引用能指向的对象
垃圾回收: java虚拟机启动,就会创建一些垃圾回收线程,来并发的执行垃圾回收工作
给对象增加一个引用计数器,多一个地方引用,计数器+1;少一个就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”
缺陷: 无法解决循环引用问题(java中没有采用)
循环引用:
上面提到Java并不采用引用计数法来判断对象是否已“死”,而采用“可达性分析”来判断对象是否存活(同样采用此法的还有C#,Lisp-最早的一门采用动态内存分配的语言)
核心思想: 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:
GC Roots: 根结点(程序运行起始的位置引用到的对象)
引用链: 从GC Roots往下搜索,走过的路径
如果从GC Roots,无法到达的对象,就是垃圾
对象Object5-Object7之间虽然彼此还有关联,但是他们到GC Roots是不可达的,因此他们会被判定为可回收对象
在Java语言中,可作为GC Roots的对象包含下面几种:
从上面可以看出“引用”的功能,除了最早我们使用它(引用)来查找对象,现在我们还可以使用“引用”来判断死亡对象了。所以在JDK1.2时,Java对引用的概念做了扩充,将引用分为强引用、软引用、弱引用和虚引用四种,这四种引用的强度依次递减
四种引用类型(了解)
1.强引用: new对象的都是强引用,只要强引用还存在,垃圾回收器永远不会回收被引用的对象实例
2.软引用: 用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常
3.弱引用: 也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。也就是说:当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象
4.虚引用: 它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到系统一个通知
整体看垃圾回收:
jvm启动,就会创建一些垃圾回收线程(清理工),执行垃圾回收工作:
使用工具(垃圾回收器)按照一定的方式(垃圾回收算法)来清理垃圾
上面我们已经将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了
“标记-清除”算法是最基础的收集算法。
分为“标记”和“清除”两个阶段:
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
存在两个缺陷:
(1)效率问题: 标记和清除这两个过程的效率都不高
(2)空间问题: 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集
“复制”算法是为了解决“标记-清理”的效率问题。 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。
好处: 每次都是对整个半区进行内存回收,内存分配时就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可,实现简单,运行高效
缺点: 内存利用率比较低,只有50%
新生代中98%的对象都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用到的Survivor空间
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保
HotSpot默认Eden与Survivor的大小比例是8:1,也就是说Eden:Survivor From:Survivor To=8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象
HotSpot实现的复制算法流程如下:
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法
针对老年代的特点,提出了一种“标记-整理算法”。标记过程中仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是将存活对象移动到连续的空间,再清除剩余空间
优点:不会出现内存碎片的问题
分代算法和上面讲的3种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收
当前JVM垃圾收集都采用的是“分代收集”算法,这个算法并没有新思想,只是根据对象存货周期的不同将内存划分为几块
在堆中,根据对象创建,及回收的特点,分为了两块区域:
(1)新生代:(对象朝生夕死:很快的创建,又很快变为不可用的垃圾)
方法调用:创建栈帧、入栈、
方法返回:出栈,里边局部变量指向的对象在方法返回就不可用
采取的算法:复制算法(复制算法(存活对象大多数情况下不多))
默认划分为E:S:S=8:1:1,空间利用率就是90%
默认每次使用E区和一块S区来保存对象,另一块S区留空
gc的时候,把存活对象复制到另一块留空的S区
可能存活对象需要的空间,S区还不够放:由老年代进行担保,如果放不下,就放到老年代
Eden(E区),2个Survivor(S区)
(2)老年代: 对象可能长期存活,对象存活率高,没有额外空间对它进行分配担保
采取的算法:标记清除算法,标记整理算法
对象什么时候进入老年代/新生代?
新生代gc(Minor GC) vs 老年代GC(Major GC)
Minor GC: 对我们自己写的程序影响比较小,回收速度快
对程序有何影响?垃圾回收是在垃圾回收线程中并发执行,可能导致用户线程暂停(Stop the world,STW)
Major GC: 对程序影响大,回收速度慢
如果说上面我们讲的收集算法时内存回收的方法论,那么垃圾收集器就是内存回收的具体体现
垃圾收集器的作用:
垃圾收集器是为了保证内存能够正常,持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存对象
下面图中这些收集器是HotSpot虚拟机随着不同版本推出的重要的垃圾收集器:
上图展示了7种作用域不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。
在讲具体的收集器之前我们先来明确三个概念:
Java应用程序来说:大略上,可以分为2种类型:
ParNew(新生代)+CMS(老年代)
G1
用户体验优先:每次的STW时间最少;总的STW时间可能变多
Parallel Scavenge+Parallel Old
吞吐量优先:总的STW(用户线程暂停)时间最少;单词的STW时间可能变多
ParNew和Parallel Scavenge都是新生代收集器,使用复制算法
Parallel Old是老年代收集器:标记-整理算法
特性:
针对效率不高:JVM实际对这个算法有进一步的优化
分为四个阶段:(名称,大概作用,特点(是否STW))
第一个和第三个阶段需要停止用户线程
第二个和第四个阶段可以并发执行
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
CMS收集器优点: CMS是一款优秀的收集器,并发收集、低停顿
缺陷:
G1垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收
G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩
G1垃圾回收器回收region的时候基本不会STW,而是基于most garbage优先回收
算法:整体看基于“标记-整理”算法,局部看是基于“复制”算法(将多个E区,多个S区,存活对象,复制到另一块空白的S区)
全堆收集器:用户体验优先
内存划分: 把堆划分为相等的很多个区域,每个区域根据需要设置为E(Eden)、S(Survivor)、T(Tenured老年区)。结果如下图:
一个region有可能属于Eden,Survivor或者Tenured内存区域。
图中的E表示该region属于Eden内存区域
S表示属于Survivor内存区域
T表示属于Tenured内存区域
图中空白的表示未使用的内存空间
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象,即大小超过一个region大小的50%的对象
年轻代垃圾收集:
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法,把Eden区和Survivor区的对象复制到新的Survivor区域
老年代垃圾收集:
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同,也分为四个阶段:
Garbage First:优先回收。老年代存活率低或没有存活的,就直接回收
G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。如果应用追求低停顿,G1可以作为选择;如果应用追求吞吐量,G1并不带来特别明显的好处
JVM定义了一种Java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。 在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,有可能导致在一套平台上并发完全正常,而在另一套平台上并发访问经常出错
作用:不同硬件及操作系统,对内存的访问操作也不同,Java采取统一的内存模型来屏蔽以上的差异
(1)工作内存: 线程私有的,cpu执行线程的指令时,使用寄存器来保存上下文
(2)主存: 线程共享的,Java进程的内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包含实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被线程共享
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示:
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么他们就不能保证他们的有序性,虚拟机可以随意的对他们进行重排序
happens-before: JVM规定了多组线程,执行操作的时候,哪些操作必须先于哪些操作的规则
关键字volatile可以说是JVM提供的最轻量级的同步机制,但是它并不容易完全被正确理解和使用。JVM内存模型对volatile专门定义了一些特殊的访问规则
不保证原子性,保证有序性和可见性: 隐藏含义:(操作本身是原子性,使用volatile就可以保证线程安全)
happens-before提供了volatile修饰的变量的规则
>volatile变量规则: 对一个变量的写操作先行发生于后面对这个变量的读操作
volatile变量的代码行,建立了一个内存屏障(禁止指令重排序)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。