JVM相关知识总结:聊聊JVM的垃圾收集(一)

前言

Java相对于C/C++来说,引入了自动的垃圾收集。平时我们在开发中很难感知到GC的存在,但是如果涉及到性能调优,线上问题排查,我们必须对GC有一个深入的了解。

一、JVM内存区域

为了介绍GC,我们首先要知道数据在JVM中是如何存储的,GC主要作用于哪块区域。

对上图做一个简单分析,我们可以看到内存区域分为五个:

  1. 虚拟机栈: 线程私有,由一个个栈帧组成,每个栈帧对应着一个调用的方法,保存有方法的局部变量等信息。方法被调用,栈帧入栈,方法结束,栈帧出栈。入栈和出栈时机很清楚。所以这部分不需要进行垃圾收集;
  2. 本地方法栈: 类似于虚拟机栈,只不过本地方法栈执行的是本地方法;
  3. 程序计数器: 线程私有的,它的作用可以看做是当前线程所执行的字节码的行号指示器。我们知道JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会被挂起,而另一个线程获取到时间片开始执行。在JVM中,就是通过程序计数器来记录某个线程的字节码执行位置,当被挂起的线程重新获取到时间片的时候,就知道上次被挂起时执行到哪个位置了。这块区域也不需要GC;
  4. 方法区: 方法区主要存储类的信息,常量,静态变量。在java8之前,方法区在堆中,通过-XX:MaxPermSize配置,很容易造成OOM。在java8之后,方法区被移到了本地内存区域中,也就是所谓的直接内存,这块区域不受JVM控制。元空间的大小不受本地内存限制,所以java8以后,方法区不需要GC;
  5. 堆: 堆中主要存储java对象,数组等,垃圾回收主要回收这块区域。

二、如何判断对象是否可以被回收

在GC的算法中,有两种方法可以判断对象是否可以被回收:1. 引用计数法,当对象的引用次数为0时就可以被回收;2. 可达性分析算法,如果一个从GC Root开始不可达,则被视为垃圾。

2.1 引用计数法:

给对象添加一个引用计数器,每当一个地方引用该对象,计数器就加1,引用失去就减1。

优点: 实现简单,效率高

缺点: 无法解决对象之间的相互依赖问题,对象A中引用对象B,对象B中引用对象A

2.2 可达性分析算法

GC Root是一组活跃的引用,基本思路就是从一系列GC Root往下搜索,通过GC Root串成的一条线成为引用链,如果对象不在任何一条以GC Root为起点的引用链中,则可以被垃圾回收。

关于什么是GC Roots,在google中查询资料得到的解释如下:

Every object tree must have one or more root objects.As long as the application can reach those roots, the whole tree is reachable. But when are those root objects considered reachable? Special objects called garbage-collection roots(GC roots) are always reachable and so is any object that has garbage-collection root at its own root.

在Java中有四种GC Roots:

  • 虚拟机栈中引用的对象 ——Local variables are kept alive by the stack of a thread. This is not a real object virtual reference and thus is not visible. For all intents and purposes, local variables are GC roots;
  • 方法区中常量引用的对象——Active java threads are always considered live objects and are therefore GC roots. This is especially important for thread local variables;
  • 方法区中类的静态属性引用的对象——static variables are referenced by their classes. This fact makes them de facto GC roots. Classes themselves can be garbage-collected, which would remove all referenced static variables. This is of special importance when we use application servers.
  • 本地方法栈中JNI引用的对象——JNI reference are java objects that the native code has created as part of a JNI call.

三、常见的垃圾收集算法

垃圾收集算法主要有:

  • 标记清除
  • 标记整理
  • 复制算法
  • 分代收集算法

3.1 标记清除

       第一步: 找出活跃对象。根据可达性算法标记出需要回收的对象;

      第二步: 除了上面标记的对象外,其余的都清除掉。

  

缺点: 标记和清除的效率不高,并且在标记清除之后会产生大量的不连续的内存碎片。

3.2 标记整理

   前两个步骤和标记清除算法一样,不同的是在标记清除的基础上多了一个整理的过程。将所有存活的对象移动到一边,然后清理另外一边的区域,这样就可以避免内存碎片的产生。这只是一个理想的状态,在实际中,对象的引用关系一般是非常复杂的。在效率上来说,一般整理算法要低于复制算法。这个算法规避了内存碎片的产生。

3.3 复制算法

        复制算法在新生代中使用,新生代Eden:S0:S1 = 8:1:1,其中后面的1:1就是用来复制的。当其中一块内存使用完后,就将还存活的对象复制到另一块上,然后把已经使用过的内存空间一次清除掉。因为新生代中的对象一般都是朝生夕死的,存活的对象数量并不多,这样使用copying算法效率比较高。JVM将Heap内存划分为新生代和老年代,又将新生代划分为Eden区和两块Survivor Space,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。之所以按照8:1:1划分,这样始终有90%空间是可以用来创建对象的,剩下的10%用来存放回收后存活的对象。

步骤:

  1. 当Eden区满的时候,触发一次young gc,把还存活的对象copy到from区;当Eden区再次出发young gc的时候,会扫描Eden区和From区,对这两个区域进行垃圾回收,经过这次回收后存活的对象,直接复制到To区域,并且把Eden和From清空;
  2. 当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,把存活的对象复制到From区域。并且把Eden和To区域清空;
  3. 部分对象会在From和To区域复制来复制去,如果年龄超过15岁(JVM参数MaxTenuring Threshold决定,这个参数默认是15),最终会进入到老年代
  4. 如果存活的对象数量比较多,那么To区域不够放,这个时候会借助老年代空间。
  • 优点:

在存活对象不多的情况下,性能高,可以解决内存碎片和标记清除算法导致的引用更新问题。

  • 缺点:

会造成一部分内存浪费,不过可以根据实际情况对内存比例进行调整;

如果存活对象数量比较多,copying的性能会比较差。

四、为什么要STW

Java中的Stop The World机制简称STW,执行垃圾收集算法时,Java应用程序的其他线程都会被挂起,当垃圾收集完成后,再继续运行,所以尽量减少STW的时间,是优化JVM的目标。

可达性分析算法中,枚举根节点GC ROOTS 会导致所有java程序执行线程停顿:

  • 分析工作必须在一个能确保一致性的快照中进行;
  • 一致性指在整个分析期间整个执行系统像被冻结在某个时间点上;
  • 如果出现分析过程中对象引用关系还在不断变化,则分析的结果准确性无法保证。
  • 如果分析时判断该对象已经没有引用,但在分析下一秒又引用了,那么gc时如果将该对象清理,就会导致错误发生。

stop-the-world is guaranteeing that new objects are not allocated and objects do not suddenly become unreachable while the collector is running.

五、垃圾收集器

垃圾收集器是对垃圾收集算法的具体实现,目前没有哪种垃圾收集器是最好的,要根据具体场景结合不同垃圾收集器的特点做一个选择。常见的垃圾收集器除了G1垃圾收集器外,其他的都只作用于一个区域,要么年轻代,要么老年代。总共七种。

5.1 Serial收集器

Serial收集器作用于年轻代,是单线程的垃圾收集器。当垃圾回收的时候由于STW机制,其他工作线程都会被暂时挂起,直到垃圾收集结束。通过-XX:+UseSerialGC可以开启这种模式。

优点: 简单高效,拥有很高的单线程收集效率;

缺点: 收集过程中要暂停所有其他线程

算法: 复制算法

应用: Client模式下默认新生代收集器

收集过程:

5.2 ParNew收集器

       ParNew收集器是Serial收集器的多线程版本,作用于年轻代,默认开启数量和cpu数量一样多。数量可以通过修改ParallelGCThreads设定。

5.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,采用复制算法,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量:

吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间)

比如虚拟机总共运行了120秒,垃圾收集时间用了1秒,吞吐量=(120-1)/120=99.167%。

若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。

可以设置参数:

-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GC Time Ratio直接设置吞吐量的大小。

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值