对象的创建过程
-
package ex3; /** * @author King老师 * JVM遇到new关键字 * **/ public class ObjectCreate { private int age; private boolean isKing; public static void main(String[] args) { ObjectCreate objectCreate = new ObjectCreate();// System.out.println(objectCreate.age); //objectCreate 1 System.out.println(objectCreate.isKing); //objectCreate 13 //if else } }
JVM遇到一条字节码new指令
1.检查加载
- 检查类是不是存在,检查符号引用(说白了就是全地址),同时检查类是不是被加载过
2.分配内存
-
最重要的步骤
-
第一步检查加载通过进入第二步
-
从堆空间划一部分内存4
-
划分内存的方法
-
方法1:指针碰撞
-
适用的情况:
$$.内存中分配区域和未分配区域比较规整,连续
此时只需要从前一个对象使用完开始的连续空闲区域开始存储即可
-
-
方法2:空闲列表(cms不带整理空间区域)
-
适用的情况:
$$.堆内存不规整,内存中的空闲区域不连续,很分散
-
-
为什么堆内存可以是规整的,也可以是分散的?
常用垃圾回收器是带整理的,CMS垃圾回收器不带整理功能,不会对内存进行整理
-
-
解决并发安全,内存中的某一位置,可能同时有多个线程来抢这个位置
-
解决方法1:CAS加失败重试
-
cas简称compare and swap,先读取old值,然后比较old值和实时值是否相等,相等则把内存中的值更新为新值,不相等则cas失败
是一种乐观锁的方式;
-
上锁相比不上锁,性能肯定是会下降的,所以还有另外一种不上锁的方式
-
-
解决方法2:TLAB(本地线程分配缓冲)
-
Thread Local Allocation Buffer
-
新生代中eden区,线程1来了,事先在eden区划分一块区域,专门属于线程1,线程2来了,又给线程2划分一块区域,这块区域专属于线程2
-
受制于大小,一般只会为eden区的百分之一,如果分配区域比TLAB还要大,就会采用cas的策略
-
cas是有开销的,TLAB相比于cas效率高
-
-XX:+UseTLAB,默认情况下是开启的
-
-
3.内存空间的初始化
-
注意不是new一个对象的构造方法
-
碰到new字段,会进行内存空间的初始化,也就是0值,
例子
-
package ex3; /** * @author King老师 * JVM遇到new关键字 * **/ public class ObjectCreate { private int age; private boolean isKing; public static void main(String[] args) { ObjectCreate objectCreate = new ObjectCreate();// System.out.println(objectCreate.age); //objectCreate 1 System.out.println(objectCreate.isKing); //objectCreate 13 //if else } }
-
代码实例中,执行完ObjectCreate objectCreate = new ObjectCreate(),虽然age和isKing没有赋值,但是age初始化为0,isKing初始化为false
-
int默认是0,boolean默认是false
4.设置环节(设置对象头)
对象的组成
-
对象头Header,64位操作系统下是8字节,共64位
-
markword(存储对象自身的运行时数据)
- 哈希码
- 对象的GC分代年龄
- 锁状态标识
- 线程持有的锁
- 偏向线程id
- 偏向时间戳
-
类型指针
- 指向你是哪一类出来的
-
若为对象数组,还需要有记录数组长度的数据
-
-
实例数据
- 存储的对象数据
-
对齐填充(非必须),64位操作系统下,对象必须是8字节的整数倍,如果刚好是8字节的整数倍就不需要了
-
为什么要是8字节的整数倍?
方便内存分配和垃圾回收
-
5.对象初始化
- 经过前面的过程后,还需要调用java层面的构造方法
对象的访问定位
使用句柄
-
首先存在一个句柄池,存储的是reference引用到对象实例的指针
-
句柄池的好处?
对象修改后,不用修改引用,只需要修改引用在句柄池中的指针即可。另外如果对象消失后,只需要让指针指向空,但是性能降低了,需要多传递一次。
-
直接指针
-
hotspot中使用的是直接指针,直接指针就是引用上面指向的是谁,堆内存里面的对象就是谁
-
任何对象,除了对象实例数据以外,还有一个对象类型数据的指针
-
解决了句柄带来的开销,提高了性能
判断对象的存活
什么是垃圾
-
垃圾回收的重点是堆
-
c语言是手动操作内存,malloc(申请)–free(释放)
-
c++, new(申请)–delete(释放)
-
java是自动回收内存,编程上简单,系统不同意出错,而手动释放内存,容易出现忘记回收和多次回收的问题
-
问题1:忘记回收会造成内存泄漏
-
问题2:多次回收,可能导致某些对象被误删,会出现npe
-
多线程场景下:
第1个线程调用malloc、free、free
第2个线程在第1个线程两次free之前,调用malloc,此时第二个线程并没有释放,后续调用这个对象时直接报空指针了
-
-
-
没有任何引用指向的一个对象或者多个对象就是垃圾(循环引用)
判断对象是否存活的方法
1.引用计数法
- 对象被引用,计数就加1,引用被撤掉,计数就减一
- 缺点
- 无法解决对象间相互引用,引用计数都为1,循环引用的问题,这种对象实际跟主方法、跟线程没有任何关系,还是孤零零的一个对象,这是引用计数法无法解决的问题
- python使用的就是引用计数法,需要额外手段解决这种循环引用问题,gc效率比jvm低
2.可达性分析(根可达)
-
在jvm主流虚拟机都是采用的这种,比引用计数法更好
-
可达性分析又叫根可达
-
什么叫根?
-
根称为gc roots(前四种重点)
-
1.虚拟机栈(栈帧中的本地变量表)引用的对象
-
2.方法区中静态属性引用的对象
-
3.方法区中常量引用的对象
-
4.本地方法栈中的JNI(即一般所说的native方法)引用的对象
-
5.jvm内部引用的对象(class对象、异常对象npe、oomError、系统类加载器)
-
6.所有被同步锁(sysnchronized)锁住的对象
-
7.jvm内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
-
8.JVM实现中的"临时性"对象,跨代引用的对象(分代模型只回收部分代的对象)
$$j假设新生代和老年代的两个对象相互间有引用关系,如果只单一回收老年代或新生代,所以不能莫名其妙干掉与它相连的跨代的对象
-
-
-
可达性分析真正判断时用的是引用链,引用链与根直接相连、或者同在一条引用链上
- 1.判断的对象直接与gc roots可达
- 2.与gc root可达的对象中的属性引用了别的对象,凡是跟这个可达对象有这种引用关系的都不可回收,同样的跟这个引用对象再有引用关系的也不可回收,如此循环下去,直到不存在有引用关系的对象
-
哪些是可回收对象?
- 1.孤零零的对象
- 2.循环引用的对象,但是跟gc roots或者gc roots可达的对象却没有可达关系
-
天然解决了循环引用的问题,比引入计数法要优
验证是根可达实例
-
package ex3; /** * @author King老师 * 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();//objectA 局部变量表 GCRoots Isalive objectB = new Isalive();//objectB 局部变量表 //相互引用 objectA.instance = objectB; //强引用 objectB.instance = objectA; //切断可达 objectA =null; objectB =null; //强制垃圾回收 System.gc(); } }
-
objectA =null; objectB =null;
- 因为objectA和objectB属于gc roots,所以要把它置为null,则它的属性字段就变成不可达了
-
system.gc
-
打印结果
[GC (System.gc()) 25732K->880K(249344K), 0.0008033 secs] [Full GC (System.gc()) 880K->720K(249344K), 0.0047411 secs]
objectA和objectB一共是20M左右,但是gc之后只有不到1M,说明被内存被回收了
-
说明循环引用的问题可以解决
-
3.class回收条件
-
方法区怎么回收?class对象、静态变量、常量
-
必须同时满足以下所有条件
- 1.该类的所有实例都被回收,堆不存在任何实例
- 2.加载该类的类加载器被回收
- 3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法(就是存在这个类但是没有new的情况),这是一个非常严苛的条件,因为在java中反射可以射一切,这也就意味着没有任何地方可以用到它
$为什么单独划分一个方法区?
它的回收方法跟堆完全不同
- 参数控制:
- -XX:+Xnoclassgc,禁止类的垃圾回收,可配置
-
废弃的常量和静态变量的回收其实就和class回收的条件差不多,因为常量和静态变量定义在类里面的
4.finalize方法
-
判断一个对象是不是垃圾的时候,如果是垃圾也并不是立马回收!
-
特例:Object类 有一个finalize()方法
-
可回收的方法也不是立马回收,第一次判断是是否根可达,第二次判断finalize,对于没有根可达的对象,可以在finalize方法里面对它进行拯救,让这个对象不会被回收
-
重写finalize方法
package ex3; /** * @author King老师 * 对象的自我拯救 */ 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方法优先级很低,无法保证执行顺序,所以为了保证它执行finalize方法,这里执行第一次gc后,线程睡眠了1秒钟,所以finalize方法用起来很不确定,而且又释放对象又拯救对象就很矛盾,一般不推荐使用
-
执行两次finalize方法,只有第一次有效,第二次system.gc()方法就不会执行finalize方法了,对象只会执行一次
-
-
尽量不要使用finalize,有更好的方式,try-finally
- 如果觉得这个对象要拯救了,在finally里面拯救,重新跟gc roots挂上钩
各种引用
强引用 =
- = ,new出来的对象,用=和某个符号引用关联起来后,对于new出来的对象永远不会回收
- 垃圾收齐永远不会回收这部分对象,即使是oom也不会回收
软引用(SoftReference)
-
out of memory会对软引用进行回收,jvm在即将oom,会把这部分对象回收
-
软引用需要强引用构建
-
package ex3.reftype; import java.lang.ref.SoftReference; import java.util.LinkedList; import java.util.List; /** * @author King老师 * 软引用 * -Xms20m -Xmx20m */ public class TestSoftRef { //对象 public static class User{ public int id = 0; public String name = ""; public User(int id, String name) { super(); this.id = id; this.name = name; } @Override public String toString() { return "User [id=" + id + ", name=" + name + "]"; } } // public static void main(String[] args) { User u = new User(1,"King"); //new是强引用 SoftReference<User> userSoft = new SoftReference<User>(u);//软引用 u = null;//干掉强引用,确保这个实例只有userSoft的软引用 System.out.println(userSoft.get()); //看一下这个对象是否还在 System.gc();//进行一次GC垃圾回收 千万不要写在业务代码中。 System.out.println("After gc"); System.out.println(userSoft.get()); //往堆中填充数据,导致OOM List<byte[]> list = new LinkedList<>(); try { for(int i=0;i<100;i++) { //System.out.println("*************"+userSoft.get()); list.add(new byte[1024*1024*1]); //1M的对象 100m } } catch (Throwable e) { //抛出了OOM异常时打印软引用对象 System.out.println("Exception*************"+userSoft.get()); } } }
SoftReference<User> userSoft = new SoftReference<User>(u);//软引用 u = null;
1.其中u是强引用,而userSoft是软引用,同时干掉强引用,确保这个实例只有userSoft的软引用
2.然后虚拟机设置最大20M,同时不断往list里面添加对象,使得oom,然后捕获异常,并打印软引用,发现对象已被回收
-
缓存的图片、视频资源可以用软引用,因为这部分空间很大,即将oom时就把这部分回收掉
-
弱引用WeakReference
-
package ex3.reftype; import java.lang.ref.WeakReference; /** * @author King老师 * 弱引用 */ public class TestWeakRef { public static class User{ public int id = 0; public String name = ""; public User(int id, String name) { super(); this.id = id; this.name = name; } @Override public String toString() { return "User [id=" + id + ", name=" + name + "]"; } } public static void main(String[] args) { User u = new User(1,"King"); WeakReference<User> userWeak = new WeakReference<User>(u); u = null;//干掉强引用,确保这个实例只有userWeak的弱引用 System.out.println(userWeak.get()); System.gc();//进行一次GC垃圾回收,千万不要写在业务代码中。 System.out.println("After gc"); System.out.println(userWeak.get()); } }
-
只要发生垃圾回收,这部分对象一定会被回收
-
比较经典的使用也是缓冲,创建一些不太重要的缓冲,weakHashmap
虚引用PhantomReference(幽灵引用)
- 随时可能被回收
- 用的很少
- 在jvm启动时,需要自我检查,检查垃圾回收线程是否正常,可以设置一些虚引用,如果能被回收,说明jvm正常,起到监控垃圾回收期的作用
对象的分配策略
对象的分配原则
-
对象优先在Eden分配(几乎所有对象都在堆空间分配,因为虚拟机中还会有个技术,可以在栈上分配,此时必须满足逃逸分析)
-
逃逸分析触发在栈上分配有很多原因
1.这个对象很小
2.一个大的for循环触发
-
-
空间分配担保(minor gc前会对老年年的最大可用连续空间进行判断,相当于担保,如果小于则还会判断是否大于历次晋升的平均对象大小,如果是则还是进行minor gc)
-
大对象直接进入老年代(不需要在新生代和老年代之间交换)
-
长期存活的对象进入老年代(垃圾回收)minor gc
-
动态对对象年龄判断(垃圾回收)minor gc
虚拟机优化技术
-
package ex3; /** * @author King老师 * 逃逸分析-栈上分配 * -XX:-DoEscapeAnalysis -XX:+PrintGC */ public class EscapeAnalysisTest { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); for (int i = 0; i < 50000000; i++) { //5000万次---5000万个对象 allocate(); } System.out.println((System.currentTimeMillis() - start) + " ms"); Thread.sleep(600000); } static void allocate() { //逃逸分析(不会逃逸出方法) //这个myObject引用没有出去,也没有其他方法使用 MyObject myObject = new MyObject(2020, 2020.6); } static class MyObject { int a; double b; MyObject(int a, double b) { this.a = a; this.b = b; } } }
-
逃逸分析
-
对象不会逃逸出一个方法,这个引用也没有其他地方使用
-
满足逃逸分析,分配在栈上
-
默认开启逃逸分析
-
-XX:-DoEscapeAnalysis -XX:+PrintGC
关闭逃逸分析,打印GC日志
-
static void allocate{
person a = new person(18,“man”);
}
-
创建对象时,虚拟机分析过程
-
new 一个对象时,虚拟机优化处理的过程
- 1.判断是否在栈上分配
- 是则生成在栈上,否进入第2步
- 2.本地线程分配缓冲
- 是则优先在eden区分配,否进入第3步
- 3.是否是大对象
- 是则直接在老年代Tenured上分配,否则在Eden区分配
- 避免多余的垃圾回收,因为大对象迟早会回收
- 1.判断是否在栈上分配
-
-XX:PretenuredSizeThreshold = 4m(大对象的标准),只对Serial和ParNew两种收集器可用有效;
jvm有自己的判断逻辑去判断是否是大对象;
长期存活的对象
- 垃圾回收过程,从整个机器的第一次垃圾回收开始
- 1.如果eden区满了,进行一次垃圾回收,复制回收算法,判断对象存活(可达、强引用),进行垃圾回收(minor gc)之后对象仍然存活,对象会进入from区(swap1),对象头里面分代年龄(age)会加1
- 2.然后如果eden区又满了,所有的对象会被复制到to区,从eden区进入to区的对象分代年龄为1,同时把from区的对象也复制到to区,同时所有对象的分代年龄加1
- 3.假设eden区又满了,又复制存活的对象到eden区,它的分代年龄是1,同时把to区的对象复制到from区,这些的对象的分代年龄都加1
- 4.按照1,2,3的过程循环往复,当对象的分代年龄达到15,进入老年代
- 为什么是15,hotspot mark word,分代年龄最大是二进制 4位,最大就是15,所以就是15,而cms变小了一点,只有6
- 长期存活的年龄是可以设置的,但是一般不设置
- -XX:MaxTenuringThreshold,最大年龄
动态对象年龄判断
- 有可能很多对象的分代年龄还没有达到15,但是此时它所在的from区或者to区中,同一年龄的对象超过了from区或者to区的一半,这些对象全部进入老年代,到了老年代,就不区分年龄了
gc
空间分配担保
- 不同代之间转移的过程
- 1.有空间分配担保,在minor gc之前,老年代的最大连续可用空间是不是大于新生代所有对象的总空间,如果成立则进行一次minor gc就是安全的
- 2.如果不成立,则会判断老年代的最大连续可用空间是不是大于历次晋升的平均大小,是则进minor gc,否则进行full 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,对象变为可回收,不会立刻回收,垃圾回收器需要空间满了才会触发垃圾回收