一:JVM内存模型
1:jvm内存模型按照jvm规范分为五块区域(其实是六块,运行时常量池和方法区在一块,所以也是五块内容)如下:网上盗图
关于内存模型的介绍网上一大推,可以参考篇文章查看:JVM内存模型
补充:
1:Java虚拟机栈中的动态链接指的是,在编译期JVM无法确认的行为需要在执行时才能知道,比如我们的多态,这个特性就需要在执行的时候才能知道具体使用的子类是那个。
2: 在具体的HotSpot虚拟机中把本地方法栈和Java虚拟机栈合二为一了。
二: 对象创建,对象定位,对象的内存布局
关于这部分:可以参见我之前写的一篇关于美团六问的博文:对象创建,定位和内存布局
三:对堆和 方法区进行再研究
方法区和堆都是线程共享区,也占JVM内存中较大的比例,在JVM中对于这两个部分还有更值得研究的地方,见下图:
![](https://i-blog.csdnimg.cn/blog_migrate/d80c96a20613ad9fe8babd5093c1b0bc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/dd35bf44878b39c7a98b6a4975361503.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c001eeffcf70372f95254f35de932c95.png)
垃圾回收完,堆中有了一些空间,但是这里的空间并不连续,如果这时要创建一个对象,对象很大,堆中连续的空间并不满足要求,这就是所说的空间不连续性或者称为空间碎片,会导致堆中有空间但是由于不连续无法创建对象的情况。
针对这种情况我们把堆划分成了Eden区和Survior区,在Eden区中存储我们新创建的对象,当进行一次垃圾回收之后,Eden区空间不连续了,把Eden区存活的对象复制到Survior区,这样Eden区空间连续,可以继续创建新的对象。
那么Survior区为什么要分为S0和S1呢?试想,如果Survior只有一整块区域,当Survior区的对象回收之后也会存在空间碎片,所以在这块区域也划分为两个区域,而且一直保持其中一部分区域空间连续,另外一部分存放Eden区复制过来的对象。
3:空间担保机制。由于上面空间的划分,就出现了一种担保机制。当上面的Eden区空间不够用时触发YoungGC,然后把Eden存活的对象复制到S0/S1区域,这时候S0/S1空间不够用了,就会向老年代借用一些空间存放对象,这种机制就是空间担保机制,对象是存在老年代的。
4:老年代也会有空间不足的时候,老年代也会发生垃圾回收,叫Major GC/Old GC,但是老年代发生GC一般随着Young GC。
对于内存地址的分配可以参考如下图流程:但是这个流程还不完整,并没有包括在栈上分配,和TLAB。具体流程见美团六问博文。
在JDK1.8种Full GC也包括 Old GC+Young GC+Metaspace GC
5:非堆(方法区)
非堆(方法区)是JVM运行时数据区规范。对于不同的虚拟机可以有不同的实现。 我们常用HotSpot虚拟机种,从1.8开始方法区的实现也开始和之前不一样了。
JDK1.7 :称为 永久代
JDK1.8:称为MetaSpace 占用的是本地内存
首先我们要清楚,通常所说的方法区其实是JVM的一种规范,永久代和元空间只是不同版本的JVM对其的实现不同。
规范种提到:常量池,静态变量,类信息,即时编译器编译之后的代码等存在这部分区域。
但是对于我们常用的HotSpot虚拟机,在具体实现的时候,在JDK1.7中这部分区域称为永久代。
在JDK1.8中,把字符串常量池,静态变量,存储在堆里面。把类信息,常量(final修饰)等放到Metaspace占用直接内存。
至于这样划分,是因为字符串常量池,静态变量生命周期也随着对象的结束而结束,可以及早的进行回收。
四:垃圾回收
1:思考那部分需要垃圾回收呢?
对于那些线程私有的区域,由于栈帧的压栈出栈,生命周期结束,所以一般不需要进行垃圾回收。
对于方法区或Metaspace区域也会进行垃圾回收但是回收的次数不是很多,而且效率比较低,这部分主要回收的内容如下两部分:
-
废弃变量
废弃常量的回收
这与堆中对象的回收类似。以常量池的字符串为例,如果没有任何对象引用了此字符串,那么它就有可能被系统清理出常量池。 -
无用的类
废弃类的回收
此类回收条件较为苛刻,需要满足如下的3点:
该类所有实例已被回收,即Java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足这三点后,仅仅是“可以”回收,但是并非必然进行。 - 废弃类回收的控制参数
JVM提供了几个参数控制类回收:
-Xnoclassgc:关闭CLASS的垃圾回收功能
-verbose:class:在控制台查看类加载情况
-XX:+TraceClassLoading:查看类加载信息
-XX:+TraceClassUnLoading : 查看类卸载信息(FastDebug版的虚拟机才支持)
就剩下堆部分了,这部分的垃圾回收也是主要的部分。
2:首先如何确认一个对象是否需要进行垃圾回收
1)可达性分析
这个算法的基本思想就是通过一系列的称为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Root没有任何引用链时,则证明此对象不可用。如下图,对象7,8虽然有互相引用,但是到GC Root不可达。所以对象可以被回收。
Java种可以作为GC Root的对象有如下几个:
虚拟机栈(栈帧中的本地变量表)中引用的对象 。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即本地方法)引用的对象。
但是对于不可达的对象不一定就会被垃圾回收,只是暂时被标记可回收,虚拟机会堆这些对象二次分析,最终决定是否进行回收,具体的细节可以参考《深入理解JVM》.
3:垃圾回收的时机
垃圾回收的时机是不可控的,及时满足的垃圾回收的条件,JVM也会根据当时系统的状况有自己的一套算法来决定是否进行垃圾回收,即使手动调用System.gc(),也只是发出一个信号,并不代表马上进行垃圾回收。
有如下几种情况可能触发垃圾回收:
Eden区或S区不够用了。 触发Minor GC
Old区不够用了。 触发Major GC --通常也会触发Minor GC.
方法区不够用了。
System.gc() 发出一个垃圾回收的信号。
4:垃圾回收算法
1)标记清除
最基础的收集算法:标记-清除(Mark-Sweep).算法分为两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收所有的标记对象。它主要有两个不足的地方。
a:效率问题,标记和清除两个过程的效率都不算太高 b:空间问题,标记清除之后产生大量的不连续的空间碎片,导致后续分配较大对象时,无法找到足够的连续内存而不得不提前触发另外一次垃圾回收。
回收过程如下图:
2:复制算法
为了解决效率问题,一种称为“复制”的算法出现了,它将可用的内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,将存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时就不用考虑碎片等复杂情况,只要移动堆顶指针,按照顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价有点高。复制算法的过程如下:
现在的商业虚拟机都是采用这种收集算法来回收新生代,IBM公司研究表明,新生代的对象98%都是朝生夕死,所以不需要按照1:1比例划分空间,而是将内存分为已块较大的Eden区和两块较小的Survior空间(见上图),每次使用Eden区和其中一块Survior区。当回收时,将Eden种还存活的对象一次性复制到另外一块Survior空间上。最后清理掉Eden和刚才用过的Survior空间。HotSpot虚拟机默认Eden和Survior区大小比例是8:1,也就是每次新生代种可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存被浪费掉。当然,98%的对象可回收只是一般场景下的数据,我们没办法保证每次回收都只有不多于10%的对象存活,当Survior空间不够时,需要依赖其它内存(这里指老年代)进行分配担保。担保机制见上面所述。
3:标记整理
复制收集算法在对象存活率较高时就要进行比较多的复制操作,效率会变得很低,更关键的时,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在来年代一般不能直接采用这种算法。
根据老年代的特点,有人提出了另外一种标记-整理算法,标记过程仍然和标记-清除算法一样,但是后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存,标记-整理算法的示意图如下:
4:分代收集算法
当代商业虚拟机的垃圾回收算法采用“分代收集“算法,这种算法并没有新的思想,只是根据对象存活周期的不同,将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾回收时都发现大批对象已死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象的存活率高,没有额外的空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。