- 首先,Java 的运行时内存区域主要是划分为共享区域和线程隔离区域的。
- 线程共享:其中的堆区主要存放Java对象实例,方法区主要存放已经被加载的类信息、常量以及静态变量等数据。
- 线程隔离:虚拟机栈主要存放线程执行方法时创建的栈帧,存储局部变量等,程序计数器主要用于记录当前线程指令执行位置,而本地方法栈主要是存储本地方法的一些信息,和虚拟机栈类似。
- 在 JVM 中根据各个内存区域特性,我们可以得出三个名称,永久代、新生代、老年代,用于指示某一片内存区域。
- 新生代:新生代属于堆区的一部分,我们知道堆区是存储 Java 对象实例的地方,而“新生”两个字我们可以理解为产生新的 Java 对象实例的地方。
- 老年代:也属于堆区的一部分分,但是是存放的实例可以说是经历过数次GC没有挂掉的“老家伙”所存放的区域。
- 永久代:永久代属于方法区的一部分,因为根据上面所说的,方法区主要存放类型信息、常量以及静态变量等信息,这些都不像堆区的数据那么更新换代很频繁,相对而言可谓是“永久”的,所以称为永久代,但是并不代表整个方法区全是永久代,JVM 的运行时内存划分还可以划分的更细致,上面只是大的方向。
- 注意:一般来说GC的主要工作区还是堆区,因为那里的“垃圾”产生的很快且多,方法区相对而言则少很多
- 既然要进行 GC ,怎样判断哪些对象该 GC ?
-
判断对象挂了的算法(堆区):
- 引用计数 :所谓引用计数算法,就是给每个对象添加一个计数器,每当有一个地方引用了该对象,则计数器加一,减少一个引用,则计数器减一,直到计数器为0时,则可以认为该对象不再被引用,可以判断为“垃圾”。
- 问题:难以解决对象间相互引用的问题,比如两个同一类型的对象,互相用一个字段引用对方,然后再将两个对象的引用赋值为 null,那么我们首先将不能获取到两个对象,但是每个对象实际还被另一个同类型对象引用,那么计数器始终不会为0,那么就不会被判断为挂了。
- 可达性分析:主流方法,通过设置一些对象作为起始节点,然后顺着节点进行深入遍历,就像我们查找的文件系统一样,去找其下的某个文件一样,如果某个对象不能通过任何起始节点找到,那么说明它就不再被引用,就像文件系统中的文件一样不再存在一样的。那些所谓的起始节点被称为 “GC Roots” ,在
Java 中,可用作 “GC Roots” 的对象一般是虚拟机栈中引用的对象、方法区的静态变量或者常量的对象等,还有本地方法所引用的对象等。
- 引用计数 :所谓引用计数算法,就是给每个对象添加一个计数器,每当有一个地方引用了该对象,则计数器加一,减少一个引用,则计数器减一,直到计数器为0时,则可以认为该对象不再被引用,可以判断为“垃圾”。
-
方法区的回收判定:
- 回收内容:方法区主要回收的是不再使用的类和一些不使用的常量。
- 怎样判定常量不再使用?:在常量池中的某个常量在目前没有任何一个对象引用它,并且也没有任何一个地方使用它的字面量,那么就可以判定它被“废弃”了,当发生内存回收时就可能会清理掉它。
- 怎样判定类不再被使用?:
1):该类的所有实例都都被回收,也就是说堆区没有它的对象了。
2):该类的类加载器已经被回收。
3):该类的Class对象没有被引用
-
- 垃圾收集算法:
- 标记-清除:从名字可得,先将可回收的对象进行标记,标记完毕后统一进行内存回收,但是问题就是会产生大量的空间碎片,而且效率低下。
- 复制-清除:这个方法是先将内存划分为两块,一块用作保留区,一块使用,当要进行内存回收时,先将存活的对象移动到保留区,然后将使用区整个区域进行清理,但是空间浪费过多,但是后来根据调查显示大多数对象都挂的很快,到下一次回收的时候往往不会留下多少活着的对象,所以进行了优化,将堆区划分为8:1:1三块,即 Eden 和两块 Survivor 区域,每次使用 Eden 和一块 Survivor 即可。但是当存活对象和多的话,这种方式就很不堪了,因为用作复制存放的区域过小。
- 标记-整理:该算法基于“标记-清除”,先标记,然后将所有活着的对象移动到一边,然后直接清理整个分界点的一边即可,相对而言,内存的利用更大。
- 分代:因为如上面所说,堆区可以大致分为“新生代”和“老年代”,它们的特点很明显,一个是经常产生大量新对象,大量对象死亡,一个是对象存活时间很长,相对而言更新换代很慢,所以可以根据其特点分别采用某种收集算法来收集垃圾,这是目前商业虚拟机基本都使用的算法。新生代采用“复制-清理”算法,老年代采用“标记-清除”或者“标记-整理”。
- 对象的回收流程:
首先采用判断对象是否挂掉的算法来判断对象是否还被引用,一般来说使用的是“可达性分析”,然后判断不可达后就将步入回收流程,在该流程中将经历至少两次标记过程,在第一次标记过后,系统会判断是否执行该对象的 finalize() 方法(该方法没有被覆写或者已经被调用过,则不会再调用),但是一个问题,即使判定执行,也不保证执行完。然后经历第二轮标记,如果该对象还是不可达,那么该对象就真的挂了,就将被清理,但是也有可能第二轮“可达”(在第一轮的 finalize() 执行中可能将对象自身引用赋值该某个变量,以使自己可以被访问),那么就将进行下一轮标记去判定,但是如果对象执行过一次 finalize() 方法,那么后面的标记过程中将不会再调用。
- ,,,,,,,