四、java对象从new-内存分配-GC流程

对象的创建:

image.png

1.类加载检查

虚拟机字节码执行引擎执行jvm指令如果为new时,会查看方法区中常量池该类的符号引用,查看符号引用的类信息是否存在,如果不存在,则执行类加载过程。

2.加载类

执行类加载机制,通过双亲委派机制,使用类加载器加载类文件,验证字节码文件的完整性,将类中的静态变量赋值为初始值,将字面量的符号引用转换成直接引用,初始化赋值静态成员变量。

3.分配内存

分三种情况: 1.直接分配到old区(老年代) 2.分配到Eden区 3.分配在线程本地缓存区 分配内存的方式: 1.指针碰撞(默认使用)java堆中的内存规整的情况下,会将用过的内存和没用过的内存分开放置,将加载后计算好的类的大小,挪动类的大小的指针; 2.不规整的情况下 空闲列表(Free List) 如果内存为不规整的情况下,虚拟机就需要维护一个内存可用的列表,分配时从列表中查找,如果找到一块可用的内存,进行分配内存,并更新列表的状态 并发问题: 1.cas+失败重试: 在分配内存时可能被其他线程抢占分配,虚拟机会采用CAS+失败重试来保证内存分配的同步和原子性; 2.TLAB(Thread Local Allocation Buffer)线程本地缓冲区 在Eden 区会给每个线程分配一块小的内存,用来存储对象,是线程非共享的,只能当前线程访问,可使用­-XX:+UseTLAB开启,但是默认是开启状态;­XX:TLABSize可指定大小

4.初始化

内存分配后将对象实例变量初始化为初始值,例如int会初始为0,String 会初始为null;保证变量可以不用赋值就可以进行操作;

5.设置对象头

类在初始化后,会给对象进行设置,设置类的引用实例信息,还有在对象头中设置对象的hash值,对象的GC年龄,锁信息等; 对象在内存中的存储分为三个部分: 1.对象头 Object Header 对象头中分为两个部分,第一个为对象的运行时数据,包括对象的hashcode,锁的状态信息,是否偏量锁,对象被GC的分代年龄,第二个为类型指针,对类元信息的引用指针;(如果是数组对象的话,还有一个数组长度的数据部分) 2.实例数据 Instance Data 3.对象填充 Padding 个人理解:对象填充是优化对象存取速度,假设对象占用3个字节,会填充一个字节,改为为4个字节,4个字节存储起来速度是最优的; 指针压缩: -XX:-UseCompressedOops 关闭指针压缩,1.6以后默认开启指针压缩 32位系统一个对象指针占用的字节为4个,而64位操作系占用的为8个字节,在移动指针时性能较低,所以采用指针压缩方式,进而减少GC的压力;

对象在内存中的存储结构如下:

clipboard.png

对象的内存分配


image1.png

1.对象在栈上分配机制

对象在分配内存时,如上图所示,会先尝试在栈上分配内存,在分配之前,会有一个逃逸分析算法,他的作用呢也是优化垃圾对象尽快进入老年代的优化,大概逻辑为:判断对象是否会被外部引用,假设说有如下方法: 方法1: public void createUser(){ User user = new User(); } 方法2: public User createUser(){ User user = new User(); return user; } 方法1中只是创建了User对象,并没有对外返回User 对象,而方法2中将User返回了,调用方式时,肯定外部会对该对象的引用;所以当方式1执行时,可能就会分配在栈中,方法执行完 ,对象就会被回收了,而方法二中会有返回值,可能外部对它进行引用,则不会向栈中分配;jdk1.7以后默认开启逃逸分析; 全量替换: 当对象在认为在外部不会引用时,进一步分解时,虚拟机不会创建该对象,而是它的成员方法,变量分解为这个方法使用的成员变量,这样就不会因为栈中没有一块连续的空间而无法分配内存。 -XX:+EliminateAllocations jdk1.7以后默认开启全量替换;

2.tlab线程本地缓存区

如上图,如果会被外部所引用的情况下,虚拟机会首先判断是否为大对象,如果不是大对象的话,会尝试在Eden区中线程独有的一块区域中分配;

3.对象在Eden区分配机制

如果为大对象的话,会在Eden区进行分配内存,当Eden区的内存不足以存放该对象时,会出发minorGC/youngGc 年轻代的GC。 GC分为两种GC,一种是minorGC/youngGC,一种是majorGC/Full GC; minorGC负责对年轻代内存区域进行回收; majorGC负责老年代内存区域进行回收;

4.老年代分配担保机制

minorGC 后会计算老年代的内存空间,如果设置了一个参数“-XX:-HandlePromotionFailure” ,会判断之前minorGC后的平均内存如果大于老年代,如果大于的话会直接执行一次majorGC/Full GC,然后在执行minorGC。

5.对象动态年龄判断

个人理解:动态年龄判断机制,也就是说,在Eden区和survivor1区域gc复制对象时中假设说Survivor有三批对象,第一批对象占用10%,GC年龄为8,第二批对象占20%,GC年龄也为8,第三批 占用51%,通过虚拟机参数-XX:TargetSurvivorRatio可配置大小;它就会把第一批和第二批对象放进老年代;这个规则是想把对象尽早进入老年代,也是对jvm垃圾回收的一个优化;

6.大对象直接进入老年代

虚拟机为了优化垃圾回收,大对象会直接被分配到老年代,有个虚拟机参数-XX:PretenureSizeThreshold 可设置对象大小标准,如果超过这个标准会直接分配到老年代,而不会存放到年轻代,只针对于Serial和Parnew两个收集器的情况下有效;什么是大对象,需要连续的一块内存,例如字符串和数组;

7.长期存活的对象进入老年代

虚拟机给每个对象都标记了年龄,当在Eden区被gc后可以放到survivor区,这个对象接下来每次gc都会年龄+1,直到达到阙值后就会被放到老年代,通过-XX:MaxTenuringThreshold参数可以设置对象可存活的年龄。默认为15岁,CMS收集器的阙值为6岁。

对象的内存回收

1.引用计数器

这个算法是效率最高的,不过也有缺点,他的原理是,给对象添加一个计数器,每有一个引用就会计数器+1,如果释放了引用,就会-1;如果已经没有被引用后,他的计数器就会为0,就会被gc掉,可以又有这么一个问题: 有这么一段代码:思考 user1 和 user2 是否计数器会变为0; User user1 = new User(); User user2 = new User(); user1.property = user2; user2.property = user1; user 1 = null; user2 = null; user1 和user2的计数器是不会变为0的,以为他们相互引用;所以说基本上这个算法不会用上;

2.可达性分析算法

理解这么一个概念GC Root ,什么是GC Root,可以理解为祖宗,他是最高级别的对象,这个算法的原理就是,在执行gc时,会扫描,栈中的对象,类的静态变量,本地方法栈, 从GC Root开始查找引用的对象,如果有对该对象的引用,该GC Root就不会被标记为垃圾对象,其他的都会被标记为垃圾对象,等待被垃圾收集器所回收。如下图所示:


image2.png

扩充:

如何判断是一个无用的类

方法区主要回收无用的类分为三种:

1.堆中的对象已经被回收掉了
2.加载该类的classloader已经被回收了 
3.该类的java.lang.Class 对象已经没有存在引用关系,并且没有用反射获取方法和字段

你可能感兴趣的:(四、java对象从new-内存分配-GC流程)