JVM垃圾回收

为什么Java需要JVM

  • java相比C/C++最显著的特点便是引入了自动垃圾回收,它解决了C/C++最令人头疼的内存管理问题,让程序员更关注程序本身,不用太多的关注内存回收这些烦恼的问题,这也是Java能一直占领编程语言排行榜的重要原因之一,哪到底什么是垃圾回收(GC)下面将详细阐述:
  • GC真正让程序员的生产力得到释放,但是程序员很难感知到他的存在,这就好比我们去吃自助餐吃完在桌子上放下餐具就走,服务员会替你收拾这些剩余的餐具和残留物,不用管他们什么时候来,怎么收
  • 哪有人说既然GC可以自动回收,那我们为啥还要去学习它,这么说貌似也没啥问题。在大多数情况下确实没啥大问题,不过涉及到一些性能调优,问题排查,深入的了解GC还是必不可少了,对于面试也有很大的帮助!所有了解GC是成为一名优秀Java程序员的必修课!

要了解GC首先要了解JVM内存区域和基本的理论

  • 要搞懂垃圾回收的机制,首先要知道垃圾回收主要回收那些数据,这些数据在那一块区域
    在这里插入图片描述在这里插入图片描述

  • 虚拟机栈:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈帧,主要保存执行方法时的局部变量,操作数栈,动态连接和方法返回地址等,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行GC。

  • 本地方法栈:与虚拟机栈的功能非常类似,主要区别在于虚拟机栈为虚拟机执行Java方法时服务,而本地方法栈为虚拟机执行本地方法时服务的,这块区域也不需要GC

  • 程序计数器:线程独有的,可以把它看作时当前线程执行的字节码的行号指示器,大家可以查看一下字节码指令对照表可以使用 javap -c D:\Hello.class查看 .class文件的字节码
    示列
    记录这些数据有啥作用呐,其实这些数字是指定地址,我们知道Java虚拟机的多线程是通过线程轮流切换并分配处理器的时间来完成的,在任何一个时刻,一个处理器只会被执行一个线程,谁抢到了这个时间片谁就去执行,如果这个线程被分配的时间片执行玩了并且这个线程的任务还没有完成它就会被挂起,等到下次再去抢到时间片再去执行,哪它怎么知道我们上次执行到哪了呐,这就需要我们通过记录在程序计数器中的行号指示器就可以知道,所有线程计数器的主要作用是记录线程运行的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行 所有这块区域也不需要GC

  • 本地内存:线程共享区域,在java8,本地内存也就是我们常说的堆外内存,包含元空间和直接内存,注意到我们上面画的图中java8和java8以前的jvm区域有区别,在java8以前有个永久代的概念,实际上指的是HotSpot虚拟机上的永久代,它用永久代实现了JVM规范定义的方法区功能,主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分由于在堆中实现的,受GC的管理,不过由于永久代有-XX:MaxPermSize的上限,所有如果动态生成类或大量执行String.intern(将字符串放如永久代)很容易造成OOM(当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error),所有java8把方法区的实现移到了本地内存中,这样方法就不受JVM的控制了,也就不会进行GC,也因此提升了性能,也就不存在由于永久代限制大小而导致OOM了(假设1G内存,jvm分配了100 那么理论上元空间也足够大了)所有java8此区域也不用GC

  • :前面的区域都不需要哪只剩下堆了,这是发生GC的区域!对象实列和数组都是在堆上面分配的,GC也主要堆这俩类数据进行回收,这块也是我们重点分析的区域

我们知道了回收区域,接下来我们来了解那些数据是需要回收的,又是怎么识别的或者说判断那些数据是否是垃圾的方法有那些

  • 引用计数法
String str = new String("java);

以上代码str引用了右侧定义的对象,所有引用的次数是1

String str = null;

以上代码str引用了右侧定义的对象,所有引用的次数是0,由于不被任何变量引用,此时如果发生GC就会被回收
看起来没啥大问题,不过它没法解决一个主要的问题:循环引用

public class Test{
Test instance;
public Test(String name){
}
public static void main(String[] args){
//第一步
A a = new Test("a");
B b = new Test("b");
//第二步
a.instance = b;
b.instance = a;
//第三步
a = null;
b = null;
}
}

到了第三步,虽然a,b都被设置为null了,但是之前互相指向了对方,所以无法被回收,也正是这个问题,所以现代虚拟就都不用这种回收算法

可达性算法

  • 现在基本都是采用可达性算法来判断对象是否存活,可达性算法的原理是以一系列叫做GC ROOT的对象为起点出发,引出它们指定下一个节点,再以下一个节点为起点,引出此节点指定下一个节点。。。这样通过GC ROOT串成一条线就叫做引用链,直到所有的结点都遍历完毕,如果相关对象不在任意一个以GC ROOT 为起点的引用链中,则这些对象就会被认为是垃圾,会被GC回收
    在这里插入图片描述

  • 如上图,如果用可达性算法即可解决循环引用的问题,因为从GC Root出发没有达到a,b所以a,b被回收
    对象a,b一定会被回收吗,并不是,对象的finalize方法给了对象一次挣扎的机会,当对象不可达时,当发生GC时,会先判断对象是否执行了finalize方法,如果未执行,则会执行finalize方法,我们可以再此方法里将当前对象与GC Roots关联,这样执行finalize方法后,GC会再次判断对象是否可达,如果不可达,则对象会被回收,反之不可回收!
    那么那些GC Roots到底时什么东西,那些对象可以作为GC Roots

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 方法区中类的静态属性引用对象

  • 本地方法栈中JNI(Native 方法)引用的对象

虚拟机栈中引用的对象

如下代码所示,a是栈帧中的本地变量,当a = null时,由于此时a充当了GC Root的作用,a与原来指向的实列new Test()断开了连接,所以会被回收

public class Test{
	public static void main(String[] args){
	Test a = new Test();
	a = null;
	}
}

方法区中类的静态属性引用对象

如下代码,当栈帧的本地变量a = null 时,由于a 原来指向的对象与 GC Root(变量a)断开了连接,所以a 原来指向的对象会被回收,而由于我们给了s赋值了变量的引用,s在此时时类的静态属性引用,充当了GC Root的作用 它指向的对象依然存活!

public class Test{
public static Test s;
	public static void main(String[] args){
	Test a = new Test();
	a.s = new Test();
	a = null;
	}
}

方法区中常量引用的对象

如下代码 常量a指向的对象并不会因为a指向的对象被回收而回收

public class Test{
public static final Test s = new Test();
	public static void main(String[] args){
	Test a = new Test();
	a = null;
	}
}

垃圾回收的主要方法

  • 标记清除法
  • 复制算法
  • 标记整理法
  • 标记清除法

1.先根据可达性算法标记出相应的可回收对象
2.对可回收对象进行回收
在这里插入图片描述
上述方法会产生内存碎片!假如我们想在上图中的堆中分配一块需要连续的内存占用4M,显然是会失败的,但是我们是否可以把上图中的内存碎片连成一片空间是不是就可以到达我们的目的了,哪又该怎么做?

  • 复制算法

把堆分成俩块区域A,B区域A负责分配对象,B区域不分配,对区域A使用以上所说的标记法把存活的对象标记出来,然后再把区域A中存活的对象都复制到区域B 最后再把A区对象全部清除释放空间,这样就解决了内存碎片的问题了。

在这里插入图片描述
不过复制算法的缺点很明显,比如给堆分配了500M内存,结果只能用一半,空间无故的减少了一半,这是不能接受的并且效率低下

  • 标记整理法

前俩步和标记清除一样,不不同的是它在标记清除的基础上添加了一个整理的过程,但缺点也很明显:每一次垃圾清除都要频繁的移动存活的对象,效率也很低在这里插入图片描述

  • 分代收集算法

分代收集算法整合了以上算法,综合了这些算法的优点,最大程度避免了它们的缺点,所有是现在优先级比较高的算法,倒不是说它是一种策略,因为它把我们上面说的算法整合到了一起,为啥需要分代收集呐?其实大部分对象都很短命,都在很短时间都被回收了,所以分代收集算法是根据对象的生命周期的不同讲堆分成新生代和老年代,以jdk1.8为例默认比例为1:2,新生代又分为Eden区,from Survivor(S0),to Survivor(s1)三者比例为8:1:1,这样就可以根据新老生代的特点选择合适的垃圾回收算法,我们把新生代发生的GC称之为Young GC(也叫 Minor GC),老年代发生的GC称之为Old GC(也叫Full GC)
在这里插入图片描述

  • 分代收集工作原理

1.对象在新生代的分配与回收

  • 大部分对象在很短时间内都被回收,对象创建时一般分配在Eden区,当Eden区将满时触发Minor GC,这样大部分对象被回收,只有少部分存活,它们会被移动S0区,同时对象的年龄+1(其实对象的年龄就是发生Minor GC的次数),最后把Eden区对象全部清理释放内存

  • 当发生下一个Minor GC时,会把Eden区存活的对象和S0(或S1)中存活对象一起移到S1,年龄+1,同时清空Eden,S0的空间

  • 若再触发一次Minor GC 则重复上述步骤,只不过此时变成了从Eden,S1区将存活对象复制到S0,因为每次垃圾回收s0,s1都会互换角色,都是Eden,S0(或S1)将存活的对象移动到S1(或S0)
    2.对象如何晋升老年代

  • 第一种:当对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代

  • 第二种:大对象 当某个对象分配需要大量的连续内存,此时对象不会创建不会分配再Eden区,会直接分配在老年代,因为如果把大对象分配在Eden区,Minor GC后在移动到S0,S1会有很大的开销

  • 第三种:S0(或S1)区相同年龄的对象大小之和大于S0(或S1)空间一半以上,则年龄大于等于该年龄的对象也会晋升到老年代
    3.空间分配担保

  • 当发生Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果大于,那么Minor GC可以确保是安全的,如果不大于,那么虚拟机先会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用连续空间是否大于历次到老年代的平均大小,如果大于则进行Minor GC 否则可能进行一次Full GC
    4.Stop The World

  • 如果老年代满了,会触发full GC 它会同时回收新生代和老年代,会导致Stop The World,造成很大的性能开销,因为只有垃圾回收线程在工作,其他线程则被挂起

  • 一般Full GC会导致工作线程停顿时间过长,如果此时server收到了很多请求,则会被拒绝服务!其实我们JVM优化也是减少Full GC(当然Minor GC也会造成STW,但只会是轻微触发)

  • 现在我们知道为什么要把新生代区设置成三个区域Eden S0 S1或对象设置年龄阈值默认把新生代和老年代空间大小设置1:2都是为了尽可能的避免对象过早的进去老年代,尽可能晚触发 Full GC

  • 由于Full GC 会影响性能,所以我们要在一个合适的时间点发起GC,这个时间点为称之为 Safe Point,这个时间点的选定既不能太少也不能过去频繁

垃圾收集种类

Java虚拟机没有规范使用那种收集器,因此不同的厂商,不同的版本虚拟机提供垃圾收集器可能会用差别,根据业务自己调整
在这里插入图片描述

  • 新生代收集器

  • Serial
    单线程收集器,单线程意味它只会使用一个Cup或一个收集线程来完成垃圾回收,进行垃圾回收时,其他用户线程就会停,直到垃圾回收结束,也就是说GC期间,此时的应用不可用,看起来不实用,不过我们知道任何技术都不能脱离场景,在client模式下,它就简单有效,对于限定单个线程来说单线程模式无需与其他线程交互,减少了开销,专心做GC能将单线程发挥到极致。
  • ParNew
    -ParNew就是Serial的多线程版本,其他都一样,它主要的工作在Server模式下,我们知道服务端如果接受的请求太多,相应时间就很重要了,多线程可以让垃圾回收得更快,也就是减少了STW的时间,提升响应时间
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值