什么是垃圾?
严格来说对于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对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中的类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即一般说的Native方法)中引用的对象
以上可作为GC ROOT对象,也就是起点,那顺着起点往下走,还是将就上面的图和代码
- A对象作为GC ROOT对象,往下找到B对象
- B对象往下找到C,然后没了
- 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!!!