菜鸟修行之路----java虚拟机二:垃圾回收与内存分配
垃圾回收与算法概述:
JVM 的GC就主要做以下3件事:
- 判断内存是否需要回收。(确定垃圾)
- 决定回收时机
- 确定回收方法
在Java的运行时数据区中,程序计数器、虚拟机栈、本地方法栈三个区域都是线程私有的,随线程而生,随线程而灭,在方法结束或线程结束时,内存自然就跟着回收了。
Java堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存。
1.垃圾回收
1.1 确定垃圾
在堆中存放着java世界中几乎所有的对象实例,GC一般主要也是对于堆进行垃圾回收。垃圾,换句话说就是死去的对象(不可能在背任何途径使用的对象)。
GC一般通过以下2种方法来确实垃圾:
- 引用计数算法
- 可达性分析算法
1.1.1 引用计数算法
判断过程:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
但是JVM并未采用该算法来确定垃圾,引用计数算法实现简单,判定效率比较高,但是该算法不能解决对象之间的循环引用的问题。
1.1.2 可达性分析算法
为了解决引用计数法中的循环引用问题,JVM采用了可达性分析法(tracing GC)。
判定过程:通过一系列的称为**“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain)**,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的。如下图:
通过上图分析可以得知,对象object 5、object 6、object 7虽然互有关联(引用),但是它们到GC Roots是不可达的。
但是:不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
注:GC Roots在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。
GC Roots不是一组对象,而是一组特别管理的指向引用类型对象的指针。这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针,这也是tracing GC算法不会出现循环引用问题的基本保证。
所以我们可以得知:只有引用类型的变量才被认为是Roots,值类型的变量永远不被认为是Roots。
在Java中,可作为GC Roots的对象包括以下几种:
- **虚拟机栈(栈帧中的局部变量表,Local Variable Table)**中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- **本地方法栈中JNI(即一般说的Native方法)**引用的对象。
选择原因:不能被GC管理或者回收。
1.2 垃圾收集算法
在完成垃圾确定的步骤后,就可以开始进行垃圾回收。一般来说垃圾收集会采用以下4种算法:
- 标记-清除(Mark-Sweep)算法
- 复制(Copying)算法
- 标记-整理(Mark-Compact)算法
- 分代回收(Cenerational Collection)算法
1.2.1标记-清除算法
最基础的垃圾收集算法。
算法步骤:顾名思义,算法分为标记和清除2个步骤。
- 标记:标记处所有需要回收的对象,标记过程就是上文中的对象死亡判断过程。
- 清除:回收被标记的对象所占用的空间。
实现如图:
通过上图分析可以得知:
标记-清除算法的最大弊端:内存碎片化严重,后续可能出现大对象找不到可利用空间的问题。
1.2.2 复制算法
为了解决标记-清除算法的问题,提出了复制算法。
算法思想:它将可用内存按容量分成大小相等的两块,每次只使用其中的一块。当这一块内存用完,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
具体实现如图:
算法优点:内存分配时,不用考虑内存碎片问题,只用移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
算法弊端:可用内存被压缩到原来的一半,而且如果存活对象过多的话,该算法的效率将会大大降低。
1.2.3 标记整理算法
结合上诉2个算法的优点,提出了标记整理算法。
算法思想:标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
1.2.4 分代回收算法
主流的虚拟机都采用的是该算法。
算法思想:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。
新生代:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
回收原理:一新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space)(比例大概为8:1:1),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代:在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用**“标记-清除”或“标记-整理”**算法来进行回收。
2. java中的4种引用类型
2.1 强引用
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
简单实例:
使用new一个新对象来的方式来创建强引用。
Object obj = new Object();
2.2 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
简单实例:
使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
2.3 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
简单实例:
使用 WeakReference 类来实现弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;// 使对象只被若引用关联
2.4 虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
简单实例:
使用 PhantomReference 来实现虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
3.内存分配
java的自动内存管理可以理解为自动化解决以下2个问题:
- 给对象分配内存
- 回收分配给对象的内存(就是上文中的垃圾回收)
对象的内存分配通常是在堆上分配(除此以外还有可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是固定的,实际取决于垃圾收集器的具体组合以及虚拟机中与内存相关的参数的设置。
给对象分配内存一般满足以下原则:
- 对象优先在Eden区分配:大多数情况下,对象在新生代的Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
- 大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象,最典型的大对象就是很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息(尤其是遇到朝生夕灭的“短命大对象”,写程序时应避免),经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来安置它们。
虚拟机提供了一个**-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制**(新生代采用复制算法回收内存)。
总的来说,具体的内存分配步骤和原则如下;
- JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
- 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
- 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。
- 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
- 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
- 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象(可以通过XX:MaxTenuringThreshold设置)会被移到老生代中。
修行之路艰辛,与君共勉
----2020年3月 成都
注:菜鸟修行之路的学习笔记集合已经上传到github中。
链接:https://github.com/752534553jl/practice