虚拟机:指通过软件模拟的具有完整硬件功能的,运行在一个完全隔离的环境中的完整计算机系统
一、JVM运行时数据区域(共有六个模块,又分为线程私有和线程共享)
线程私有:{1.程序计数器:一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
2.Java虚拟机栈:每个方法执行的同时都会创建一个栈帧保存局部变量表、操作数栈、动态链接、方法出口等信息。
-- 局部变量表:存放编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表在编译期间完成分 配, 即,进入一个方法是,局部变量的空间是确定的,执行期间不会改变局部变量表的大小。
3.本地方法栈:本地方法栈和虚拟机栈的作用完全一样,只不过本地方法栈是为本地方法Native方法服务服务的,而java虚拟机栈为Java程序服务。
}
线程共享:{
4.Java堆:(Java Heap)是JVM管理的最大内存,在JVM启动时创建。此内存存放的都是对象的实例。Java堆也是垃圾回收管理的主要区域,也被成为"GC区"。(-Xmx设置最大值,-Xms设置最小值)。如果没有足够的内存完成实例分配并且堆无法再扩展是,将会抛出OOM(eg:无限向集合里添加匿名对象)。
5.方法区:主要用于存储虚拟机类加载信息,常量,静态变量,编译器编译后的代码等数据。JDK1.8以前方法区被称为“永久代”,1.8被元空间取代。当方法区无法满足内存分配时,抛出OOM异常。
6.运行时常量区(方法区的一部分):运行时常量区也是方法区的一部分,存放字面量和符号引用。
--字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
--符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
}
二、垃圾回收器与内存分配策略
前面已经说过,垃圾回收管理的只要内存就是堆内存,Java堆中存放着几乎所有的实例对象。JVM虽然为我们提供了自动管理机制,但是他回收对象不是想怎么收就怎么收的,要回收一个对象,首先得判断这个对象是否已经“死去”,通过以下几种算法来判断对象是否已“死”。- 判断对象是否已"死":
1.引用计数法 : 给对象加一个引用计数器,每当有一个地方引用它,计数器+1;引用失效,计数器-1;任何时刻计数器为0的对象就是不能再被使用的,判断为"死"。
但是,主流的JVM没有选用引用计数法来管理内存,主要的原因就是引用计数无法解决对象循环引用的问题。
2.可达性分析算法:
核心思想为:通过一系列称为"GC Roots"的对象作为起始点,从这些节点向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots 没有任何的引用链相连时(从GC Roots到这个对象不可达)时,则认为这个对象时不可用的。
在Java中,可作为GC Roots的对象包含以下几种:
1.虚拟机栈中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中JNI(Native方法)引用的对象。
在JDK1.2以前,引用的定义:: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就
称这块内存代表着一个引用,这种定义很狭隘,即只分为被引用和不被引用。
为了更好的管理对象,在JDK1.2之后,Java对引用做了扩充,将引用分为:强引用、软引用、弱引用、虚引用。
强引用:代码中普遍存在的,如 Object obj=new Object(); 只要强引用存在,垃圾回收器永远不会收掉被引用的对象实例
软引用:描述有用但不是必须的对象,在系统将要发生内存溢出之前,会将这些对象列入回收范围中进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常。SoftReference
弱引用:垃圾回收是,无论当前对象是否有用,弱引用关联的对象都会被垃圾回收。
虚引用:虚引用不会对它所关联的对象产生任何生存时间的影响,目的仅仅只是该对象被垃圾回收时收到一个系统通知。
当我们通过可达性算法判断一个对象是该"死"的,但它就一定得死吗? 答案是:不!
可达性算法判断一个对象时该"死"的时候,这时他们暂时处于“缓刑”阶段,要宣告他真正死亡,至少要经历两次标记过程:①通过可达性算法发现没有与GC Roots相连接的引用链,那么它第一次被标记并且进行一次筛选,筛选条件是对象是否有必要执行finalize()方法。②-1 当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况认为没有必要执行,那么很幸运,这个对象才算是真正的“死”了。②-2 如果这个对象被判定为有必要执行finalize()方法,(哦,真是麻烦),那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。在finalize()方法中,这个对象还有一次逃脱的机会,怎么逃脱?(test是一个对象,test=null, 在finalize()方法中:test=this,test再次被获得引用地址,逃脱成功)。如果该对象没有逃脱,基本上就算“死”了。
三、垃圾回收算法
1.标记-清除算法
算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。效率问题 : 标记和清除这两个过程的效率都不高
空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
2.复制算法(新生代回收算法)
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。我们知道,新生代中98%的对象都是朝生夕死的,所以并不需要以1:1的比例来划分,将内存划分为一块较大的Eden区和两块较小的Survivor区,比例为 8:1:1 ;Survivor一个称为From区,另一个称为To区。每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
3.标记-整理算法(老年代回收算法)
老年代的对象存活几率高,所以不适合复制算法,效率会变低。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法
将Java的堆分为新生代和老年代,新生代使用复制算法,老年代使用标记-整理算法。Minor GC和Full GC这两种GC有什么不一样?
1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。