new对象在JVM会产生怎么操作,从创建到分配和执行以及回收等一系列的流程,是这篇文章的具体分析。
JVM对象的创建
Object obj = new Object(); //创建New对象
大致分为4个步骤:
- 类加载器
- 检查加载
- 分配内存
- 内存空间初始化
类加载器
分配当期这个Class是属于哪个加载器所加载,之前有简单写过介绍 Android中的类加载,以及双亲委派机制
检查加载
根据指定是否能够在常量池定位带一个类的符号引用,并且检查该类是否被加载,加载以及初始化
内存分配
划分内存的方式,并且初始一些内存并发安全机制CAS,下面会着重写这块
- CAS(比较并替换):乐观锁 synchronized:悲观锁 。 之前有写过文章 Java并发同步锁
内存空间的初始化
执行init{}
代码块,初始化对象属性、构造函数直到被创建。虚拟机栈中的Reference
指向堆中的对象
内存分配
内存分配就是划出一些不等分区间数值,用来存储对象属性,那么对象内部又是如何分配
对象内存的分布
对象的组成大致分三大类:对象头、实例数据、对齐填充
- 对象头:描述对象信息的作用,自身状态
- 存储对象的运行时数据(Mark Word)
- 哈希码
- GC分代情况、年纪
- 锁状态标识
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针
- 若为对象数组,还应该有记录数组长度的数据
- 存储对象的运行时数据(Mark Word)
- 实例数据:具体信息内容
- 对齐填充:对象即使是没有内容,应该有对象头,所以最小是8字节,并且对象的内存大小是8的倍数,例如当前只有30个字节,那么就会补齐32字节,保证内存细分是等分
对象的访问定位
前面说在执行初始化后,因为栈与方法区的一些变量表只能存储基本数据类型,其对象与数组都是在堆内存放,那么就需要 句柄 方式访问对象
堆内存的划分
堆内存划分分 新生代 和 老年代 俩大块,为什么需要划分?主要是帮助提升GC垃圾回收器的效率与策略
一个新New的对象都是先进入新声代(End),通过多次GC递增后,对象依然存在,那么该对象就会晋级后面的区域,那么依据是是否被标记为 “垃圾对象”,在后面Gc回收机制在细分
Eden --> From --> To (约15次gc)--> Old
这样的好处,可以提升CG回收大量对Eden
区域处理,减少全部对象的筛选,就好比如在Old
内存的对象,是经过多次GC还存在,那么可以判断内部对象经常性被使用,就可以几乎完全没必要回收它。
内存比例大小原则
新生代 :老年代 (1/3:2/3)
Eden:From:To (8:1:1)
对象分配的一些原则
- 对象优先分配在Eden
- 空间分配担保
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判断
会走3个判断逻辑
- 是否分配在栈上
- 是否内存占用大对象
- 是否分配Eden本地线程TlAB中
New 一个对象:
//栈判断
if(是否分配在栈上){
Yes - 分配在栈
}else{
//大对象
if(是否内存占用大对象){
Yes - 直接分配Old 老年代/元空间
}else{
//是否本地线程缓冲区
if(是否分配Eden本地线程TlAB中){
Yes - Eden内部的TLAB线程中
}else{
独立的Eden中
}
}
}
GC回收机制
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
特别是当内存吃紧,不确定机制下以及 System.GC()
手动调用下触发GC回收器,释放堆中没有用 或是 “不重要”的内存空间。
判断对象是否为垃圾,是否可存活的一些依据
- 引用计数算法
- 可达性分析(根可达 GCRoots)
- Class回收
- finalize()
引用计数算法
是在对象头维护了一个 counter
标识,每次增加一次该对象的引用计数器自加,如果该对象的引用失联,则计数器自减,那么当counter
为0的时候,表示该对象已经被放弃,不处于存活资格应该被视为垃圾对象。
引用技计数算法在某些常见会失效,例如"强、软、弱、虚"引用下,还有就是俩个对象相互引用,造成死锁导致无法释放counter
,无法GC。
可达性分析算法
通过一系列的 GC Root Set :顾名思义就是GC 会通过 根节点的集合来依次向下查询,与根对象链在一起的引用,就形成链路集合,那么这个查询结束后,那位孤立,并且未在链路上的对象便可以视为“垃圾对象”。
通过图表的形式,可以很清楚的观察到,经过可达性分析后,孤立的没有在链路上的对象视为“垃圾”
还有就是在 “A/B”俩个对象在相互引用和指向对方,因为没被根集合所指向,当然也视为“垃圾”被回收
Class回收条件
在对象是可以频繁也是比较容易回收,而class
如果是想被回收是非常的困难,
- 类中的所有实例都已经被回收
- 所属的ClassLoader加载器被回收
- 并且该为没有被如何地方所引用
finalize
该方法是Object类中的函数,其作用就是 垃圾对象第一次是被伪清除,可以通过 finalze()函数所抢救回来,防止Gc把某些对象给清除掉,所以是在万物祖宗Object类中,但是finalize()函数有效性只有一次,当被抢救回来,下次GC再次标注为垃圾所清除就不具备抢救性,即使被finalize调用N次也没有用。
public class Presenter {
public static Presenter instance = null;
public void isAlive() {
System.out.println("Presenter 还活着,不为nNULL ");
}
@Override
protected void finalize() throws Throwable {//不推荐使用
super.finalize();
System.out.println("finalize 开启抢救模式");
Presenter.instance = this;//把引用接上
}
public static void main(String[] args) throws Throwable {
instance = new Presenter();
//对象进行第1次GC
instance = null;
System.gc();
System.out.println("instance = null GC ");
Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
if (instance != null) {
instance.isAlive();
} else {
System.out.println("Presenter 挂了 NULL");
}
System.out.println(" ");
//对象进行第2次GC
instance = null;
System.gc();
System.out.println("instance = null GC ");
Thread.sleep(1000);
if (instance != null) {
instance.isAlive();
} else {
System.out.println("Presenter 挂了 NULL");
}
}
}
//日志输出
finalize 开启抢救模式
instance = null GC
Presenter 还活着,不为nNULL
instance = null GC
Presenter 挂了 NULL
四种引用类型
不同的引用类型,更好的管理对象生产时间,结合业务和内存达到最优状态
强引用 Strong Reference
如果一个对象是强引用,它就不会被垃圾回收器回收,即使当内存空间不足,JVM也不会进行回收,而且抛出相应的OutOfMemoryError错误,使得程序进行终止,如果想让强引用对象可以被GC所回收,那么在使用该对象后进行赋值null
,这样GC在某次触发后便可以回调掉。
//只要obj还指向Object对象,Object对象就不会被回收
Object obj = new Object();
//手动置null
obj = null;
软引用 Soft Reference
在软引用的情况下,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器所回收,只有当内存紧张下,系统就会回收软引用对象,如果回收之后依旧内存不足,就会抛出内存益处OutOfMemoryError
弱引用 Weak Reference
弱引用所拥有的生命周更加短暂,当JVM进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收,垃圾回收器是一个优先级别较低的线程,并不一定及时发现弱引用对象。
虚引用 Phantom Reference
如果一个对象持有了虚引用,那么它相当于没有被引用,在任务时候都可能被垃圾回收器回收,虚引用有个特点也是区别弱、软引用的。虚引用必须和引用队列(ReferenceQueue)结合使用,当垃圾回收期回收该虚引用对象之前,先会加到引用队列中。
垃圾回收的基本回收算法
复制算法
执行步骤:
- 将可用的内存按照容量划分为大小相等的俩快
- 每次只使用其中的一块
- 当这块的内存使用完,就将还活着的对象复制到另一块上
- 最后把已经使用的内存空间一次全清理掉,
具有的特点:
- 实现简单,运行效率高效
- 没有内存碎片
- 利用率只有一半
Eden区的来源(Apple式回收)
为了避免内存空间浪费,提高空间利用率和空间分配担保,在复制算法基础上进行升级,也就是派上 “Eden、From、To” 内存区
- 首先把新生代内存空间分成3个区域,“Eden、From、To” 比例:8:1:1
- Eden把已经GCRoots链路上的对象,复制移动到 From上,清除Eden内存,此时Eden、To目前是干净内存
- 再次遇到GC回收,把Eden和From的链路,复制移到To内存,同时清除Eden、From内存,此时From为干净内存
- 又一次GC回收,把Eden和To内存的链路,复制移到From内存上,同时清除Eden和To,此时To目前还是干净内存【回到2步,来回交替复制,这样就提高空间利用率和空间分配担保】
- 需要注意的当某个对象内存一直在From和To内存来回的复制移动,就需要进入老年代内存区
标记清除算法
标记清除算法是直接根据GCRoots上的链路,保留,其他的孤立、未在链路上的对象空间直接回收释放
- 清除后会产生大碎片,位置不是连续
- 效率比较低
- 唯一特性优点就是不用阻塞线程
为了提高碎片凌乱问题,就有了整理,毕竟内存不连续会导致大一点的内存对象就无法存储,特别是Android里面对Bitmap
位图的内存声明,如果没有连续的内存就会出现OutOfMemoryError
异常
标记整理算法
顾名思义,就是在标记算法上的改良
- 可达性分析后
- 把链路进行整理排序,会造成指针调整
- 最后一把清除孤立的未引用的内存对象
综合这俩个算法
- 复制算法效率高,会阻塞线程
- 标记算法效率低,不会阻塞线程
- 标记算法不整理的情况下,会产生大量的碎片
常量的垃圾回收器
单线程回收器
多线程并行垃圾回收器
并发垃圾回收器
回收器 | 回收对象和算法 | 回收器类型 |
---|---|---|
Serial | 新生代、复制算法 | 单线程(串行) |
ParallelScavenge | 新生代、复制算法 | 并行多线程回收器 |
PaeNew | 新生代、复制算法 | 并行多线程收集器 |
Serial Old | 老年代、标记整理算法 | 多线程(串行) |
Parallel Old | 老年代、标记整理算法 | 并行多线程回收器 |
CMS | 老年代、标记清除算法 | 并行多线程回收器 |
G1 | 跨新生代和老年代、标记整理+化整为零 | 并发多线程回收器 |