@Alpacadh
2022-09-18T18:33:25.000000Z
字数 2261
阅读 239
JVM
1)类加载
当 JVM 遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析以及初始化。如果没有,则先执行类加载过程。
(此过程详见类加载章节)
2)内存分配
类加载检查通过后,对象所需的内存空间大小在类加载完成之后便可确定,JVM 会根据垃圾回收期选取内存分配算法:
指针碰撞法
serial,ParNew 等带有压缩功能的回收器,内存是连续的,内存指针移动基于对象大小移动。
空闲列表法
CMS,通过维护一个空间内存列表,存放对象、分配内存还需考虑并发申请内存问题(CAS、TLAB本地线程缓冲)。
堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错。虚拟机必须维护一个列表,记录那些内存块是可用的,在分配的时候从列表中找到一块足够大的内存划分给对象实例,并更新列表上的记录。
3)初始化
内存分配完成之后,则需要初始化对象的信息,主要涉及属性默认值、对象头信息以及执行构造函数。
JVM 就需要将分配的内存空间都初始化为零(不包括对象头),如果在 TLAB 上分配内存,此过程可提前至 TLAB 分配时进行。这一步保证了对象的实例字段可以不赋初值也可以直接使用。
设置对象头信息,这些信息包括该对象是那个类的实例,如何才能找到该类的元数据信息,对象的哈希码,对象的 GC 分代信息等。
执行完以上步骤之后,对于 JVM 来说新的对象已经创建完成,但对于 Java 程序来说,对象创建才刚开始,因为构造函数还没有执行,所以要执行方法进行自定义初始化。
由于 JVM 中创建对象的行为非常频繁,因此需要考虑内存分配的并发问题解决方案:
1)对分配内存空间的动作进行同步,即用CAS失败重试的方式;
2)把内存分配的动作按照线程划分在不同的空间中进行,每个线程在 Java 堆中预先分配一小块内存,即本地线程分配缓冲 TLAB(Thread Local Allocation Buffer),各线程首先在 TLAB 上分配内存,TLAB 使用完之后,分配新的 TLAB 时才需要同步锁定。JVM 是否使用 TLAB 可以通过 -XX:+/-UseTLAB 参数指定。
Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。由于 reference 类型在 JVM 规范中只规定了一个指向对象的引用,并未定义这个引用如何定位和访问具体位置,所以对象访问方式由具体虚拟机实现而定。目前主流的访问方式有句柄和直接指针两种。
二者各有优势,使用句柄访问这样做的好处是栈中 reference 存储的句柄地址较为稳定,因为在 Java 堆中进行了垃圾回收,对象的地址发生了改变的时候,只需要修改句柄的对象实例数据指针就行。而使用直接指针的最大好处就是速度更快。
对象在堆内存的内存布局主要有三部分,即对象头、实例数据以及对其填充。
1)对象头(Header)
2)实例数据(Instance Data)
3)对齐填充
1、内存溢出(Out Of Memory)
是程序在申请内存时,没有足够的内存空间供其使用。比如:你需要10M的空间,内存空间只剩8M,这就会出现内存溢出。
以栈举例:栈满时在做进栈必定产生空间溢出,叫上溢,栈空时在做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。
2、内存泄露(Memory Leak)
是程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重。memory leak最终会导致out of memory。
这块内存不释放,就不能再用了,就叫这块内存泄漏了。