引入
JAVA不同于C或者C++编程语言,其一是编程的思想,JAVA面向对象,C或者C++面向过程;其二就是JAVA是一门自动管理内存的语言,而C或者C++则需要编程人员自动管理。而JAVA的自动内存管理实现则是要依靠JVM的内存管理机制
JVM运行时数据区
在说明JVM的自动内存管理的前,需要对JVM的内存划分有清楚的模型认识,才能更好的立即JVM的自动内存管理的实现。JVM的内存划分为如下几部分:程序计数器、虚拟机栈、本地方法栈、方法区、堆;其中“堆”和“方法区”是在多线程下,线程共享的内存区域;而“程序计数器”、“虚拟机栈和本地方法栈”都是线程私有的内存区域。下面是对各内存区域的具体作用说明:
1)程序计数器:记录当前线程执行的字节码行号(地址),所占内存少,是线程私有内存区域。同时也是JVM中唯一的不会出现“OutOfMemoryError”异常的内存区域。而至于为什么会有程序计数器,其实是为了多线程在切换回来后的恢复状态。
2)虚拟机栈:其实是JAVA方法执行的内存模型,它由本地变量表、操作数栈、动态链接、返回地址组成。当方法被JVM执行引擎调用的时候,该方法中的数据将由以上的四部分封装为“栈帧”的数据结构进行对应的入栈和出栈。
3)本地方法栈:组成和原理同虚拟机栈查不多,区别是本地方法栈封装的是本地方法,即带有native的方法。有的虚拟机会将虚拟机栈和本地方法栈合并
4)方法区:该区域是存放类加载器加载的字节码对应的类或者接口的信息,比如该类或者接口里的常量(字面量和符号引用)、代码编译成的字节码指令流等详细数据
5)堆:从说周知就是实例变量的存放区域,也是GC主要关注和工作的区域
JVM内存回收
说明JVM的内存回收之前要先明确三个问题才能更好的理解JVM的内存回收机制。
1、那些内存需要回收?
2)什么时候回收?
3)怎么回收?
在说明JVM的内存回收的时候其实就是在说明这三个问题对应的回答。
1)那些内存需要回收?其实大范围来说就是堆和方法区,其中主要是堆。再详细一些就是,在这些区域中的变量或者对象所占的内存在不用的时候需要回收,而判断其所占内存不用的依据则是一些算法分析, 比如:引用计数和可达性分析,其中对于引用计数而言对于相互引用的情况是不能回收的,所以一般的JVM都是采用可达性分析算法来决定哪些对象所占内存需要被回收。
对于可达性分析算法的说明要先理解哪些是GC Root对象,其实就是虚拟机栈中的局部变量表中的对象引用和静态变量的对象引用、常量的对象引用和本地方法中的对象应用,只要对象和这些GC Root对象之间不可达,即没有可达性链,那么这个对象所占的内存便可以别释放。
2)什么时候要回收,其实对于这个问题而言可以简单的说也可以复杂的说,简单的说就是但内存不够时又要分陪内存那么触发JVM的垃圾回收;复杂的角度来说就是,在安全点的时候才有GC的可能性。
3)怎么回收,这一部分其实就是一系列的回收算法:
标记-清除:标记,就是通过可达性分析出那些对象所占内存可以被回收,其中的标记这个动作其实会涉及一个F-Queque的队列和finalizer的线程和对象本身的finalize的方法的调用,其实这里提出来的原因就是想说明标记这个动作其实还有许多细节这里就不展开了。标记后,就可以对被标记的内存进行清除。所以这一算法的缺点就是会造成内存的很多碎片,可能会在新分配的时候,再一次提前触发垃圾回收
标记-整理:该算法同标记-清除而言唯一的不同就是在清除这一块,标记-整理会在内存被标记完后不是立即清除内存,而是把被标记的内存移动到一端,之后才清除这一端的内存,所以不会产生内存碎片
复杂算法:这个算法的原理就是内存每次都留一半,使用的时候就使用一半,剩下的一半用于要回收的时候,把不用回收的对象所占内存移动到之前没使用的那一半内存里,然后把使用的内存全部清除。所以它也不会产生内存碎片,但却造成了内存利用不高。所以为了解决这一算法的问题JVM使用了“内存担保”这一策越。
内存担保:在堆中将堆细分了“新生代”和“老年代”这两块更具体的内存,其中“新生代”中有细分了“Eden、From Survior Two Survior”这三块区域,默认比例为8:1:1其中前两块内存是使用的内存区域最后的一块内存是用于回收时保存的内存。当保存的那块区域不能装下要被保存的数据时,就会把要保存的数据全部保到“老年代”所占的内存区域,这便是“内存担保”
分代收集:其实这不是什么算法,就是一种思想,即将内存如上面的“新生代”和“老年代”一样进行更一步的细分后针对不同的内存区域特点采用不同的回收算法,比如新生代就采用“复制算法”、而“老年代”采用"标记-清除”或者“标记-整理”
而以上的所有回收算法都是理论上的回收,而具体的落实则是垃圾回收器上,JVM中的垃圾回收器共用七种,分别针对不同的内存区域作用。
Serial:单线程的垃圾回收器,作用于新生代内存区域,其主要的设计目标是减少STW(Stop The World)到来的停顿时间。
ParNew:多线程的垃圾回收器,作用于新生代内存区域,其主要的设计目标是减少STW(Stop The World)到来的停顿时间。
CMS:并发的多线程垃圾回收器,作用于老年代,其主要的设计目标是减少STW(Stop The World)到来的停顿时间。而他的并发不是完全的并发而是在某些步骤上的并发。比如在“初始标记”、“重新标记”这两个步骤上进行并发,其余的也是会带来STW的影响。它的缺点是
一、会在单CPU情况下因为用户线程和回收线程的并发,会占用一部分的资源,对用户程序造成效率的影响
二、因为并发,在回收的时候,用户程序也在执行所以可能会造成在重新标记后对用户程序产生的新的垃圾不能在本次回收,即浮动垃圾
三、造成内存碎片
G1:该垃圾回收器是目前比较优秀的垃圾回收器,它集成了之前的垃圾回收器的优点,多线程、并发、内存整理(不会产生内存碎片)、可预测的STW、但它会把内存分为大小相同的region,会维护一个优先级表,在允许的垃圾回收的时间内会优先回收优先级表上优先级高的region的垃圾,带来垃圾回收的最高效益。
说明还有的垃圾回收器,这里就不再叙述,因为我把他们的名字忘了。。。。。但要说明的一点是,这些垃圾回收器不是单独作用,而是新生代和老年代的配合使用
JVM内存分配
说完了内存了自动回收,那么就说下内存的分配;其实内存的分配和内存的回收有关联,但JVM有一些如下的内存分配规则:
1)优先分配对象所占内存为新生代的Eden的区域,如果当Eden的区域不能在装下下次分配的对象,那么就触发一次Minor GC(即新生代的垃圾回收),在这期间可能会有“内存担保”,那么就会把新生代保存的对象复制到老年代,如果“内存担保失败”,那么就会触发Major GC(老年代的垃圾回收)让老年代能装下新生代的数据
2)活的时间长的对象会被移动到老年代,每个对象都用Age的计数器,当某个对象在Minor GC中能被Surviro内存装下,那么就把计数值加一,以后的每一次都能在Minor GC中存活下(不会被回收)那么计数值就加一,直到超过某一个阈值就会把这个对象移动到老年代
3)动态时间,就是当Survior中的对象所占内存大小之和超过了Survior的内存的一半那么所占内存大于等于Survior内存一半的对象就移动到老年代