JVM 垃圾回收前置知识

4 篇文章 0 订阅

什么是垃圾?

严格来说对于JVM而言什么算垃圾?了解过类加载之后,应该知道,JVM的运行时数据区中分配一块堆内存,里面存放了我们的实例对象。
那么什么样的对象才算垃圾呢?
我们通常创建一个对象时一般是这样色儿的:

User user = new User();

可以这样简单理解,new去帮我们在堆当中创建了User对象实例,并且把这个实例的引用返回赋值给了user变量。

那么我们可以这样去理解一个“合格”的对象,首先它得存在(堆中有实例),其次它被引用着(这里就包含上面那样被一个变量引用,还包含着如果这个对象是另一个对象的成员变量)

所以垃圾对象就被这样区分:指没有被引用的对象

//这里接着上面的代码
user = null;

我们给user变量重新赋值之后,原本的User对象实例便不再被引用,此时它就是垃圾。

为什么要回收垃圾?

虽然这个问题感觉怪怪的,但毕竟本篇是初探,所以讲得细致(废话多)一点。
刚刚也说了,我们创建的对象是被放在堆内存中的,而堆内存又是有一个固定内存大小的。如果我们不断的把对象实例放入堆中而不去清除,那么迟早它的内存会被用完对吧。因此我们需要回收一些内存。

如何进行回收?

首先第一个问题就是如何去识别处垃圾对象,总不能随便就去清理掉我们正在使用的对象。

GC ROOT

敲黑板!!!

简介

首先,GC ROOT 是一个算法。其次,它能完成我们上面所说的识别垃圾的功能。

原理

GC Roots基本思路就是通过一系列的称为“GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链( Reference Chain),当一个对象到 GC Roots 没有任何引用链相连( 用图论的话来 说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在这里插入图片描述
这里还涉及到一个三色标记法,但篇幅有限这里不去讲解。

class A{
	B b = new B();
}

class B{
    C c = new C();
    D d;
 }
class C{}
class D{}

有奖竞猜谁是垃圾?
结合代码和图,A对象又引用着B对象,B对象中引用着C对象,而D对象并未进行引用。C和D对象没有其他引用。
当然D对象是垃圾了。

GC ROOT对象
  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中的类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(即一般说的Native方法)中引用的对象

以上可作为GC ROOT对象,也就是起点,那顺着起点往下走,还是将就上面的图和代码

  1. A对象作为GC ROOT对象,往下找到B对象
  2. B对象往下找到C,然后没了
  3. D对象没人要

对象内存分配

对象内存分配流程图
在这里插入图片描述
仔细看上图,发现好像跟刚刚上面说的过程不太一样,栈内分配?什么鬼?
这里容我辩解一二:

番外篇

首先来讲讲堆,大部分人知道堆中存放对象实例,但是堆中内存如何划分呢?

年轻代和老年代

在这里插入图片描述
堆内存被这样划分为年轻代和老年代,前者占有 2/3 后者占有 1/3的堆内存。

对象一开始会存放在eden区中,待到eden区满了,会产生一次minor gc,也就是针对eden区的一次垃圾回收,并且会将一部分剩下还没回收的放入survivor区,此时这些对象逃过一次GC,那么阅历(分代年龄)+ 1
每经历一次gc 分代年龄都要+1,当年龄达到一定数值(默认15),将会加入到长老席位(老年代

那一定要等到15岁才能进入老年代吗?

非也,你要是minor gc之后,没回收什么对象,survivor区装不下,那就只能破格晋级,放入老年代

有年轻代的gc那老年代呢?

这个问题问的非常好,答案是肯定的啦,老年代的就叫做full gc
不过它并不是只限于回收老年代,你看都叫 full 了,它会对老年代,年轻代,方法区的垃圾进行回收。

在这里插入图片描述
从功能上大家都能看出来,它涵盖的范围不仅是老年代,还有年轻代和方法区(也就是常说的永久代,不过现在改为元数据区了)所以在性能上同minor gc相比较耗时大概是10倍。

机智的程序猿

ok,书归正传,刚刚番外篇里面八卦了一下堆,现在大家回头再来看这张图
在这里插入图片描述

什么叫做栈内分配

关于线程栈的知识这里不在赘述。可参考 jvm 内存结构
当我们频繁的创建对象时,容易触发gc,给gc带来很大压力,也会影响性能。为了减少临时对象在堆内存中分配的数量,JVM通过逃逸分析确定该对象是不是会被外部访问。如果不会,则会进行栈上分配内存,这就相当于利用帧栈的机制帮我们做垃圾回收,岂不美哉?

现在来解释上面加粗字体,先来看下面两个方法

public User getUser(){
	User u = new User();
	u.setId(1);
	...;
	return user;
}

public void test(){
	User u = new User();
	u.xx;
	...
}

上面两个方法中,u都是局部变量,但是getUser()将user返回了,也就是这个局部变量存在被外部引用的可能。那么我们就称这个user对象发生逃逸,而test()并未返回,纯粹局部变量,test方法结束之后,这个对象就没用了。

JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优
先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)

标量替换

通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该
对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就
不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认
开启。

标量与聚合量

标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及
reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一
步分解的聚合量。

综上:像test方法中的user对象就不必在堆中创建实例,而是将user中的属性以局部变量的形式放入栈中。

大对象

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。如果对象超过设置大小会直接进入老年代,不会进入年轻代。

为啥呢?

为了避免为大对象分配内存时的复制操作而降低效率。

TLAB

关于这个请大家参考这篇博客
https://www.jianshu.com/p/8be816cbb5ed

面向工资编程,respect!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值