第3篇:JVM对象创建与内存分配机制剖析

创建对象

  下图是一个对象创建的基本流程,通过流程中的每一个步骤来细化相关的知识点
image.png

加载类

  类加载过程可以参考类加载过程与双亲委派机制这篇文章

分配内存

  在类加载完成后,这个类所需的空间都是确定的,因此在创建对象时,给对象分配的内存空间也是基本确定的,JVM划分内存的两种方式如下

分配内存的方式

  • 指针碰撞(默认方式):指针碰撞就是开辟一段连续的空间用于存放对象,两个对象之间使用指针隔开
  • 空闲列表:空闲列表是虚拟机维护的一个列表。存对象时会根据对象的大小分配位置,并更新列表

分配的时候还会遇到的问题就是并发,针对并发JVM有两种解决方法

解决并发的方式

  • CAS(compare and swap):CAS思想在很多地方都有应用到,大概意思就是先比较在操作,加上重试机制保证了内存分配的原子性
  • TLAB(Thread Local Allocation Buffer):直译是本地线程分配缓存,大概意思是每个对象创建时,JVM都提前分配一块空间,虚拟机默认开启(-XX:+UseTLAB)

隐式初始化

  隐式初始化是JVM创建对象给的默认值

设置对象头

  对象头包括两部分信息,第一部分是自己当前类的加载信息比如: 如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等 ,第二部分是指向类的类型指针,代表自己是哪个类的对象

显式初始化

  显示初始化即程序员自己给定义的变量赋值以及调用构造方法。

指针压缩

  目前市面上的电脑大致有两种,分别是32位与64位的。在JVM中32位最大支持4G的内存(2的32次方,这里的的位就是bit,1byte = 8bit)。举个简单的例子,假设内存是8G,就需要33位来存储,如果不压缩64位就只能存一个33位,经过指针压缩后,64位可以存储两个,极大的提高了空间的利用率。

扩展

  • 堆内存大于32G时,指针压缩会失效,会使用64位对Java对象寻址
  • 堆内存小于4G时,不需要指针压缩。在压缩64位也是只能放对象

内存分配

在栈上分配

  通常创建一个对象,都是在堆内存里面分配内存空间,但是如果对象不逃逸的情况下,对象会被分配到对应的栈帧,使用完后立即释放,从而减少GC压力。另外大家都认为对象是存放在一块自己的内存空间下,实际JVM中存在一个标量的概念。假设内存可用空间都是小块零散的,每块零散的空间不足以放下创建的对象 ,这时候对象就有可能被拆分,a变量,b变量分别被放在两块不连续的空间。下面说说对象逃逸分析与标量替换。

对象逃逸

  简单来说就是创建对象的作用域,method1()方法返回了user对象,这个对象有可能被外部使用(逃逸)。而method2()方法的user对象只在方法内部使用,在这种情况下第二个方法的对象就会存在栈中,从而减少堆内存的使用,从而减少gc的次数

public User method1() {
   User user = new User();
   return user;
}

public void method2() {
   User user = new User();
}

标量替换

  标量在JVM中指的是可以被再分的量,假设新创建的一个对象中含有成员变量a,变量b。这个对象需要两个连续的格子来创建对象并存放变量。但是有下图所示情况(空白代表可用空间),没有连续的两个格子,这个时候JVM就不会创建这个对象,而是创建两个变量,将两个变量分别放在两个格子中。
image.png

在伊甸园分配

  大多数情况下,新建的对象都是存在伊甸园区的。我们知道轻gc是在伊甸园满了之后触发的,相对来讲伊甸园是越大越好。所以伊甸园与幸存0区,幸存1区的内存比例是8:1:1。
基于内存占位比,可能出现这样的情况,在伊甸园中的一个对象占用内存大于幸存0区的最大内存,此时这个对象应该何去何从?这就涉及到对象进入老年代的几种情况。

在老年代分配

分代年龄达到指定值

  上文中提到了每个对象都有对象头,对象头中包含了分代年龄字段。如果对象在轻gc后没有被回收,它的分代年龄就会增加1,直到分代年龄达到指定值(默认15)就会被移动到老年代中。这个默认值可以通过 **XX:MaxTenuringThreshold ** 來修改。

对象动态年龄判断

  堆内存中有幸存0区与幸存1区,每次gc对象都会在这两个区来回跑。当每次轻gc后会触发的一种机制,即对象动态年龄判断机制。这个机制的规则是在幸存区中,如果一批对象内存的占比和超过幸存0/1区的一半时,会将对象年龄大于等于 这批对象中年龄最大值的所有对象都送入老年代,这样为了减少gc的次数,既然迟早要去老年代,不如早点去?

大对象

  这里的大对象有两个特点,一个是占用内存空间大,另一个需要大量连续的空间(比如数组)。这样的对象会直接送入老年代,由于占用空间大的原因,在轻gc时会频繁造成对象的复制,效率比较低。
JVM如何判断一个对象是大对象呢?在JVM中有个参数是** -XX:PretenureSizeThreshold ** ,需要注意的是这种机制只有在收集器是Serial 和ParNew 时才会生效。

内存回收

引用计数法

  JVM会给每个对象一个引用计数器,每当对象被引用时,计数器就+1。因此我们认为当这个计数器为0的时候就可以判断这个对象是可以回收的,便会通知gc回收器进行回收。
这种方式是非常高效的,但是存在引用互相依赖的情况(类似于死锁,互相拿着对方的锁),导致这两个对象都无法被回收,因此目前主流的JVM都不是使用这种方式。

可达性分析算法

  这里要引入一个新的概念是GC根节点,这个根节点用于查找可以回收的对象,它可以是线程栈的本地变量,静态变量,本地方法栈的变量等。
  它的查找方式类似于数据结构中的树,从根节点出发往下找,引用到的会添加一个标记。最终内存中没有被标记的对象基本都会被回收。(为什么是基本,因为涉及到Java的finalize()方法,它可以自救一次)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jayden 

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值