一、JVM对象创建过程
1.检查加载
检查这条指令的参数是否在常量池中定位到一个类的符号引用。并检查该类是否被加载解析初始化过。
符号引用:用一组符号来描述所引用的目标。JAVA在编译的时候,每个java类都会编译成一个.class文件,但在编译的时候虚拟机是不会知道所引用的类的实际地址的,所以就用符号引用来代替。而在类的解析阶段则会把符号引用替换为真实的地址。
2.分配内存:
分配内存需要处理的两大问题:内存划分的方式、并发处理如何安全。
1) 划分内存的方式:指针碰撞、空闲列表
指针碰撞(规整的内存):
空闲列表(不规整的内存):
2)解决并发安全:CAS失败重试、本地线程分配缓冲(TLAB)。
CAS失败重试:
TLAB:
3.内存空间初始化。
成员变量赋初值。
4.设置对象头。
对齐填充:对象所占空间需要8字节的整数倍。
5.对象初始化。
执行构造方法。
二、对象的访问定位
1.句柄。
在堆中有句柄池,在操作数栈里保存的引用是指向句柄池的。然后再通过句柄池找到真实的对象。
优点:当进行内存整理的时候,在操作数栈中的地址可以不进行改变,依然指向句柄池,只不过句柄池对应的对象地址发生了变换。
缺点:耗时需要进行二次转换。
2.直接指针。(hotspot使用)
操作数栈的对象地址直接是对象的真实地址。
优点:查找速度快。
缺点:当对象地址改变了,需要改变线程里的对象地址。
三、垃圾收集器。
1.判断对象的存活
核心:有用,且可以执行。
1)引用计数法。
问题:两个对象相互引用。
python用的这个算法:通过额外启动一个线程来解决这个问题。
2)根可达算法
roots:root set(根的集合)
gcroots:一般是静态变量、线程栈里局部变量表里的变量、常量池、JNI(指针)
扩展:
1) 内部引用:class对象、异常对象、类加载器
2)同步锁:synchronized(对象) 锁里的对象。
3)内部对象:
4)临时对象:跨代引用(老年代持有新生代对象的引用)
面试题:CLASS对象能被回收么?
1. 这个class对象不能有实例对象。
2. 这个类的类加载器被回收了。(因为类加载器会持有创建这个class对象的引用)
3. class对象没有没有在任何地方引用,并且没有被反射使用。
4.参数控制:允许回收class对象。
2.finalize()方法。
当一个对象不满足根可达的时候,当内存不足的时候,会调用finalize方法一次。如果在内部重新变成可达,则不会被回收(理论上的)。当再次不可达便不会进入这个方法了,会直接被垃圾收集器回收掉。
但这个方法的执行线程优先级很低,很多时候对象已经被回收了才会执行这个方法,已经为时已晚。
3.各种引用
1)强引用:不会被回收。
2)软引用:当内存不足的时候(将要OOM),会被回收。
3)弱引用:当进行垃圾回收的时候就会被回收。
4)虚引用:通过此引用无法获得对象。
4. 对象分配策略
对象的分配技术:1)对象分配原则。 2)虚拟机的优化技术。
1.对象的分配原则
- 对象优先在Eden区分配
- 空间分配担保:新生代升级进入老年代默认老年代是放的下的。如果老年代放不在再进行fullgc.担保的范围根据每次fullgc的回收量带动态计算。
- 大对象直接进入老年代:serial、parnew垃圾回收器才会有这个行为>4M。
- 长期存活的对象进入老年代。最大是15,CMS默认是6.
- 动态对象年龄判断。
1)先根据逃逸算法,判断对象是否逃逸,如果没有逃逸,则进行标量替换,把对象拆分到栈里。
2)逃逸了,则判断对象是否是大对象,如果是大对象,则把大对象直接放入老年代。
3)如果不是大对象,优先往TLAB里创建对象。
4)如果TLAB里放不下,则放入Eden区。
2.虚拟机的优化技术
- 逃逸分析 + 触发jit(热点数据) -> 满足两点才能进行栈上分配。
- 本地线程分配缓冲
5.垃圾回收算法
复制算法、标记清除算法、标记整理算法
1)复制算法
特点:(用于新生代的幸存者0,1区)
- 实现简单运行效率高。
- 没有内存碎片。
- 利用率只有一半。
appel式回收(新生代的划分方式)
因为绝大部分都是垃圾。所以新生代划分是8:1:1的比例。(空间利用率百分之90)
2)标记清除算法
特点:
- 位置不连续,产生碎片。
- 可以做到不暂停
3)标记整理算法
特点:(使用的标记整理算法是先整理再清除)
- 没有内存碎片。
- 指针需要移动,耗时。