深入理解Java (二) 垃圾回收与内存分配

(一)、对象存活判断

1.引用

   两种算法:引用计数算法–每当一个地方引用就加1,当引用失效,计数器就减1.计数器为0时就说不可能再被使用的。可达性分析算法–通过“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径成为引用链
   在java中 可作为GC Roots的对象包括以下几种:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI(Native方法)引用的对象
   強引用:程序代码中普遍存在的,类似于new关键字创建的引用,只要强引用还在,垃圾收集器就不会回收掉被引用的对象
   软引用:描述一些还有用但并非必须的对象,在系统将要发生内存溢出异常之前,会吧这些对象列进回收范围之中进行二次回收,若回收还是没有足够的内存,才会抛出异常。Softreference类实现
   弱引用:也是描述非必须对象,但强度比软引用更弱一些,被关联的对象只能生存到下一次垃圾收集发生之前,垃圾收集器工作时,无论内存是否足够,都会回收掉被弱引用关联的对象。WeakReference来实现
   虚引用:又称幽灵引用或者幻影引用,最弱的一种引用关系,完全不影响对象的生存时间,也无法通过其来取得一个对象实例。其唯一目的是能在这个对象被收集器回收时收到一个系统通知。PhantomReference类实现

2.生存还是死亡

   要真正宣告一个对象死亡,至少需要经历两次标记过程:若对象在进行可达性分析后发现没有与GC Roots相连接的引用链,它将会被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖或者finalize()方法已被虚拟机调用过之后,虚拟机将这两种情况都视为没必要执行。
   若对象被判定为有必要执行finalize()方法,那么对象将会在一个F-Queue的队列中,并稍后有虚拟机自动创建的、低优先级的Finalizer线程去执行,即虚拟机会触发这个方法。但不承诺会等待他运行结束。对象在finalize()方法中执行缓慢,或者发生了死循环就会导致队列中其他对象永久处于等待。甚至导致整个垃圾回收系统崩溃。
   finalize()方法是对象逃脱死亡名义的最后一次机会,稍后GC将对F-Queue中进行第二次小规模标记,若要拯救自己,只需与任何一点对象建立关联即可,譬如将this赋给某个类变量或者对象的成员变量。即可被移除即将回收集合,如果还没逃脱那就基本上真的被回收了。

3.方法区回收

   永久代中垃圾收集主要回收两部分内容:废弃常量和无用的类。
   回收废弃常量和回收java堆中的对象非常类似。而回收无用的类则需:该类所有的实例都被回收,也就是java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。改类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

4.jvm的内存结构

   (1).程序计数器
当前线程所执行的字节码的行号指示器(选取下一条需要执行的字节编码指令)线程私有
异常: 如果执行的是java方法,则记录的是正在执行的虚拟机字节码指令的地址。如果是native方法,则为空。java虚拟机规范中唯一没有规定任何OutOfMemmoryError情况的区域。
   (2).java虚拟机栈
每个方法在执行时都会创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等信息 线程私有
一般所说的栈内存就是指这个,或者说是虚拟机栈中局部变量表部分。
局部变量存放基本类型加引用类型(reference)
64位的long或者double类型的数据会占用2个局部变量空间,其余数据类型占用1个。
局部变量所需内存空间在编译期间完成分配
异常:两种异常:
1、如果栈的深度大于了虚拟机所允许的深度,—stackoverflowerror
2、如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,会抛出OutOfMerroryError
   (3).本地方法栈
与虚拟机栈所发挥的作用非常类似,为虚拟机使用到的Native方法服务。异常同样。
   (4).java堆
虚拟机管理内存中最大的一块。所有线程共享,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配。垃圾收集器管理的主要区域。
分代收集算法:新生代和老年代。
Eden空间,From survivor空间,To Survivor
java堆可以处于物理上不连续的内存空间中。
异常:若堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出OutOfMemoryError异常。
   (5).方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。很多人喜欢方法去称为永久代
不需要连续的内存和可以选择固定大小或者可以扩展外,还可以选择不实现垃圾收集。垃圾回收期在这区域主要是针对常量池的回收和对类型的卸载。
异常:方法区无法满足内存分配需求时会抛出OutOfMemoryError异常。
   (6).运行时常量池
运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项常量池,用于存放编译器生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放。
class文件中每一部分存放哪种数据都必须符合规范才能被虚拟机认可装载和执行。但对于运行时常量池,java虚拟机规范没有做任何的细节要求。
运行时常量池具备动态性。java中并不要求常量一定是有编译器才能生成,也就是并非置入class文件中常量池的内容才能进入方法运行时常量池,运行期间也能。 String中的intern()方法。
异常:当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
   (7).直接内存
不是虚拟机运行时数据区的一部分,也不是java虚拟机中定义的内存区域。
NIO,引入一种基于通道与缓冲区的IO方式,他可以使用Native函数库直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
不受java堆大小的限制,受本机总内存大小以及处理器寻址空间的限制。
异常:忽略了大小,使得各个内存区域和大于物理内存限制从而导致动态扩展时出现OutOfMemoryError异常

(二)、垃圾收集算法

1 标记-清除(Mark-Sweep)

   标记清除之后会产生大量不连续的内存碎片,会导致在分配大对象时无法找到连续内存而进行另外一次垃圾收集动作

2 复制算法(Copying)

   商业虚拟机都采用这种收集算法来回收新生代。Eden 和 Survivor,当Survivor空间不足时,需要在老年代中进行分配担保。实现简单,运行高效,代价是内存缩小为一半。

3 标记-整理算法(Mark-Compact)

   主要用于老年代。标记过程跟标记清除算法一样,但后续是将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4 分代收集算法(Generational Collection)

   新生代和年老代。根据不同年代的特点选择相应的收集算法。

(三)、HostSpot算法实现

   可作为GC Roots的节点主要在全局性的引用于执行上下文中。对时间的敏感还体现在GC停顿上。GC进行时必须停顿所有的Java执行线程。(Stop The World)
   HotSpot中使用一组OopMap数据结构来直接知道哪些地方存放着对象引用。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。
   HotSpot在特定位置记录OopMap信息,这些位置称为安全点。程序执行到安全点才能停顿下来开始GC,Safepoint的选定不能太少以至于让GC等待时间太长,也不能太平频繁增大运行时的负荷。
   GC发生时让所有线程跑到最近安全点方式两种:抢先式中断和主动式中断。前者是首先将线程全部中断,如果发现有线程中断的地方不在安全点上没救恢复线程,让它跑到安全点上。现在基本不用
   安全区域:这个区域中任何地方开始GC都是安全的。线程执行到Safe Region中的代码时,首先标识自己已经进入Safe Region,GC时就不用管标识自己为Safe Region状态的线程了,离开时他要检查系统是否已经完成根节点枚举,如果完成则线程就继续执行,否则就需要等到收到可以离开的信号为止。
   后者设置一个标志,各线程主动去轮询这个标志,发现为真时就自己中断挂起,轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

猜你喜欢

转载自blog.csdn.net/u013305783/article/details/80415845