一. 堆和GC介绍
1.java堆的特点
《深入理解java虚拟机》是怎么描述java堆的
- Java堆(Java Heap)是java虚拟机所管理的内存中最大的一块
- java堆被所有线程共享的一块内存区域
- 虚拟机启动时创建java堆
- java堆的唯一目的就是存放对象实例。
- java堆是垃圾收集器管理的主要区域。
- 从内存回收的角度来看, 由于现在收集器基本都采用分代收集算法, 所以Java堆可以细分为:新生代(Young)和老年代(Old)。 新生代又被划分为三个区域Eden、From Survivor, To Survivor等。无论怎么划分,最终存储的都是实例对象, 进一步划分的目的是为了更好的回收内存, 或者更快的分配内存。
- java堆的大小是可扩展的, 通过-Xmx和-Xms控制。
- 如果堆内存不够分配实例对象, 并且堆也无法在扩展时, 将会抛出outOfMemoryError异常。
2.堆内存划分:
- 堆大小 = 新生代 + 老年代。堆的大小可通过参数–Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定。
- 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Edem : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。
- 即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
- JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为存储对象,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
- 新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
3.堆的垃圾回收方式
java堆是GC垃圾回收的主要区域。 GC分为两种: Minor GC、Full GC(也叫做Major GC).
1. Minor GC(简称GC)
Minor GC是发生在新生代中的垃圾收集动作, 所采用的是复制算法。
GC一般为堆空间某个区发生了垃圾回收,
新生代(Young)几乎是所有java对象出生的地方。即java对象申请的内存以及存放都是在这个地方。java中的大部分对象通常不会长久的存活, 具有朝生夕死的特点。
当一个对象被判定为“死亡”的时候, GC就有责任来回收掉这部分对象的内存空间。
新生代是收集垃圾的频繁区域。
回收过程如下:
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
2.Full GC
Full GC 基本都是整个堆空间及持久代发生了垃圾回收,所采用的是标记-清除算法。
现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,一般是Minor GC的 10倍以上。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作
扩展: Minor GC是如何触发的, 又是如何工作的? 如下图:
Minor GC是由字节码执行引擎触发的. 当我们的程序中需要new一个对象的时候, 就会将这个对象放入到Eden区域, 当Eden区域中的对象越来越多, 直到满了, 这时放不下了, 就会触发字节码执行引擎发起GC操作. 第一次发起的GC, 将会看看哪些对象还活着, 哪些对象已经不用了, 活着的对象放入survivor中的一个区, 不再被引用的对象, 直接被回收了
如何判断对象是否还活着呢?
字节码执行引擎会去找很多gc root.
什么是gc root呢?
GC Root是一个对象, 以这个对象作为启动点,从这些节点开始向下搜索引用的对象, 找到的对象都标记为非垃圾对象, 其余未标记的对象都是垃圾对象.
GC Root根节点有哪些?
线程栈的局部变量, 方法区中的静态变量, 本地方法栈的变量等等。
垃圾收集的原理
在Math中, 我们看栈中main方法的局部变量表中的math变量. 方法区中的user变量. 他们都是GC Root根对象. 他们指向的是一块堆内存空间.
实质是, GC垃圾回收的过程, 就是寻找GC Root的过程. 从栈中找局部变量, 从方法区中找静态变量. 从GC Root出发, 找到所有的引用变量. 这些变量可能会引用其他的变量, 变量还会再引用其他变量. 直到不再引用其他变量为止, 以上这些都是非垃圾对象. 如果一个对象没有被任何对象引用, 那它就是垃圾对象。
垃圾对象最后就被回收, 非垃圾对象进入到Survivor的一个区域里面. 每次进入sruvivor区域,对象的分代年龄都会+1, 分代年龄保存在哪里呢?保存在对象头里面.
程序还在继续运行, 又会产生新的对象放入到Eden区, 当Eden区又被放满了, 就会再次出发GC, 此时会寻找Eden+sruvivor(一个区域)中的GC Root, 将其标记,
没有被引用的对象被回收, 其他被引用的对象会保存到另一个survivor区域. 分代年龄+1
这样运行, 直到分代年龄为15(默认15,可设置)时, 也就是GC发生了15次还活着的对象, 就会被放到老年代.
通常什么样的对象会被放到老年代呢?
静态变量引用的对象, 静态常量. 比如说: 对象池, 缓存对象, spring容器里面的对象,
二. 使用工具查看GC流转的过程
我们使用的工具是jvisualvm工具, 这是jdk自带的一个工具。这个工具通常是在开发环境使用,因为其本身比较耗性能,所以线上一般不用。本地调试可以使用。
先来准备一段代码, 一段很简单的代码, 不停的去产生新的对象
package com.lxl.jvm; import java.util.ArrayList; import java.util.List; public class HeapTest { public static void main(String[] args) throws InterruptedException { List<User> userList = new ArrayList<>(); while (true) { userList.add(new User()); Thread.sleep(10); } } }
我们来按照上面的逻辑分析代码
- userList: 是放在栈中的局部变量表中的一个变量
new ArrayList<>(): 是放在堆中的一个对象 - new User(): 在堆中构建一个新的User对象, 并分配了一个地址,并将这个地址添加到new ArrayList()中.
这里面 userList是根对象, new User()最终会被newArrayList()引用, 而userList又引用new ArrayList(); 所以, 他们都不会是垃圾, 因此都不会被回收.
那么死循环不停的构造对象, 添加引用. Eden区迟早会放满, 放满了就会触发GC, 那么GC能把他们回收呢? 回收不了, 因为都在被GC Root直接或间接引用. 最终都会被放入老年代. 然后还在持续构造新的对象,最终会怎么样?最终会内存溢出. 我们来看看可视化效果。
首先, 我们启动程序, 然后在控制台启动jvisualvm
我们来看的是HeapTest, 这里面有很多性能指标可以查看. 我们重点看visual GC. 如果没有visual GC 可以参考这篇文章: https://xiaojin21cen.blog.csdn.net/article/details/106612383
从这个图上,我们可以看到每过一段您时间, 触发一次GC, 因为不能被回收, 因此会转移到另一个survivor区域. 经过15次回收, 还没有收走, 那么就进入到old老年区.
老年区的对象越来越多, 当老年代对象满了以后, 会触发full GC, full GC回收的是整个堆以及方法区的内容. 实际上老年代没有能够回收的对象, 这时候在往老年代放, 就会发生OOM
使用这个工具还可以分析我们自己的程序代码的垃圾回收清空
三. Stop The World
在发生GC的时候, 会发生STW, Stop the world.
1. 什么是Stop The World呢 ?
举个例子:在一个电商网站,用户正在下单,这是由于内存满了,触发GC,这时候整个线程就会处于停滞状态。用户的感受就是一直在loading。。。。直到GC完毕,应用线程恢复工作。所以,Stop The World对我们的用户是有一定影响的。JVM调优主要的目的就是减少Full GC的次数和时间。minor GC也会stop the world,但是他的时间很短,所以我们重点调优还是在full gc
2. 那么为什么一定要stop the world呢? 不STW不可以呢?
回答这个问题, 我们可以使用假设法, 假设没有stop the world 会怎么样?
我们知道, 在垃圾回收之前, 要先找到GC Root, 然后标记是否被引用, 最终没有被引用的对象就是我们要回收的垃圾. 那就是没有对象引用他了.通常会回收这块内存空间地址 这个时候, 如果主线程也在运行, 刚好有一个变量存放在这个内存地址了, 而你并行的触发了GC, 这时候程序就发生混乱了.
这是一种情况,另一种是在触发GC的过程中,一部分变量正在被标记,而GC已经开始了,标记完以后,发现了垃圾,结果由于GC已经扫描完这里了,到这这一块垃圾没有被清理掉,要等待下一次垃圾回收来清理。