redis作为内存非关系型数据库,是一种key - value 缓存产品,但它同时还支持数据持久化,常常和一些老牌关系型数据库配合使用 下文将作为学习笔记介绍一下
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。 因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
局部变量表存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double)、 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(GarbageCollected Heap)
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、 常量、静态变量、 即时编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现
otSpot虚拟机在Java堆中对象分配、 布局和访问的全过程。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没有,那必须先执行相应的类加载过程
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。 对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找到类的元数据信息、 对象的哈希码、 对象的GC分代年龄等信息
从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头包括两部分信息:
接下来的实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。 无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。对象访问方式也是取决于虚拟机实现而定的。
目前主流的访问方式有使用句柄和直接指针两种:
注:对虚拟机Sun HotSpot而言,使用的是直接指针访问。
由于书中所实例均为jdk1.6或jdk1.7,在jdk1.8中很多vm参数都被移除,这里就不赘述,发生上述异常,基本上都是创建了太多的对象,导致内存不足,然后溢出。
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经死去”(即不可能再被任何途径使用的对象)。
一些教科书给出的说法为:
该算法在大部分情况下它都是一个不错的算法,但至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。就会被gc回收
在Java语言中,可作为GC Roots的对象包括下面几种:
在jdk1.2之后,引用分为如下四中:
一个对象死亡,至少要经历两次标记过程,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、 低优先级的Finalizer线程去执行它。
方法区(或者HotSpot虚拟机中的永久代)垃圾收集主要回收两部分内容:废弃常量和无用的类。
判定一个常量是否是“废弃常量”:就是没有任何对象引用常量池中的该常量,也没有其他地方引用了这个字面量
定一个类是否是“无用的类”的条件则相对苛刻许多。 类需要同时满足下面3个条件才能算是“无用的类”:
注:虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。 是否对类进行回收,由虚拟机的参数控制。
算法分为“标记”和“清除”两个阶段:
此算法的不足为:
“复制”(Copying)的收集算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这种算法的代价是将内存缩小为了原来的一半
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。 更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,更适合另外一种“标记-整理”(Mark-Compact)算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法。
该算法只是根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代使用复制算法:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活
在老年代使用“标记—清理”或者“标记—整理”算法:因为对象存活率高、 没有额外空间对它进行分配担保
注: