JVM-垃圾回收两大方法介绍

垃圾判断

垃圾介绍

垃圾:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾

作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象

垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收

在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:引用计数算法可达性分析算法

引用计数法

引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用)

优点:

  • 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收
  • 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误
  • 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象

缺点:

  • 每次对象被引用时,都需要去更新计数器,有一点时间开销

  • 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。

  • 无法解决循环引用问题,会引发内存泄露(最大的缺点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Test {
    public Object instance = null;
    public static void main(String[] args) {
    Test a = new Test();// a = 1
    Test b = new Test();// b = 1
    a.instance = b; // b = 2
    b.instance = a; // a = 2
    a = null; // a = 1
    b = null; // b = 1
    }
    }

可达性分析

GC Roots

可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集

什么是对象可达?

对象可达指的就是:双方存在直接或间接的引用关系
根可达或GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。

GC Roots 对象:

  • 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等
  • 本地方法栈中引用的对象
  • 堆中类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 字符串常量池(string Table)里的引用
  • 同步锁 synchronized 持有的对象

GC Roots 是一组活跃的引用,不是对象,而且是JVM确定当前绝对不能被回收的对象的引用,放在 GC Roots Set 集合

当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。

即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。

下面就一起来理解一下为什么这几类对象可以被作为GC Roots。

1、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。

2、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。

3、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。

4、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。

5、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。

工作原理

可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象

分析工作必须在一个保障一致性的快照中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因

基本原理:

  • 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链

  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象

  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象


Java-Stream应用之快速集合类型转换数组

mapToXXX

IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

这三个方法除了对应返回三种指定类型的stream外其他使用方法和作用和map类似,相当于map方法的特例。

现在就一mapToInt为例进行说明,看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void stream_mapToInt() {

List<String> list = List.of("Mr.zhangsan","Mr.lisi","Mr.wanger","Mr.mazi");

IntStream stream = list.stream().mapToInt(a -> a.length());
stream.forEach(System.out::println);

}

运行结果:

11

7

9

7

上面代码使用mapToInt方法统计集合中个元素的字符长度,统计结果保存在IntStream流中返回,该流中只能包含int类型数据,map方法也能达到同样的目的,看代码:list.stream().map(a -> a.length()).forEach(System.out::println);

该代码直接结果和上面一样,区别就在于保存统计结果的流的类型不一样,map返回的是Stream,就是流中保存的是Integer类型的数据。当然,Stream类型可以通过IntSteram的box方法得到。详细可以参看以前的IntSteram方法的说明文档。

toArray

Object[] toArray()
A[] toArray(IntFunction generator)

简单地说这个方法就是把流转为数组。要把流转为集合可以使用collect方法,把流转为数组就用toArray方法。

无参方法返回的时候一个Object对象数组,有参数的方法返回数组数据类型根据该方法参数函数定义的数据类型来确定。Generator函数用来生成一个数组数据类型。

看例子:

1
2
3
4
5
6
7
8
9
void stream_toArray() {

Object[] obarray = Stream.of("aa","bb","cc").toArray();

int[] array = IntStream.of(12, 4, 4, 6, 10, 3, 6, 8, 9).toArray();

long[] loarray = Stream.of("43","55","23","25","65").mapToLong(a -> Long.parseLong(a)).toArray();

}

第一行代码,通过of方法直接生成一个流,然后直接toArray,返回的肯定是Object类型的数组,不论流中保存的是什么类型的元素都是返回Object类型数组。

第二行代码,这里使用IntStraem的of方法生成的是IntStream流,然后使用toArray方法返回的就是int类型的数组,LongStream和DoubleStream也可以生成确定数据类型的数组。

第三行代码,其实就是通过mapToLong方法把普通流转化为LongStream。

上面示例中第一行代码,如果不想返回Object对象数组,我就想指定数组类型为String怎么办?看下面代码:

String[] strArray = Stream.of("aa","bb","cc").toArray(String[]::new);
这个时候就用到了带参数的toArray方法了,这个方法的参数可以指定返回数组的数据类型。

再来2个例子:

Integer[] intArray = Stream.of(12, 4, 4, 6, 10, 3, 6, 8, 9).toArray(Integer[]::new);

User[] userArray = Stream.of(new User("111@qq.com","北京"),new User("222@qq.com","shanghai")).toArray(User[]::new);

最后这个例子很直接的诠释了带参数toArray方法的重要性,就是我可以把任何数据类型的流转为以其相同数据类型的数组。

应用案例 LeetCode

349. 两个数组的交集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> s1 = new HashSet<Integer>();
Set<Integer> s2 = new HashSet<Integer>();
for (int i = 0; i < nums1.length; i++){
s1.add(nums1[i]);
}
for (int i = 0; i < nums2.length; i++){
if (s1.contains(nums2[i])){
s2.add(nums2[i]);
}
}
int[] res = s2.stream().mapToInt(x -> x).toArray();
return res;
}
}

注意 int[] res = s2.stream().mapToInt(x -> x).toArray();这行很方便的进行了转换!


JVM-堆

Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题

存放哪些资源

  • 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
  • 字符串常量池:
    • 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
    • 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
  • 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
  • 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率

堆内存相关

设置堆内存指令:-Xmx Size

内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常

堆内存诊断工具:(控制台命令)

  1. jps:查看当前系统中有哪些 Java 进程
  2. jmap:查看堆内存占用情况 jhsdb jmap --heap --pid 进程id
  3. jconsole:图形界面的,多功能的监测工具,可以连续监测

堆的划分

Java7 中堆内会存在年轻代、老年代和方法区(永久代)

  • Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间
  • Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区
  • Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理,但是触发GC的时机比较晚,内存管理效率不高

分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// 返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回Java虚拟机使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
}