Jvm组成部分
1. PC寄存器/程序计数器
一块小的内存空间,作用可以看做是当前线程所执行的字节码的行号指示器。分支,循环,跳转,异常处理,线程恢复等基础功能都依赖计数器完成。在任何一个时刻,一个处理器只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每条线程需要一个独立的程序计数器(私有内存)。Java方法的计数器记录正在执行的虚拟机字节码指令的地址,Native方法的计数器为空。计数器不存在OutofMemoryError.
2. Jvm栈
Jvm栈也是线程私有的,生命周期与线程相同。Jvm栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧(stackframe)用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在Jvm栈从入栈到出栈的过程。局部变量表存放了基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(指向对象地址的指针)和returnAddress(指向一条字节码指令的地址)。在方法运行期间不会改变局部变量表大小。
3. 本地方法栈
本地方法栈作用与Jvm栈作用类似,区别不过是Jvm栈执行的是Java方法,本地方法栈执行的是Native方法。
4. Jvm堆
Jvm堆是Jvm内存最大的一块,被所有线程所共享。Jvm堆在Jvm启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Jvm堆是垃圾收集器(GC)管理的主要区域。Jvm堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。因为堆是Jvm中所有线程所共享的,因此在其上进行对象内存分配均需要进行加锁,这也导致了new对象的开销是比较大的。Sun Hotspot Jvm为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由Jvm根据运行的情况计算而得,在TLAB上分配对象不用加锁,因此Jvm给线程分配对象的内存时会尽可能的在TLAB上分配,如果对象过大,则仍需在堆空间分配。TLAB仅作用于Eden Space。
5. 方法区
与Jvm堆一样,方法区是各个线程共享的区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。方法区也可以叫永生代,但是Hotspot准备放弃永生代,使用Native Memory来实现方法区。相对而言,GC在这个区域比较少的出现,这个区域内回收目标主要是常量池和对类型的卸载。
6. 运行时常量池
运行时常量池是方法区的一部分,这里存放的是类中的固定的常量信息,方法和Field的引用信息。
Jvm垃圾回收机制
触发GC的条件:
1)GC在优先级最低的线程中进行,一般在应用程序空闲,即没有应用程序在运行时,被调用。
2)例外: 当Jvm堆内存不足时,GC会被调用。当应用线程正在运行,并且在运行过程中创建对象,若这时内存不足,Jvm会强制调用GC。若一次GC之后仍不能满足内存分配,Jvm会再次进行两次GC,若仍不能满足,Jvm报OutofMemoryError,Java应用停止。
GC的Generation算法(分代收集算法)
分代收集算法是大部分Jvm的垃圾收集器采用的算法。他的核心思想是根据对象存活的生命周期,将内存划分为若干个不同的区域。一般情况下将堆分为老年代和新生代,方法区为永生代(新版本将永生代废弃,引入元空间的概念,永生代使用Jvm内存儿元空间直接使用物理内存)。新生代分为Eden区和Survivor区(Survivor from和Survivor to),大小比例默认为8:1:1.新产生的对象优先进入Eden区,当Eden区满了之后再使用Survivor from,当Survivor from也满了之后,就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象使用Copying算法复制进入Survivor to,原来的Survivor to就成了新的Survivor from,Copying算法在复制之后清空Eden和Survivor from,原来的Survivor from就成了新的Survivor to。复制的时候,如果Survivor to无法容纳全部存活的对象,则根据老年代的分配担保,将对象复制进入老年代,如果老年代也无法容纳,则进行Full GC(老年代GC或者称为Major GC)。老年代的特点就是每次垃圾收集时,只有少量对象需要被回收,而新生代的特点是每次都要回收大量的对象,因此可以根据不同代的特点采取合理的收集算法。程序中主动调用System.gc()强制执行的GC为Full GC。
Jvm对新生代采用Copying算法,因为新生代每次GC都要回收大部分对象,需要复制的操作次数较少;
老年代的特点是每次GC只回收少量对象,一般使用Mark-Compact算法;
1)大对象直接进入老年代,Jvm有个参数配置,大于这个值直接进入老年代,避免Eden和Survivor区之间发生大量的对象复制操作。
2)长期存活的对象进入老年代,Jvm给每个对象定义一个对象年龄计数器,如果对象在Eden出生,经历一次Minor GC之后仍存活,将其移入Survivor区并且年龄+1.每经过一次GC年龄+1,当年龄到达15(Jvm参数,默认为15),移入老年代。
3)但是Jvm不是永远要求年龄必须达到最大年龄才可以晋升老年代,如果Survivor空间中相同年龄(如年龄X)的所有对象大小的总和大于Survivor的一半时,年龄大于等于X的所有对象直接进入老年代。
简单的三个收集算法
1)标记清除算法:
分标记,清除两个阶段。首先标记所需要回收的对象,在标记完成之后统一回收所有被标记的对象。不足:一个效率问题,标记和清除的效率都不高;一个空间问题,标记清除之后产生了大量不连续的内存碎片,碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不再触发一次GC。
2)复制算法Copying:
为了解决效率问题,复制算法将可用的内存分为两部分,每次只使用其中一块,当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用过的内存空间一次性清理掉,这样解决了碎片问题。
3)标记整理算法 Mark-Compact:
复制算法在对象存活率较高时就会频繁进行复制操作,效率将降低,因此有了Mark-Compact算法。标记过程和标记清除算法相同,但是后续不是直接清除对象,而是让所有存活的对象向同一侧移动,然后直接清理边界以外的内存。
两种算法判定对象是否存活
1)引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器+1,当引用失效,计数器-1.计数器为0表示该对象已死,可回收。但是这个方法很难解决两个对象循环相互引用的情况。
2)可大型分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明对象已死,可回收。Java中可以作为GC Roots的对象包括:Jvm栈中引用的对象,本地方法栈中Native方法引用的对象,方法区静态属性引用的对象,方法区常量引用的对象。主流实现中,都是通过可达性分析算法来判定对象是否存活。