当虚拟机遇到一条new指令时,首先会先检查这个指令的参数是否能在常量池
中定位到一个类的符号引用
,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,必须先执行相应的类加载过程
。
在类检查通过之后,虚拟机会给新生对象分配内存
。对象所需要的内存大小在类加载完成之后就可以确定,为对象分配空间的任务等同于把一块确定的内存从JVM堆
中划分出来。
指针碰撞
(Bump the Pointer): 默认使用; 如果JVM堆中的内存是绝对规整的,所有用过的内存在一边,空闲的内存放在另外一边,中间放一个指针作为分界点的指示器,那所分配内存就是将指针向空闲空间那边挪动一段与对象大小相等的距离空闲列表
(Free List):JVM堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,此时就无法使用指针碰撞了,JVM就必须维护一个空闲内存的列表,记录堆中哪些位置是可用的,在分配内存时,在列表中分配一块足够大的空间给对象实例,并且更新列表上的记录。无规则排列
的,并且未使用的内存空间的大小是不一致的,当一个对象实例需要存储的时候,就需要先去空闲列表中找一个和实例大小相匹配的内存空间,并且还需要更新空闲列表。指针碰撞
:当给对象A分配内存时,指针位置还未及时修改,此时对象B也使用原来的指针来分配内存空间,俗称抢内存。空闲列表
:当给对象A分配内存时,在空闲列表寻找合适对象A的内存空间,如果此时找到一块内存位置,还未及时存入对象A,空闲列表也未做更新,对象B也通过空闲列表找到同一块内存位置,此时就会出现两个对象争抢同一块内存空间的现象。CAS
(Compare And Swap): 比较与交换
,是实现多线程同步的原子指令,将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容更新为给定值。JVM虚拟机采用CAS并且配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。TLAB
(Thread Local Allocation Buffer):线程本地分配缓存区
,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存 。内存分配完成后,JVM虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)(如果使用TLAB,此步骤也可以提前至TLAB分配时进行),保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段类型所对应的零值。
★ 初始化零值之后,虚拟机需要对对象进行的必要的设置,例如:这个对象是哪个类的实例,如何才能找到类的元数据信息
,对象的哈希码值
,对象的GC分代年龄
等信息,这些信息存放在对象的对象头Object Heade
r中。
★ 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头
(Header)、实例数据
(Instance Data)和对齐填充
(Padding)。
★ HotSpot虚拟机的对象头包括 两部分信息
第一部分用于存储对象自身的运行时数据,如哈希码
(HashCode)、GC分代年龄
、锁状态标志
、线程持有的锁
、偏向线程ID
、偏向时间戳
等。
另外一部分是类型指针
,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
即对象按照程序的编码进行初始化,也就是属性赋值
(此处赋值,并非赋零值,而是真实的程序编码赋予的值)和执行构造方法
。
Mark word
是一种用于对象头部的标记
,它记录了对象的元数据信息和运行时状态。Klass pointe
: 是指向对象类元数据的指针
,在64位JVM中,klass pointer占据了4字节的空间。模拟: 对象大小和指针压缩(代码如下)
<!-- 可以明细jvm中的对象大小 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public class JolSample {
public static class Model{
//8B mark word
//4B klass pointer 如果关闭指针压缩,则占用8B
int id; //4B
String name; //4B 如果关闭指针压缩,则占用8B
byte b; //1B
Object o; //4B 如果关闭指针压缩,则占用8B
}
public static void main(String[] args) {
ClassLayout classLayout = ClassLayout.parseInstance(new Object());
System.out.println("Object对象" + classLayout.toPrintable());
ClassLayout classLayout1 = ClassLayout.parseInstance(new int[]{});
System.out.println("数组对象" + classLayout1.toPrintable());
ClassLayout classLayout2 = ClassLayout.parseInstance(new Model());
System.out.println("自定义对象" + classLayout2.toPrintable());
}
}
两种模式运行结果:
‐XX:‐UseCompressedOops
关闭指针压缩★ 什么是指针压缩?
指针压缩是一种内存优化技术
,可以减少程序中指针类型变量
所占用的内存空间。在32位系统中,每个指针类型变量占用4个字节的内存空间,而在64位系统中,每个指针类型变量占用8个字节的内存空间。指针压缩通过将指针地址转换为相对于某个基地址的偏移量来实现,从而可以将指针类型变量占用的内存空间减少至4个字节,而不会引起错误。指针压缩通常应用于内存占用较大的程序中,例如Java虚拟机。
★ 为什么使用指针压缩?
主要原因是为了减少内存占用
,提高性能
。
在32位的计算机系统中,一个指针占用4个字节,而在64位的计算机系统中,一个指针占用8个字节,而且64位指针存储的范围更大,会导致在某些情况下内存浪费。
因此,为了更好地使用内存,JVM引入了指针压缩技术。当堆内存小于32GB时,JVM会启用指针压缩,将对象引用从原来的64位压缩为32位。
内存概念: 内存是计算机中存放程序和数据的地方,它可以被电脑随时读取和写入。内存通常指的是随机存储器
(RAM),因为它可以随机访问,也就是在任何时间和任何位置都可以读取和写入。内存是临时的存储器
,当计算机关闭时,内存中的所有数据都会被清除。
计算机内存的大小通常以GB(千兆字节)
来计量,因为随着电脑处理速度的提高,程序和数据的大小也越来越大。在计算机运行多个程序时,内存的大小将会决定电脑的性能和响应速度。
JVM内存布局规定了Java在运行过程中的内存申请
、分配
、管理的策略
,保证了JVM高效的运行。
步骤解析: new一个对象,对象在JVM内存中是如何流转的?
逃逸分析
判断,如果对象不会逃逸,则在栈中
开辟一个临时空间(很小)存储,随着栈帧
的空间的回收而销毁。如果会逃逸,则会在堆中
开辟内存空间存储。老年代
中。TLAB
,如果开启,会在堆中的Eden
给当前线程开辟一块本地分配缓存区
用于存放对象,不管是否开启TLAB,对象都会存放在堆中的Eden中,只是存储在Eden中的位置不一样。OOM
Minor GC
次数增多,未销毁的对象会在S0
和S1
区域来回转移,当分代年龄达到设定阀值时,对象就会被移到老年代中。Full GC
,如果GC后对象还是在老年代中放不下,也会报OOM。技术诞生背景:当new一个对象时,对象是存在JVM的堆内存中,当对象没有被引用时,需要依靠GC来回收对象释放内存,如果对象的数量较多,会给GC带来压力,也间接的影响应用的性能。
为了减少临时对象在堆内存中分配的数量,JVM通过逃逸分析确定该对象会不会被外部引用,如果对象不会逃逸,则将对象在栈上分配一块临时内存存储,这样该对象所占用的内存空间就会随着栈帧出栈而销毁,减轻GC的压力。
public class EscapeAnalysisDemo {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
Person person = new Person(i, "Name " + i);
person.displayInfo();
}
}
static class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public void displayInfo() {
System.out.println("Person [id=" + id + ", name=" + name + "]");
}
}
}
在上面的示例中,创建了一个Person对象,并在main方法中执行一个循环,每次迭代都创建一个新的Person对象并调用其displayInfo()方法。如果JVM可以通过逃逸分析确定Person对象的作用域仅限于main方法中,则可以将其在栈上分配。
-XX:+DoEscapeAnalysis
java -XX:+DoEscapeAnalysis EscapeAnalysisDemo
-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime
-XX:+PrintCompilation
如果JVM成功地进行了逃逸分析,则可以看到较少的堆分配和更多的栈分配,并且程序执行速度更快。
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量
(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
总结:JVM在JDK7之后默认开启逃逸分析和标量替换,两者有任何一个被关闭,都会产生大量的GC,所以栈上分配依赖于逃逸分析和标量替换。
对象在Eden上的分配
当对象进入新生代中Eden区分配内存时,当Eden没有足够的内存空间进行分配时,JVM虚拟机就会触发一次Minor GC
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
★ 为什么设置大对象直接进入老年代?
为了避免为大对象分配内存时的复制操作而降低效率。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。
对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块S区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。
对象动态年龄判断机制一般是在Minor GC之后触发的。
★问: Minor GC与Full GC的区别?
Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也 比较快
Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上
★问: 为什么Eden与Survivor区(S0和S1区域)默认内存空间大小比例是8:1:1?
大量的对象被分配在Eden区,Eden区满了后会触发Minor GC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到S0区,下一次Eden区满了后又会触发Minor GC,把Eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到S1区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让Eden区尽量的大,Survivor区够用即可。
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy。
堆中存放着几乎所有的对象实例,在对堆中对象进行垃圾回收前第一步就需要判断堆中哪些对象是可回收对象(即不能被再被任何途径使用的对象)。
public class ReferenceCountingGC {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC a = new ReferenceCountingGC();
ReferenceCountingGC b = new ReferenceCountingGC();
a.instance = b;
b.instance = a;
a = null;
b = null;
}
}
虽然a与b对象最后都赋值为null,但是两者之间相互引用,导致两个对象的计数器不为0,GC不会将这两个对象当作垃圾回收的。
引用类型 | 说明 |
---|---|
强引用 | 普通的变量引用 public static User user = new User(); |
软引用 | 将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。 软引用可用来实现内存敏感的高速缓存 public static SoftReference user = new SoftReference(new User()); |
弱引用 | 将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用 public static WeakReference user = new WeakReference(new User()); |
虚引用 | 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用 |
代码演示对象的自救和回收:
public class OOMTest {
@Data
@AllArgsConstructor
static class Model {
private Integer id;
private String serialId;
@Override
protected void finalize() throws Throwable {
System.out.println("关闭资源,id=" + id + ",即将被回收");
}
}
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
int i = 0, j = 0;
while (true) {
list.add(new Model(i++, UUID.randomUUID().toString()));
new Model(j--, UUID.randomUUID().toString());
}
}
}
ClassLoader
已经被回收