1. JVM对象创建过程详解
对象创建的主要流程
1.1 分配内存空间的方法
- 指针碰撞(默认使用指针碰撞) 如果JAVA堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放一个指针作为分界点,那么所分配的内存就仅仅是把的那个指针向空闲空间移动一段与对象大小相等的距离
- 空闲列表 如果JAVA堆中内存是不规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单的指针碰撞了,虚拟机就必须维护一个列表,记录上那些内存块是可用的,再分配的时候从列表中找出一块足够大的空间分配给对象实例,并更新列表的记录
根据垃圾收集器的不一样,分配的内存方法略有不同
但是不管是那种分配方式,都会存在并发问题
解决并发问题的方法
- CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对内存空间的动作进行同步处理 - 本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)
把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,通过-XX:+/- UseTLAB参数来设定虚拟机是否使用
1.2初始化
相当于给静态变量等等一些列的方法赋予一个初值例如int i 默认初始化0 对象初始化null 等等
1.3设置对象头
初始化之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的jc分代年龄等信息。讲这些信息放在对象的对象头Object Header之中
- hotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充
- 下面是对象头的数据
- markWord标记字段(32位占4字节,64位占8字节)里面存放着自身运行时的数据:hash值,GC分代年龄等等
- Klass Pointer类型指针(开启压缩占4字节,关闭占8字节)这个Klass Pointer是干嘛用的呢,这里解释一下首先他是在堆里面,其次他指向了我们方法区里面的类元信息,这样我们就知道我们是哪个对象的实例了(个人理解)
- 数组长度(4字节,只有数组对象才有)
这里让我们来看看对象里面的具体数据
这里先导入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.9</version>
</dependency>
编写代码
package com.ruoyi.framework.com.qin;
import org.openjdk.jol.info.ClassLayout;
public class TestClassHeader {
public static void main(String[] args) {
ClassLayout classLayout = ClassLayout.parseInstance(new Object());
System.out.println(classLayout.toPrintable());
System.out.println();
ClassLayout classIn = ClassLayout.parseInstance(new int[]{});
System.out.println(classIn.toPrintable());
System.out.println();
ClassLayout classA = ClassLayout.parseInstance(new A());
System.out.println(classA.toPrintable());
System.out.println();
}
public static class A{
int id;
String name;
Object o ;
}
}
下面是结果
对象对齐
1.4 init方法
设置完对象头,按照程序员的意愿来赋值
2. 对象内存分配流程图
以前一般情况下我们是分配在堆里面的,后来慢慢地我们不一定了
这里有个比方 有两段代码
假如现在调用了test1()方法,返回了user,说明这个user是逃逸了,在其他地方是有用到的,下一个test2其实这个方法的user对象就只在当前方法下有用,说明没有逃逸,那么为什么我们不把他放在栈里面跟着方法执行完毕就把他释放掉,而要放在堆里面等待垃圾回收
jvm里面提供了一个方法,一般情况下默认开启了逃逸分析来优化内存,并且逃逸分析和标量替换会同时开启
- 标量替换:通过逃逸分析确定了该对象可以放在栈上面,当前对象不会被外部访问,并且对象可以进一步被分解时,JVM不会创建该对象,而是将对象的成员变量分解成若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续的空间导致对象内存不够分配
刚刚讲的是放在栈上面的如果是放在对上面的呢?
这里有个一例子
假设Eden区的内存大小为65536k s0区10752k s1区10752k 老年代175104k 那么
- 如果只是单独执行这个application1是放在eden区的
- 如果执行了application1和application2那么application1是放入老年代的,application2是放在Eden区的,为什么呢?首先这两块地方的内存大小已经超过了Eden区,如果放在s0区,s0区明显放不下,那么这个application1只能放在老年代
- 如果执行了所有那么,application1放在老年代,其他的全部放在Eden区
那这个如果我们想要减少垃圾回收,由于老年代比年轻代大得多,大对象我们是不是可以放入老年代呢,大对象就是需要大量连续空间的对象,(比如字符串、数组等等),那么JVM这里提供了一个参数 -XX:PretenureSizeThreshold可以直接设置大对象直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效哦
- 为什么要这样呢
避免了大对象分配内存的时候复制操作而降低效率,并且在一定程度上可以减轻minorJc
这里有一个机制:对象动态年龄判断机制,如果一批对象大于这块Survicor区域内存大小的50%,会直接进入老年代。例如Survivor区域里出现了一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor的区域的50%,此时会把年龄n和n以上的对象都放入老年代。这个规则其实是希望那些可以长期存活的对象,尽早进入老年代
老年代空间担保机制
3. 对象回收机制
就是gc垃圾回收,是如何回收的,这里一般用的是可达性分析算法,还有一个引用计数器算法但是一般不用,当发生相互引用的情况下这个算法会出现问题,相当于内存泄漏
可达性分析算法里面有一个finalize()方法,这个方法继承Object
当垃圾收集器准备回收这个类的实例的时候,就会执行这个方法,我们可以在这里建立与其他对象的连接,例如把自己的赋值给某个类或者变量挥着对象的成员变量。那在第二次标记清除的时候会将他移除即将回收的集合。如果这个对象还是没能逃脱,那么基本上他就真的被回收了
注意:一个对象的finalize()方法只会被执行一次,也就是通过调用finalize方法自我救命的机会只有一次
- 如何判断一个类是无用的类呢
- 方法区主要回收的是无用的类,那么怎么才是无用的类呢?
- 该类的所有对象实例都被回收,也就是java堆中不存在该类的任何实例,这就是说明堆里面不会再有对象头被引用
- 加载该类的ClassLoader已经被回收,每个ClassLoader都会维护一个他自己加载类的集合(这个条件挺苛刻的)那么就说明会app、ext、boot三类类加载器加载的类几乎很难被回收掉,这里说明其实你做了fullgc,其实也很难释放一定的空间
- 该类的java.lang.Class对象没有被任何地方引用,无法在任何地方通过反射来访问该类