JVM(Java Virtual Machine,java虚拟机)
虚拟机:指通过软件模拟的具有完整硬件功能的,运行在一个完全隔离的环境中的完整计算机系统
一、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倍以上。
垃圾收集器
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。 如果你的应用追求低停顿,G1可以作为选择;如果你的应用追求吞吐量,G1并不带来特别明显的好处。
内存分配与回收策略
- 对象优先在Eden分配(Eden:Survivor 8:1:1)
- 大对象直接进入老年代(XX:PretenureSizeThreshold参数)避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)
- 长期存活的对象将进入老年代(XX:MaxTenuringThreshold设置,默认为15)
- 动态对象年龄判定:为了能更好的适应不同程序的内存状况,JVM并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保:
Java内存模型
JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。
主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享。Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示 :