创建对象
下图是一个对象创建的基本流程,通过流程中的每一个步骤来细化相关的知识点
加载类
类加载过程可以参考类加载过程与双亲委派机制这篇文章
分配内存
在类加载完成后,这个类所需的空间都是确定的,因此在创建对象时,给对象分配的内存空间也是基本确定的,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就不会创建这个对象,而是创建两个变量,将两个变量分别放在两个格子中。
在伊甸园分配
大多数情况下,新建的对象都是存在伊甸园区的。我们知道轻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()方法,它可以自救一次)