玩转JVM中的对象及引用-0721
回顾
1.jvm包括方法区、堆、栈
2.处理的流程
3.堆的分代划分,
4.JHSDB可以查看内存映射,对象放在哪里,
5.查看了对应的栈,以及栈帧之间共享数据,
6.深入辨析了堆和栈
7.oom中的不同类型
8.常量池划分了三种,运行时常量池在堆里面
9.上节课作业
-
String str1 = “abc”;
String str2 = new String(“abc”);
String str3 = str2.intern();
-
此处比较的引用对象的地址
-
false、false、true
1.虚拟机对象的创建过程
- 虚拟机发现new这个关键字
1.1对象的创建过程
-
1.检查加载
- 检查类是不是存在,检查符号引用(全地址),同时检查类是不是被加载过
-
2.分配内存
-
从堆空间划一部分内存
- 指针碰撞
- 内存中分配区域和未分配区域比较规整,连续
- 此时只需要从前一个对象使用完开始的连续空闲区域开始存储即可
- 空闲列表(cms不带整理空间区域)
- 内存中的空闲区域不连续,很分散
- 指针碰撞
-
解决并发安全
-
CAS加失败重试
-
compare and swap,比较old值和实时值是否相等,相等则交换,不相等则再来一次;
是一种乐观锁的方式;
-
有一定的开销
-
-
-
TLAB(分配缓冲)
- 新生代中eden区,线程1来了,事先在eden区划分一块区域,线程2来了,又给线程2划分一块区域
- 受制于大小,一般只会为eden区的百分之一,如果分配区域比这个大,就会采用cas的策略
- 相比于cas效率高
- -XX:+UseTLAB,默认情况下是开启的
-
-
3.内存空间的初始化
- 碰到new字段,会进行内存空间的初始化,
- 零值,int默认是0,boolean默认是false
-
4.设置环节(设置对象头)
- 对象头Header
- 哈希码
- GC分代年龄
- 锁状态标识
- 线程持有的锁
- 偏向线程id
- 偏向时间戳
- 类型指针
- 若为对象数组,还需要有数组长度
- 对象头Header
-
5.对象初始化
- 进过前面的过程后,还需要调用java层面的构造方法
2.对象的内存布局及访问
2.1内存布局
- 对象头
- 运行时数据-mark word
- 哈希码
- …
- 类型指针
- 对象数组,应该有记录数组长度的数据
- 运行时数据-mark word
- 实例数据
- 对齐填充(非必须)
- 对象头+实例数据必须是8字节的整数,方便进行内存分配和垃圾回收
2.2对象的访问定位
java栈中的本地变量表是指向句柄,还是直接指针
- 使用句柄
- java堆中存在句柄池
- 句柄是到对象实例数据的指针(对象实例数据存在于堆中的实例池),到对象类型数据的指针(对象类型数据存在于方法区中)
- java堆中存在句柄池
- 直接指针(hotspot中使用直接指针)
- 到对象类型数据的指针
- 对象实例数据
- 解决了句柄带来的开销,提高了性能
3.对象的存活及各种引用
3.1判断对象的存活
-
什么是垃圾
-
垃圾回收的重点是堆
-
c语言是手动操作内存,malloc free
-
c++, new delete
-
手动是否内存,容易出现忘记回收和多次回收的问题
- 忘记回收会造成内存泄漏
- malloc 13号 free free
- 可能导致某些对象被误删,会出现npe
-
没有任何引用指向的一个对象或者多个对象(循环引用)
-
-
1.引用计数法
- 对象被引用,计数就加1,引用被撤掉,计数就减一
- 无法解决对象间相互引用,循环引用的问题,这种对象实际没有被线程使用,但是计数也不为0
- python使用的就是引用计数法,需要额外手段解决这种循环引用问题,gc效率比jvm低
-
2.可达性分析(根分析)
- 1.什么叫根?
- gc roots(前四种重点)
- 虚拟机栈引用的对象(局部变量表)
- 方法区中的静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(native)引用的对象
- jvm内部引用的对象(class对象、异常对象npe、oomError、系统类加载器)
- 所有被同步锁(sysnchronized)锁住的对象
- jvm内部的jmxBean、JVMTI中注册的回调、本地缓存等
- JVM实现中的临时性对象,跨代引用的对象(分代模型只回收部分代的对象)
- gc roots(前四种重点)
- 引用链与根直接相连、或者同在一条引用链上
- 解决了循环引用的问题
- 1.什么叫根?
-
实例验证
-
new Object A [10 * 1024 * 1024] 两个都是10m
new Object B
//设置循环引用
A = null // 切除了与局部变量表的联系 切断根可达
B = null
system.gc
- gc 25723k -> 848k
- 说明循环引用的问题可以解决
-
-
3.class回收条件
必须同时满足以下所有条件
- 该类的所有实例都被回收,堆不存在任何实例
- 加载该类的类加载器被回收
- 该类对应的java.lang.class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法(就是存在这个类但是没有new的情况)
- 参数控制:-XX:-Xnoclassgc,禁止类的垃圾回收,可配置
-
4.finalize方法
-
Object类 有一个finalize()方法
-
可回收的方法也不是立马回收,第一次判断是是否根可达,第二次判断finalize
-
重写finalize方法
super.finalize();
FinalizeGc.instance = this;
-
finalize方法优先级很低,无法保证执行顺序
-
执行两次finalize方法,只有第一次有效,
-
-
尽量不要使用finalize,有更好的方式,try-finally
-
3.2 各种引用
引用(main方法) -> T对象 -> B对象
T对象和B对象之间的引用 强、软、弱、虚
-
强引用 =
- = ,new出来的对象,用=给某个引用
- 垃圾收齐永远不会回收这部分对象,即使是oom也不会回收
-
软引用(SoftReference)
-
out of memory会对软引用进行回收,jvm在即将oom,会把这部分对象回收
-
软引用需要强引用构建
-
SoftReference userSoft = new SoftReference<~>(x);
x = null;//干掉强引用
system.gc();
//发现此时不会被回收
//新建一个list,不断往里面充填字节 new byte[1024 * 1024 * 1]
//此时会被回收掉
-
缓存的图片、视频资源可以用软引用,因为这部分空间很大,即将oom时就把这部分回收掉
-
-
-
弱引用WeakReference
- 将上一步中的SoftReference替换成WeakReference
- 只要发生垃圾回收,这部分对象一定会被回收
- 也是用在缓存部分,certLocker,weakHashmap
-
虚引用PhantomReference(幽灵引用)
- 随时可能被回收
- 用的很少
- 在jvm启动时,需要自我检查,检查垃圾回收线程是否正常,可以设置一些虚引用,如果能被回收,说明jvm正常,起到监控垃圾回收期的作用
4.对象的分配策略及优化技术
4.1对象的分配策略
-
对象的分配原则
-
对象优先在Eden分配(几乎所有对象都在堆空间分配,因为虚拟机中还会有个技术,可以在栈上分配,此时必须满足逃逸分析)
-
空间分配担保(minor gc后会对老年年的最大可用连续空间进行判断,相当于担保,如果够就不会触发老年代的gc过程)
(老年代原本有500m,已经使用了490m,如果minor gc进入的空间大于剩余,会触发major gc)
-
大对象直接进入老年代(不需要在新生代和老年代之间交换)
-
长期存活的对象进入老年代(垃圾回收)minor gc
-
动态对对象年龄判断(垃圾回收)minor gc
-
-
虚拟机优化技术
-
逃逸分析
-
对象不会逃逸出一个方法,这个引用也没有其他地方使用
-
满足逃逸分析,分配在栈上
-
默认开启逃逸分析
-
-XX:-DoEscapeAnalysis -XX:+PrintGC
关闭逃逸分析,打印GC日志
-
static void allocate{
person a = new person(18,“man”);
}
-
-
-
创建对象时,虚拟机分析过程
-
new 一个对象 ->1.判断是否在栈上分配 -> 2.本地线程分配缓冲 ->3.是否是大对象
-
1满足在栈中生成
-
1不满足,2满足,生成在eden区
-
1,2不满足,3满足,放在Tenured区(避免多余的垃圾回收,因为大对象迟早会回收);
-XX:PretenuredSizeThreshold = 4m(大对象的标准),只对Serial和ParNew两种收集器可用有效;
jvm有自己的判断逻辑去判断是否是大对象;
-
1,2,3不满足,生成在eden区
-
-
长期存活的对象
- 如果eden区满了,复制回收算法,判断对象存活(可达、强引用),进行垃圾回收(minor gc),对象会进入from(swap1),对象头里面分代年龄(age)会加1,然后如果eden区又满了,所有的对象会被复制到to区,同时把from区的对象也复制到to区,同时分代年龄加1,当分代年龄达到15,进入老年代
- 为什么是15,hotspot mark word,分代年龄最大是二进制 4位,就是15,cms是6
- -XX:MaxTenuringThreshold,最大年龄
-
动态对象年龄判断
- 不是要求所有对象的年龄达到15才进入老年代,如果相同年龄的对象总和超过了from或者to内存的一半,这一部分对象可以直接进入老年代,到了老年代,就不区分年龄了,
-
gc
- eden from to : minor gc
- tenured : major gc 或 full gc
-
空间分配担保
- 悲观策略----minor gc -> major gc
- 有空间分配担保,先进行minor gc,然后再判断老年代是否能放下,如果能发下,就不会major gc,如果放不下,就进行major gc
- 悲观策略效率很低,实际上分代作用没有体现,空间分配担保就可以允许你冒险,实际几率很大,只有5%的可能性放不下,如果major gc后也放不下,那就是oom了
答疑
1.动态年龄判断,
from区中某一个分代年龄的总和已经超过了from区的一半,就直接进去老年代(一次回收的垃圾太多,因为复制回收算法,之后也是来回的复制)
2.任何对象回收都需要进行可达性分析、各种引用
3.full gc是可管理的区域都会回收
4.什么是否进入from区,什么时候进入to区,如果对象还活着,触发minor gc,就会交换进入另一个区
5.本地线程缓存是为了保证并发安全,相比于cas效率更高
6.full gc和major gc,只有cms单独针对老年代,full gc除了新生代、老年代、元空间,所以用full gc会更精准点
7.分析逃逸,这个对象是否可以在其他地方使用,
8.把对象置为null,对象变为可回收,不会立刻回收,垃圾回收器需要空间满了才会触发垃圾回收