在HotSpot虚拟机中,有7种作用于不同分代的收集器,如下图:
黄自豪的博客
前面了解关于对象的生死判定和垃圾收集算法,而在HotSpot中是怎么实现的呢?
根据可达性分析算法,GC的时候从GC Roots节点开始找引用链的,而做为GC Roots的节点主要是在全局性引用与执行上下文中,如果逐个检查的话,必然是需要耗费很多时间的。
而执行GC的时候,所有线程是必然要停顿的。因为在执行时,如果对象的引用关系还在不断变化,就无法准确的进行GC了。
不过,主流的虚拟机现在都是采用精确式GC,即执行系统停顿后,并不需要检查全部执行上下文和全局引用位置,而是通过某些方法知道哪些地方存放着对象引用。
在HotSpot中是使用一组称为OopMap的数据结构来达到这个目的的。
在HotSpot中,特定的位置会使用OopMap来记录信息,这个位置被称为“安全点”。程序执行时到达安全点才能停下来。
安全点的选定是以程序“是否具有让程序长时间执行的特征”为标准选定的————因为每条指令执行时间很短,而长时间执行的最明显特征就是指令序列重复,如方法调用、循环跳转、异常跳转等,所以适合产生安全点。
那么在GC发生时,是如何让线程达到安全点再停顿下来呢。主要有两种方案:抢断式中断和主动式中断。
在GC发生时,中断所有线程,如果有线程不在安全点的话就恢复线程,让它到安全点再中断。(这种方式几乎被弃用了)
当GC时,简单设置一个标志,各个线程主动去轮询这个标志,标志为真时就自己中断挂起。
安全区域是指一段代码片段中,引用关系不发生变化。在这个区域中任何地方GC都是安全的。
如果程序在执行的话,安全点就足以解决了GC问题。但是如果程序没有在运行呢,没在运行也就是说没有分配CPU时间,比如线程处于Sleep或者Blocked状态,这时候就无法响应JVM的中断请求了,这时候就需要用安全区域来处理了。
在线程执行到这个区域时,会先标识自己是安全区域(Safe Region)。当JVM在这段时间内发生GC时,就不用管标识自己为安全区域的线程了。线程离开安全区域时,要检查系统是否完成GC的整个过程(根节点枚举),完成的话就继续执行线程,否则必须等待,直到收到可以安全离开安全区域的信号。
垃圾收集器回收内存是根据定义的条件来收集的,有了条件就需要算法来实现这些条件了。
垃圾收集主要的算法有标记-清楚算法、复制算法、标记-整理算法、分代收集算法这四种。
从名字就可以看出这个算法比较简单,首先标记出所有要回收的对象,在标记完成后统一回收被标记的对象。
它有两个不足的地方:
不过,标记-清除算法是最基础的收集算法,后续的收集算法都是基于此进行改进得到的。
复制算法把内存按容量分成了大小相等的两块,每次只使用其中一块。当这一块内存用完后,就把还存活的对象复制到另一块,然后把使用过的内存空间一次清理掉。
这样每次都是对半个区进行内存回收,且每次只需要移动堆顶指针,按顺序分配内存即可,简单高效。不过,虽然不用考虑内存碎片这种情况,但使用内存缩小一半,代价还是有点高。
这个收集算法主要用来回收新生代的,不过因为新生代(后面分代收集算法会提及到)的对象98%是“朝生夕死”的,所以没必要按1:1的比例来划分空间,而是分为一块较大的Eden空间和两块小的Survivor空间,每次使用一块Survivor和Eden空间。回收时,则将Eden和Survivor空间还存活的对象一次性复制到另一块Survivor空间,然后清理掉Eden和用过的Survivor空间。
一般Eden和Survivor的大小比例为8:1,也就是说每次新生代可用内存占整个新生代容量的90%,只有10%是空闲的。而当存活对象超过10%的空间时,空间就不够用了,这时候得依赖其他内存(指老年代,后面会再提到)来进行分担了。
如果存活对象比较多,用复制收集算法就比较低,如果不想浪费50%的空间,还得额外空间来做担保,所以,老年代一般不直接用这种算法,而是采用“标记-整理”算法。
它的标记过程和“标记-清除”算法一样,只是后续步骤不是直接回收,而是把存活对象往一端移动,然后清理掉另一端的内存。
这种方法是把内存划分为几块,一般是分为新生代和老年代,然后根据各个年代特点采用适当的收集算法。
关于垃圾收集需要做的是3件事:
我们已经了解到,程序计数器、虚拟机栈、本地方法栈(忘记的话,请重新看前两篇内容)这3个区域是随着线程生灭的;这几个区域的内存分配和回收是可以确定的,在方法结束或者线程结束时,内存就跟着被回收了。
而JAVA堆和方法区不一样,它们只有在程序运行时才会动态的创建对象,动态的对内存进行分配和回收,而垃圾收集器关注的就是这部分内存。我们常说JAVA是面向对象的,而这部分内存里也是充满了一个又一个的对象。那么,垃圾收集器要根据什么条件来判断哪些对象是该回收的呢?
很明显,当一个对象不再被任何地方使用的时候,就说明它该被回收了。所以,我们要了解的其实是怎么去判断一个对象已死。判断的方法有两种:引用计数算法和可达性分析算法。而在JAVA中使用的是可达性分析算法。原因下面会说名。
给对象添加一个引用计数器,每当有地方引用它,计数器就+1,引用失效时就-1;当计数器为0时就是对象不再使用的时候。
这个方法简单高效,不过在JAVA中却没有被使用。因为它很难处理对象之间互相循环引用的问题。
同一个一系列称为“GC Roots”的对象为起始点,从这些节点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链的时候,则证明这个对象不可用。

可作为GC Roots的对象包括下面几种:
无论是引用计数算法还是可达性分析算法,它们判断对象是还或者都与引用有关。在Java中,引用被分为四种:强引用、软引用、弱引用、虚引用。四种引用依次逐渐减弱。
然而,即使是可达性分析算法不可达的对象,也不是非死不可的。这时候,它们会执行finalize()方法,暂时处于死缓阶段;而当对象没有覆盖这个方法或者这个方法已经被执行过了,那么它就真要死了。
方法区的垃圾收集主要分为两部分:废弃常量和无用的类。废弃常量回收与Java堆中的对象相似;要回收无用的类,条件就比较苛刻一些了:
满足以上条件后,也仅仅只是可以被回收而不是必然被回收。
我们知道,用JAVA进行编程,最大的优势在于内存管理。我们可以完全忽略内存管理的细节,专注于逻辑业务上。但是,并不代表它就不会出现内存溢出或泄露的问题。而且,因为JAVA有自动管理内存的机制而对这一块不了解,显然是有点low的。所以,了解一下JAVA的内存管理,既有助于提高逼格,也能让我们在进行开发的时候,可以针对内存溢出或泄露的问题进行一些优化。
JAVA虚拟机在执行JAVA程序的过程中会把它所管理的内存分为若干个不同的数据区域。这些区域都有各自的用途、创建和销毁的时间,有的是随着虚拟机进程启动而存在的,有的则依赖用户线程的启动和结束而建立和销毁的。
通过下面这张图,我们可以比较直观的看出,虚拟机划分的几个数据区域。
根据JAVA虚拟机规范,内存被划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器、运行时常量池(程序运行时,由方法区分配的区域,所以在图中没画出来)
记载了线程中正在运行的JAVA方法的地址,如果执行的是本地方法,这个计数器则为空地址。
每个线程都有独立的程序计数器,该计数器主要是用来支持多线程的阻塞、挂起、恢复等操作。所以,这个计数器也是每个线程私有的内存区域。而且也是唯一一个JAVA虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈也是线程私有的。每个线程创建的同时跟着一起创建的,用于存储栈桢。
每个方法在执行时都会创建一个栈桢,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在JAVA虚拟机规范中,这个区域规定了两种异常情况:
与虚拟机栈类似,也是线程私有的。它是用来支持Native方法的执行的。在虚拟机规范中,对本地方法栈中的方法使用的语言、使用方法、数据结构都没有强制规定,可以自由实现它。和虚拟机栈一样,也会抛出StackOverFlowError异常和OutOfMemoryError异常。
用于存储对象实例及数组的。这个区域也是垃圾收集器(GC)的主要管理区域。为了方便内存回收,该区域还会进行很多划分,不变的是,存放的内容都是对象实例。该区域的大小可以通过-Xmx和-Xms来控制。如果没有内存可给实例分配,且无法拓展时,会抛出OutOfMemoryError异常。
用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
在JAVA虚拟机规范中,没有强制要求实现这部分的内存管理。垃圾收集行为在这部分也比较少。当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常。
内存的管理其实就是内存分配和内存回收。而内存分配的几个区域可以分为两类,程序计数器、本地方法栈、虚拟机栈这几个区域是属于线程私有的,随着线程创建而创建,销毁而销毁,销毁时内存也会释放掉,所以这几个区域是不需要进行GC的。方法区因为它的定义GC行为相对会比较少,所以,堆则成为了垃圾回收器的重点维护对象。

第一次搭建springmvc+mybatis框架还是踩了不少坑。通过不断的搜索和比对别人的搭建过程,还是让我搭建出来了。于是,打算总结一下我的搭建过程,并附上所需的jar包。希望能帮到下一个要搭建SpringMVC框架的人。
为什么要使类和成员可访问能力最小化?什么时候用继承比较合适?用继承有哪些缺点?接口有哪些优点?稍微总结了一下,相信看完后对于用JAVA来编程的同学会有些有帮助的。如果想看更详细的内容,就去看《Effective JAVA》这本书吧