JVM对象创建与内存分配机制学习总结

对象的创建过程

1、类加载检查
虚拟机遇到一条new指令时(new关键词、对象克隆、对象序列化等),首先会检查这个类是否已被加载、解析和初始化过。如果没有,要先执行相应的类加载过程。
2、分配内存
内存分配有两种方法:
“指针碰撞”(Bump the Pointer)默认用指针碰撞
在为对象开辟内存空间时,会把内存顺序摆放,即已用内存和空闲内存分开存放,通过指针向空闲空间那边挪动一段与对象大小相等的距离来实现内存分配。
“空闲列表”(Free List)
这种方式是已使用的内存和空 闲的内存相互交错,虚拟机会维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录。
内存分配时的并发问题
分配内存时,给对象A分配时指针没来得及修改,对象B又同时使用了原来的指针来分配内存。
解决方案:
CAS(compare and swap)
就是多个对象同时去抢一块内存空间,谁抢到了就分配给谁,没抢到的会重试去抢下一块内存空间
TLAB本地线程分配缓冲(Thread Local Allocation Buffer)JDK1.8默认使用此方式
就是每个线程在Java堆的Eden区中预先分配一小块内存,分配内存时会优先往自己线程的内存区域去分配,可以通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启),-XX:TLABSize 指定TLAB大小。
3、初始化
和静态变量初始化一样,内存分配完后,JVM需要将分配到的内存空间都初始化为零值(不包括对象头)。
4、设置对象头
一个完整的对象在内存中存储的布局可以分为3块区域:对象头、 实例数据、对齐填充(保证对象时8字节的整数倍)
对象头中包含,MarkWord,类型指针,数组长度。
MarkWord:其中包括对象的hashCode,分代年龄,锁指针等。
类型指针:对象new出来之后会存放在堆内存中,类型指针就是指向这个对象所在的方法区中的类信息的指针(jdk1.6 update14开始,64位操作系统JVM支持指针压缩)
数组长度:只有数组对象才有
指针压缩(默认开启)
启用指针压缩:-XX:+UseCompressedOops,禁止指针压缩:-XX:-UseCompressedOops
-XX:+UseCompressedOops 默认开启的压缩所有指针
-XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer
为什么要指针压缩?
为了节省内存空间,在jvm中,32位地址最大支持4G内存(2的32次方),通过一定的压缩算法压缩指针后,可以把35位(32G)的表述地址转化成32位存放在堆内存中,使用时通过cpu寄存器解压成35位,这样就可以让JVM用32位地址就可以支持<=32G的内存配置。
堆内存小于4G时,不需要启用指针压缩。
堆内存大于32G时,压缩指针会失效,会强制使用64位寻址,所以在64位平台中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右。
5、执行init方法
对初始化的属性进行赋值,并且执行对象中的构造方法。

对象内存分配

一、对象逃逸分析

对象逃逸分析就是分析对象动态作用域,就是对象在方法中被定义后,是否可能被外部方法所引用。

public void test() {
  User user = this.user1();
}
public User user1() {
   User user = new User();
   user.setName("pingfan");
   return user;
}
public void user2() {
   User user = new User();
   user.setName("pingfan");
}

1、逃逸分析:JDK7之后默认开启
user1方法中new了一个User对象,最后又把User对象当作返回值返回给调用者,这就叫对象逃逸。user2中的User对象没有被外部引用,它就没有逃逸,这种没有逃逸的对象可以优先分配到栈中,因为方法结束后这个对象就可以确定为是无效对象,让它在方法结束时跟随栈内存一起被回收掉,可以减少堆内存的gc压力。
1、标量替换:JDK7之后默认开启
标量就是java的八大数据类型,一个对象由最底层都是由基本数据类型组成的,所以一个java对象可以称为聚合量。通过逃逸分析确定该对象不会被外部引用时,这个对象尝试往栈内存中分配空间,但栈内存没有一块一大块连续空间导致对象内存不够分配,这时如果对象可以被进一步分解时,JVM会将该对象的成员变量分解多块,分别存放在内存碎片中。
1、逃逸分析参数:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
开启标量替换:-XX:+EliminateAllocations

二、对象分配流程

对象分配流程
1、大对象直接进入老年代
就是对象大小超过年轻代的内存大小,这类对象会直接进入老年代,大对象大小可以设置
2、长期存活的对象进入老年代
就是对象的分代年龄达到一定值后进入老年代(默认为15,CMS收集器默认6,不同的垃圾收集器不同)分代年龄可以通过参数设置
3、对象动态年龄判断
Minor Gc后,如果需要移动的一批对象的总大小,大于这块Survivor区域内存的50%,就会把年龄大于等于这批对象中年龄最大值的对象都放进老年代。
例:在一批对象中,其中年龄1+年龄2+年龄n的多个年龄段对象的内存总和超过了Survivor区域的50%,此时就会把年龄>=n的对象都放进老年代
4、老年代空间分配担保机制jdk1.8默认设置了参数
每次Minor Gc前JVM都会计算下老年代剩余空间,如果剩余空间大于年轻代里现有的所有对象大小总和(包括垃圾对象)就会直接Minor Gc,如果空间不足,但是开启了此机制时,就会看老年代的剩余空间,是否大于之前每一次Minor Gc后进入老年代的对象平均值,大于的话就Minor Gc,小于或者没有设置参数,就会Full Gc。
老年代空间分配担保机制
5、对象分配参数:
开启JVM运行参数显示:-XX:+PrintGCDetails
设置分代年龄:-XX:MaxTenuringThreshold
设置年轻代比例:-XX:+UseAdaptiveSizePolicy(默认开启),默认8:1:1比例会自动变化,如果想要保持8:1:1需开启:-XX:-UseAdaptiveSizePolicy
设置动态年龄判断比例:-XX:TargetSurvivorRatio
设置老年代空间分配担保机制:-XX:-HandlePromotionFailure
设置大对象大小:-XX:PretenureSizeThreshold=10000 (单位是字节) -XX:+UseSerialGC,需要配合Serial或者ParNew垃圾回收器使用

三、垃圾标记算法

1、可达性分析算法
把GC Roots对象作为起点,从这些节点开始向下搜索有引用到的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等
2、引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,引用失效,计数器减1,计数器为0的对象就是垃圾对象,但有循环引用的问题,比如图中A、B两个对象new出来的时候引用计数器都为1,然后B对象引用A的成员变量,A又引用B的成员变量,此时引用计数器都为2,最后两个对象都赋null,都-1变为1。此时方法结束,内存应该回收,但是引用计数器为1,就导致内存无法回收。

public class Test {

   Object test = null;
   
   public static void main(String[] args) {
      Test A = new Test();
      Test B = new Test();
      A.test = B;
      B.test = A;
      A = null;
      B = null;
      }
   }

引用类型一般分为四种:强引用、软引用、弱引用、虚引用
强引用被引用时不会被GC回收,其他引用就算被引用,GC的时候也可能会被回收掉。
强引用: 普通的变量引用,如new对象,被引用时不会被GC回收。
软引用: 用SoftReference软引用类型的对象包裹的对象,GC后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。比如网页中的回退操作,在当前页面打开了新的页面之后,然后又会退回当前页面,这时,当前页面的对象就可以使用软引用,当GC的时候可以把这些可有可无的对象回收掉,并不会造成什么影响。

public static SoftReference<User> user = new SoftReference<User>(new User());

弱引用: 用WeakReference软引用类型的对象包裹的对象,GC会直接回收掉。

public static WeakReference<User> user = new WeakReference<User>(new User());

虚引用: 最弱的引用关系,GC会直接回收掉,几乎不用。

对象的finalize()方法

对象在进行可达性分析后发现没有与GC Roots相连的引用链后会有两次标记
第一次标记: 判断对象没有实现finalize()方法,没有的话对象将直接被回收,有的话会进行第二次标记。
第二次标记: 会执行finalize()方法,我们可以通过在方法中重新与引用链上的任何的一个对象建立关联进行自救,比如把自己赋值给类变量或对象成员变量,一个对象的finalize()方法只会被执行一次

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值