一、虚拟机中对象的创建过程
类加载: 把class加载到内存中去
检查加载: 有没有加载进来 等等检查
遇到new 第一步是检查 能不能在 常量池方法区的常量池定位到类的符号引用
检查加载成功后 分配内存
分配内存有两种方式,两种方式划分内存:指针碰撞、空闲链表
指针指向最后一个空间,只能在堆空间很规整的办事(指针碰撞) ,垃圾回收后就不规整了零散的不规整的怎么办
空闲的和占用的交替的话用不了指针碰撞 所以用空闲链表。
堆空间的规整程度决定的用两种方式中的哪个
垃圾回收器带有整理(压缩)功能
带整理功能的就会使得内存规整,没有带整理的就不规整
(CMS)不带整理的垃圾回收器,只能使用空闲链表
带有整理的话使用指针,指针快一些,不需要查表
不管用什么分配内存方法同样可以用到多线程
多线程就会遇到安全问题
两种方式解决安全问题:CAS 加失败重试 、本地线程分配缓冲
本地线程分配缓冲:(Thread Local Allocation Buffer TLAB)
堆中分配成 Eden From to tenured
每个线程划分一块区域
在Eden区划分 ,不会再其他区划分
效率比较高
让我们对象分配更快,减少通过比较交换或者同步方法带来的性能的开销
TLAB只占 Eden 1% 占满了 再重新画一块
默认开启TLAB
新生代不够就拓展,实在没得拓展就OutofMemory
内存控件初始化 设置零值
设置:对象属于哪个实例
内存空间处理化:
分配完后是空的,需要把一些数据设置为零值
int = 0
boolean = false
不需要赋值就可以马上使用
设置对象是哪个实例的,设置对象头
对象初始化:
之后是构造方法
二、对象的内存布局
Hotspot虚拟机里面对象必须是8字节的整数,如果对象头和实例数据加起来是8的倍数,就不用填充了
三、对象的访问定位
对象的访问针对所有的虚拟机
两种方式: 使用句柄 直接指针
使用句柄好处:对象移动了,句柄池不需要改
坏处是:二次查找,指针定位的开销
直接指针坏处:对象移来移去的话reference需要改变
但是可以节约效率
hotspot等主流的虚拟机用的是直接指针的定位方式
两种方式的唯一区别就是 实例是否为直接指向,还是二次映射
堆空间满了,JVM进行垃圾回收(垃圾收集)
收集时要确定哪些对象活的 ,哪些对象死的
四、判断对象的死活两种方法:引用计数法、可达性分析
引用计数法
Python
一个地方引用了他 计数器就+1 失效了就-1 ===0 没人用 就回收
存在一个问题:对象相互引用
相互引用 外部没有与其连接这也是死
Python虚拟机 需要额外的机制(补偿机制)
可达性分析(根可达)
hotspot
根据一条链路追踪的
/**
* VM Args:-XX:+PrintGC
* 判断对象存活
*/
public class Isalive {
public Object instance =null;
//占据内存,便于判断分析GC
private byte[] bigSize = new byte[10*1024*1024];
public static void main(String[] args) {
Isalive objectA = new Isalive();
Isalive objectB = new Isalive();
//相互引用
objectA.instance = objectB;
objectB.instance = objectA;
//切断可达
objectA =null;
objectB =null;
//强制垃圾回收
System.gc();
}
}
[GC (Allocation Failure) 11900K->10787K(15872K), 0.0033926 secs]
[Full GC (Allocation Failure) 10787K->10786K(15872K), 0.0040005 secs]
[Full GC (System.gc()) 21171K->567K(36352K), 0.0043743 secs]
Object类里面有一个finalize方法
如果你写的类覆盖了这个方法
protected void finalize() throws Throwable { }
/**
* 对象的自我拯救
*/
public class FinalizeGC {
public static FinalizeGC instance = null;
public void isAlive(){
System.out.println("I am still alive!");
}
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize method executed");
FinalizeGC.instance = this;
}
public static void main(String[] args) throws Throwable {
instance = new FinalizeGC();
//对象进行第1次GC
instance =null;
System.gc();
Thread.sleep(1000);//Finalizer方法优先级很低,需要等待,单独线程跑
if(instance !=null){
instance.isAlive();
}else{
System.out.println("I am dead!");
}
//对象进行第2次GC
instance =null;
System.gc();
Thread.sleep(1000);
if(instance !=null){
instance.isAlive();
}else{
System.out.println("I am dead!");
}
}
}
finalize method executed
I am still alive!
I am dead!
finalize只能执行一次,sleep去掉的话救不活因为线程优先级低,所以这方法并不可靠,
I am dead!
finalize method executed
I am dead!
五、各种引用
强引用 =
(比如女儿) 只要方法在执行,我们的局部变量表就是GCRoots, 垃圾回收器回收不掉
软引用 SoftReference
(比如老婆) 即将OutofMemory ,也会垃圾回收掉
弱引用 WeakReference
(比如女朋友) gc 只要垃圾回收器回收,弱引用就会被回收,空间不够才会GC
虚引用 PhantomReference
(比如刚认识) 定义出来随时被回收掉
六、对象的分配策略
对象的分配原则:
- 对象优先在Eden分配
- 空间分配担保
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定
优化技术:
栈中分配对象 逃逸分析
堆中的优化技术 本地线程分配缓冲(TLAB)
new 之后是第一个优化技术 是否栈上分配
几乎所有的对象都在堆中分配,但不是100%,也可以在栈上分配
栈上分配(虚拟机栈)需要一个技术:逃逸分析
栈上分配是不需要垃圾回收的,不用考虑垃圾回收
对象满足逃逸分析,不会逃出方法体,同时不会逃出线程,栈上分配,不用回收
如果不满足逃逸分析
那么 本地线程分配缓冲(TLAB)
如果不符合TLAB,会判断 是不是大对象
Eden 伊犁园空间
如果是大对象(数组或者字符串),会直接放到老年代,就不用晋级了,老年代的大小大一些
新生代(Eden 、From、To )只占堆空间1/3
老年代占堆空间2/3
所以大对象放到老年代会避免被回收,因为老年代空间大一些
-Xms 30M
-Xmx 30M
Tenured 20m
Eden 8M
From 1M
To 1M
8:1:1
Eden 区只存放新生对象
里面的对象要么被回收掉,如果存活了,那就不会在Eden区存留 ,
会进入From区,对象头age+1
如果又有一次垃圾回收,存活了下来,age再次+1,进入To区
如果又有一次垃圾回收,存活了下来,age再次+1,进入From区
…
在From和To之间晃来晃去,这是 复制回收算法
复制回收效率高:只需要把存活的复制过去,处理剩下的就可以了,适合新生代,大部分对象new出来后会很早失效
From区 和 To区 空间往往是一样的,内存利用率只有一半,空间利用率50%
为什么还要有Eden区:
经过调查大部分对象是朝生夕死的
大数据分析90%以上的对象会第一次被回收掉,存活的只有10%,那我们放入From区,这样的话浪费的时间就是10%,空间利用率为90%
Eden区的对象是没有年龄的
From区和To区有年龄,经过一次GC存活年龄就加一。
年龄比特位用4位,年龄能记录的最大值1111=15,年龄最大值15
达到15次后进入老年代
进入老年代后就没有age了
垃圾回收器回收新生代叫 minorGC
垃圾回收器回收老年代叫 majorGC/fullGC
如果老年代要满了怎么办
悲观策略:每次进入老年代都要进行一次majorGC/fullGC
很消耗效率
所以提出了 空间分配担保
jvm来担保
只有实在是进不去了才会majorGC/fullGC
动态年龄判断 Survivor空间(from to) ,Eden进survivor进不去了 直接进 老年区