一.堆和GC介绍
《深入理解java虚拟机》是怎么描述java堆的
1.java堆的特点
1)java堆(java Heap)是java虚拟机所管理的内存中最大的一块
2)java 堆被所有线程共享的一块内存区域
3)虚拟机启动时创建java堆
4)java 堆的唯一目就是存放对象实例
5)java 堆是垃圾收集器管理的主要区域
6)从内存回收的角度来看,由于现在收集器基本采用分代算法,所以java 堆可以细分为:新生代(YONG)和老年代(old).新生代又被划分为三个区域Eden(伊甸园区)、From Survivor、To Survivor等。无论怎么划分,最终存储的都是实例对象,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
7)java堆的大小是可扩展的,通过 -Xmx和 -Xms控制 如果堆内存不够分配实例对象,并且堆也无法扩展时,将会抛出outOfMemoryError 异常。
2.堆内存划分
堆大小=新生代+老年代。堆的大小可以通过参数 -Xms(堆的初始容量) 、-Xmx(堆的最大容量)来指定。
其中,新生代(Yong)被细分为Eden 和两个Survivor 区域,这两个Survivor 区域分别被名为为from和to,以示区分。默认的,Edem:from:to=8:1:1 (可以通过参数 -XXSurvivorRation来设定)。 即: 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一般为堆空间某个区发生了垃圾回收 新生代(Yong) 几户是所有Java对象出生的地方。即Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分通常不会长久的存活,具有朝生夕死的特点。当一个对象判断为“死亡”的时候,GC就有责任来回收掉这部分对象的内存空间。 新生代是手机垃圾频繁区域。
回收过程如下:
当对象在Eden(包括一个Survior 区域,这里假设是一个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 基本都是整个堆空间及持久发生了垃圾回收,所采用的是标记-清除算法。 现实的生活中,老年代的人通常会比新生代的人“早死”。堆内存中的老年代(不同于这个),老年代里面的对象几乎哥哥都是在Survivor 区域中熬过来的,他们是不会那么容易就“死掉”了的。因此,Full GC 发生的次数不会有Minor GC那么频繁,并且做一次Full GC要比进行一次Minor GC的时间更长,一般是MinorGC的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(); 所以, 他们都不会是垃圾, 因此都不会被回收.
首先, 我们启动程序, 然后在控制台启动jvisualvm
我们来看的是HeapTest, 这里面有很多性能指标可以查看. 我们重点看visual GC. 如果没有visual GC 可以参考这篇文章: xiaojin21cen.blog.csdn.net/article/det…
从这个图上,我们可以看到每过一段您时间, 触发一次GC, 因为不能被回收, 因此会转移到另一个survivor区域. 经过15次回收, 还没有收走, 那么就进入到old老年区.
老年区的对象越来越多, 当老年代对象满了以后, 会触发full GC, full GC回收的是整个堆以及方法区的内容. 实际上老年代没有能够回收的对象, 这时候在往老年代放, 就会发生OOM
使用这个工具还可以分析我们自己的程序代码的垃圾回收清空
三.Stop the world
在发生GC的时候,会发生STW ,Stop the word 。
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已经扫描完这里了,到这这一块垃圾没有被清理掉,要等待下一次垃圾回收来清理