一.JVM中对象的创建过程
1.详细过程
①检查加载(检查有没有进行类加载)
符号引用是什么?在类加载的时候,A常量池中可能包含对B的引用,但是B还没有加载进来,所以就用一个符号来表示B,这就叫符号引用,有时也用字面量来称呼。
所以检查加载这一步还会进行将符号引用变成直接引用的步骤。如果B加载进来了,就变成直接引用。
②分配内存
划分内存的方式:指针碰撞、空闲列表
- 指针碰撞:假设堆空间是规整的(无内存碎片),那就用一个指针一个一个的进行分配。
- 空闲列表:相反如果堆内存不规整,有内存碎片,那就用空闲列表来进行分配,这个列表记录了堆中空闲的位置。
还有一个问题:如何解决并发安全问题?CAS或本地线程分配缓冲
- CAS就不说了,在之前的文章中有介绍,下面介绍下后者
- 本地线程分配缓冲(Thread Local Allocation Buffer)是说给每一个线程在堆中的
Eden
区分配一块区域,这块区域专门用于该线程分配对象。如果有新线程,那么就划分一块新的区域。CAS比较费时间,本地线程分配缓冲就是空间换时间。
③内存空间初始化
比如int
你不赋值,那么默认赋0,boolean
默认赋false
等等。
④设置对象头等等
下面这个图好理解。类型指针就类似于记录下自己的祖宗,表明自己是属于哪一个类。我们平时接触最多的就是实例数据,就是对象里面那些变量啥的。
⑤对象初始化
也就是执行构造方法
2.对象分配策略
对象如何进行分配呢?
有句话:几乎所有的对象都在堆中进行分配,言外之意就是并不是所有的对象都在堆中,对象的分配其实是可以进行栈上分配的。
(1)如何进行栈上分配(虚拟机优化技术之一)
要满足两个条件①逃逸分析②触发JIT(热点数据)
怎么着才算是热点呢?循环达到一定程度才算。
比如我们进行一个循环,每次循环都调用一个方法,这个方法就是创建一个对象。
JVM遇到这样的代码,就不会每次都会解释执行,而是会触发一个技术,叫JIT动态编译技术,同时做“逃逸分析”,它分析我们创建的对象有没有可能逃出这个方法,逃出这个线程(通俗来讲就是这个对象能否被其他线程用到)。如果没有可能的话,那就将此对象分配到栈中。因为没必要为其他线程所共享,它逃不出去。这样就减少了在堆上的对象分配,减少垃圾回收次数,提高程序效率。
也就是说如果触发了JIT技术,同时JVM进行了逃逸分析,那么这样的对象可能就分配在栈中
(2)本地线程分配缓冲(虚拟机优化技术之二)
上面有讲,这里就不介绍了。(2)和(3)都是分配到堆中
(3)正常的分配(对象的分配原则)
①对象首先在Eden区进行分配
②大对象直接进入老年代(用的比较少)
③长期存活的对象进入老年代
④动态对象年龄判定
关于第③条,我们可以设置一个标准,比如对象的年龄大于15就进入老年代,但是这个标准在很多情况下是很难设置的,有时快溢出了,但是很多对象还没到达规定年龄,无法进入老年代。所以我们就进行一种策略,比如将对象按年龄从小到大排序,将对象的大小依次相加,什么时候大于某个值了,就把后面所有的对象移到老年代。这种策略就是动态对象年龄判定
⑤空间分配担保
就是担保对象从新生代进入老年代的时候一定能放得下。
总体流程就是下面这张图
二.对象的访问定位
1.两种引用
在上一篇文章中讲过了,对象的引用可能保存在虚拟机栈的局部变量表中,如图
那么局部变量表中的引用如何指向真正的对象呢?其实有两种方式
(1)直接指针
第一,就是直接指针。顾名思义,就是指针直接指向堆中的对象,没有中间者。这也是主流的方式,hotspot也是用的这种方式
(2)使用句柄
第二,就是 使用句柄。类似于有了一个中间者。
比如我们去按摩,我们可能是找13号技师,而不是直接指名道姓或者说出那个技师的身份证号来找到技师。13只是一个媒介,真正的人可能会更换,但是13可以一直保留。就类似于这个使用句柄。对象实例可以更换,但是引用不用变,都是指向13号技师。但是这样效率可能会更低,不如直接指名道姓来的更快。
2.四种引用
强引用:=
这个垃圾回收的时候是一定回收不了的
软引用:SoftReference
垃圾回收的时候有几率回收,比如马上要溢出的时候。
弱引用:WeakReference
只要发生了垃圾回收,就一定会被回收
虚引用:PhantomReference
比弱引用还要弱。有时用来监控垃圾回收是否正常。
三.Java自动化的垃圾回收(GC)
1.判断对象的存活算法
①引用计数算法
这个算法比较简单,就是说有几个引用指向它,它就记几
如上图,没有引用指向它,它就是“垃圾”,就可以被回收了。但是这个算法有缺点,看用紫色框圈出来的那两个,是互相引用的,但是两者都没法执行,都是垃圾,但是因为他们是1不是0,所以系统不会回收他们。
这种算法比较简单,但是无法解决循环引用问题,除非另启线程去专门处理这个循环引用。导致效率并不高
②根可达性分析算法(主流算法)
它定义了一些根,这些根都是集合,也就是RootSet
,这些根也被称为GC roots
,也就是跟垃圾回收相关的root set
。常见的就是下面这4种。它的思想就是根据根来判断哪些对象可达,那些不可达的就是垃圾,就要被回收
这种算法天生就解决了循环引用问题
③class如何回收?
为什么我们一般都说对象回收而不是class回收呢?原因很简单,就是class回收的条件非常的苛刻,具体的我就不说了,可以查下相关材料。所以class回收的次数要少得多,所以我们一般说对象回收
④Finalize
如果通过可达性分析,判断一个对象是垃圾了,在回收之前可以进行一个“拯救”,也就是Finalize
,就类似于“刀下留人”。这个用的非常少,知道作用就行了。平时不建议使用。
2.分代回收理论
堆分成了新生代和老年代,新生代又被分为Eden
,from
,to
。
垃圾回收一般指两种回收,一是Minor GC
或者Young GC
,另一种是Full GC
。前者是回收新生代,后者是回收新生代和老年代和方法区(class也可能会回收)
3.垃圾回收算法
①复制算法
预留一半空间,将非垃圾的对象进行复制,复制到预留的区域,再将原来的空间做一次整体的格式化
- 优点
实现简单、运行高效
没有内存碎片 - 缺点
空间利用率只有一半
经过统计后,新生代百分之98以上的对象都是垃圾,所以就没必要预留一半的空间了,此时Eden
区和from
区,to
区就来了。
②Appel式的复制回收算法
对象优先在Eden
区进行分配,存活的对象会到from
或者to
中的一个。Eden:from:to=8:1:1
这样能使空间利用率达到百分之九十以上。( s0
和s1
就是from
和to
)
上面的两种算法是针对新生代的。而老年代中的对象大部分都不是垃圾,所以就要换一种思路
③标记-清除算法
这个很简单,标记完了清除就可以了。
- 缺点
位置不连续,容易产生内存碎片 - 优点
可以做到不暂停。我们程序在运行的时候是不会用到这些垃圾对象的,所以该算法在工作的时候不会影响业务线程,可以做到不暂停。上面那种复制算法就得暂停
④标记-整理算法
先标记,再整理,最后清除。这样有利于整批清除,提高效率
-
优点
没有内存碎片 -
缺点
非垃圾对象进行了移动,所以要进行暂停