Java虚拟机——内存管理

虽然由于java虚拟机的存在,使得java的内存实现“自动化”,但是JVM也不是万无一失的,有时候还是需要我们去了解内存的分配和回收策略。

一、内存分配

对象的创建

Java是一门面向对象的语言,在java虚拟机运行的过程中,时刻都有对象被创建出来。在语法层面,创建对象可能就是一个 new 关键字而已,我们需要了解的是在虚拟机中一个对象的创建过程。

虚拟机遇到一个new指令时,首先回去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号所代表的的类是否已经被加载,如果没有加载则首先需要对类进行加载(类的加载机制),在加载检查通过后,将为对象分配内存。

为对象分配内存的过程可以理解为把一块确定大小的内存从java堆中划分出来。

假设堆中的内存都是绝对规整的,所有使用过的内存放在一边,没有使用过的放在一边,在边界处存在一个指针,那么分配内存就是把指针往未使用过的内存那边挪动固定的距离(根据对象所需要内存大小确定),这种分配方式成为“指针碰撞”

但是如果内存不是规整的,使用过的内存和没有使用过的内存相互交错,那么就没有办法使用指针碰撞了。这时候需要虚拟机维护一个表,表上记录着哪些内存是可用的,分配内存的时候就需要找出一块可用的内存进行分配,这种分配方式成为“空闲列表”

采用哪种分配方式由内存是否规整决定,而内存是否规整又取决于垃圾收集器是否具有压缩整理功能。

并发问题:假设只有一个内存指针的时候,由于并发问题就可能导致“指针碰撞”方式分配内存出现错误:假设有两个线程,线程A先读取指针位置,但是A没有分配完内存的时候线程B也读取了指针位置,这是个极大的问题。一般有两种解决方案:

一种是对内存空间的动作进行同步处理,另一种是把内存分配的动作按照线程的不同划分在不同的区域,即每个线程预先分配一小块内存,这个小块内存成为本地线程分配缓冲(TLAB),分配完之后进行同步锁定。

分配完内存之后,把所有分配的内存空间都初始化为零值(不包括对象那个头,因为全部分配为零就成了无主之物)。然后虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如果找到类的元数据信息、对象的哈希码、分代年龄(这些信息在对象头中)。

执行了new之后,会执行<init>方法,这样一个真正可用的对象才算创建完毕。

二、内存回收

1、回收条件

当内存需要回收的时候,我们一般考虑三个问题:

  • 哪些内存需要回收
  • 什么时候进行回收
  • 如何回收

Java堆中存储着几乎所有的对象实例,所以这里也是垃圾收集器的主要工作区域,垃圾收集器在这里对“已死”的对象进行回收。下面通过两种方法来判断对象是否“已死”:

(1)引用计数算法

引用计数法就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加一;当引用失效的时候,计数器的值就减一;任何时刻计数器为0的对象是不可能再被使用的。

(2)可达性分析算法

在主流的商用程序语言中(java/C#),都是通过可达性分析法来判断对象是否存活的。

这个算法的基本思路就是通过一系列的“GC Root”的对象作为起始点,从这些起始点开始往下搜索,搜索所走过的路称为引用链,当一个对象到GC Root没有任何引用链的时候,则证明此对象是不可用的。

在Java语言中,可以作为GC Root的对象包括以下几种:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即使Native方法)引用的对象

(3)对象的自我拯救

即使在可达性分析算法中被判定不可用的对象,也不一定是必须要死亡的。一个对象的真正死亡一般要经历两次标记过程。在可达性分析算法中被判定为不可用的时候,被标记第一次,并进行一次筛选。

筛选的条件是是否有必要执行finalize() 方法。当这个对象没有覆盖finalize() 方法或者已经被虚拟机调用过finalize() 方法,将会被认为没有必要执行此方法。

如果被确定有必要执行finalize() 方法,那么这个对象将会被放着一个叫F-Queue的队列中,稍后由一个虚拟机创建的名为finalizer的线程触发这个方法(也就是执行这个方法,但是并不会等执行结束,避免锁死崩溃)。在执行finalize() 期间如果与其他对象建立连接,则在第二次标记时移出“即将回收”的集合,否则,则在第二次标记后进行回收。

2、垃圾收集算法

(1)标记-清除算法

这个算法是最基础的垃圾收集算法,后面的垃圾收集算法都是根据它改造而成。算法分为标记和清除两个阶段。首先标记出需要回收的对象,在标记后统一回收所有被标记的对象。

这个算法的最大缺点就是回收后的内存比较零碎,如果有较大的对象实例,则会因为没有足够大的内存而执行另一次垃圾回收动作,增加了开销。

(2)复制算法

复制算法的主要思想就是将内存划分为相等的两个部分,每次只使用其中一个部分。当被用的一块内存用完了之后,就将还存活的对象移到另一块内存,然后把已经使用的那块内存一次性清理。

这种算法实现简单,运行高效,但是代价是将内存缩小一半,稍显不足。

(3)标记-整理算法

根据标记-清除算法改造而来,是在清理之前让所有存活的对象都往内存的一端移动,然后清理掉边界以外的内存。

(4)分代收集算法

当前商用虚拟机采用的都是“分代收集”。这种算法并没有什么新的思想,只是根据对象存活的周期的不同将内存划分为几块。一般是把堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。

比如新生代中,每次垃圾收集都有大量对象死去,就可以采用复制算法,只需要少量存活对象的复制成本就可以完成收集。

而老年代中因为对象的存活率高,就可以用标记-清除或者标记整理进行垃圾回收。

3、垃圾收集器

如果说垃圾收集算法是方法论,name垃圾收集器就是垃圾回收的具体实现。不同虚拟机回收策略也不同。

(1)CMS收集器

CMS收集器是一种获取最低按回收停顿时间为目标的收集器。主要应用于互联网或者B/S系统的服务器上,主要是响应速度快,希望停顿时间最短,带给用户最好的体验。

CMS收集器基于“标记 - 清除算法实现”,整个过程分为4个部分:初始标记、并发标记、重新标记、并发清除

(2)G1收集器

G1虚拟机是当今收集器技术发展的最前沿成果之一,现在应用于JDK 1.7中的HotSpot虚拟机中。

G1虚拟机是一个面向服务端应用的垃圾收集器,HotSpot赋予它的主要使命是替换JDK 1.5中的CMS收集器。和其它收集器相比,G1虚拟机主要有以下特征:

  • 并行与并发:利用多CPU、多核的硬件优势减少停顿时间
  • 分代收集:依然采用分代概念
  • 空间整和:整体上看是基于“标记-整理”的收集,所以不会产生内存碎片,有利于长时间运行
  • 可预测的停顿:建立可预测的停顿时间模型,有利于使用者管理

猜你喜欢

转载自blog.csdn.net/qq_40692753/article/details/83420910