运行时数据区简介
运行时数据区简图:
在JDK8中使用元数据区(元空间)代替了方法区,它使用的是本地内存
其中本地方法栈,程序计数器,虚拟机栈是线程私有的,每个线程独一份
堆区和元数据区和JIT编译产物(代码缓存)是进程独一份的,由线程之间共享的.元数据区和JIT编译产物(代码缓存)在JVM中叫做非堆区(方法区).
每个JVM进程都对应着一个Runtime的实例,Runtime也就是运行时数据区
内存泄露与内存溢出
内存泄露指空闲空间不足,垃圾回收之后可用空间还是不足将出现OOM
内存溢出严格上指对象不被使用了但GC又无法回收的情况.宽泛而言,可以指一些生命周期过长的对象导致内存被蚕食甚至OOM.
内存泄露的举例
- 静态集合类,将局部变量添加到类变量中,导致无法回收局部变量
- 单例模式中,在单例的对象中持有其他不必要的对象时,这个外部对象就无法被GC,导致内存泄露.
- 内部类持有外部类
- 在一些io,数据库连接,socket连接使用过后不关闭也会导致无法GC而产生内存泄露
- 一个变量不合理的作用范围也很可能造成内存泄露
- 添加元素到hashSet后修改该元素的字段导致hashcode值变化的话,这个元素相当于找不到了,就会造成内存泄露
- 缓存泄露,大量的数据聚集在缓存中,导致无法GC,可以改成WeakHashMap
- 监听器与回调
- ThreadLocal,使用ThreadLocal时如果不及时对其中的数据进行回收就会导致数据和thread的生命周期一样长,虽然ThreadLocal中的Map使用的key是弱引用但value是强引用,不会在GC时回收,可能会导致内存泄露
程序计数器概论
程序计数器顾名思义就是给程序计数的,记录程序运行到哪了.
PC寄存器是软件层面的架构,是对硬件系统中的寄存器的一种模拟.
每个线程都有自己单独的pc寄存器,用于记录当前线程正在执行的方法地址(虚拟机栈中栈顶栈帧的指令地址),如果执行的是本地方法则记录为undefined
PC寄存器的内存很小,是JVM中唯一没有定义OOM的区域. ps:就存个地址不用定义.
PC寄存器的工作
PC寄存器中存储着当前线程虚拟机栈栈顶的栈帧对应的指令地址(偏移地址).执行引擎会取出pc寄存器中的地址找到对应的指令操作局部变量表和操作数栈,并将class字节码指令转换为机器指令交给CPU执行.
PC寄存器一些疑问
PC寄存器设计为私有的原因
CPU调度线程执行是时分复用的,在微观上一个单核cpu在任何一个时刻都只能执行分配给一个线程.pc寄存器设计为私有就是为了保护现场,以便CPU时间片轮转切换作业时能找到从何开始继续执行
执行native方法的时候pc寄存器的值为undefind,那它是如何在切换cpu上下文后找到运行位置的?
在jvm中关于pc寄存器的描述最后有一句,pc寄存器足够容纳一个ReturnAddress or a Native Point on the specific Platform.推测应该存了一个本地指针,只是获取值为undefind
虚拟机栈概述
首先虚拟机栈是线程私有的,由线程创建时一起创建.
虚拟机栈中存放有局部变量和部分结果在方法的调用和返回中起作用
虚拟机栈不会操作数据,只是会对栈帧进行入栈出栈操作,所以栈帧的大小可以交给堆来分配.虚拟机栈的内存地址不需要连续.
虚拟机栈的大小是可以固定的或者动态扩展的
如果一个线程要求的虚拟机栈的大小超过允许范围就会抛出StackOverflowError
如果虚拟机栈的大小可以扩展,一个线程试图要求的虚拟机栈大小已经无法扩展会抛出OutOfMemoryError
如果一个线程要求的虚拟机栈大小导致剩余的大小不足以创建一个新线程也会抛出OutOfMemoryError
虚拟机栈中操作的最小单位是栈帧
可以通过-Xss 1024k
来设置虚拟机栈的最大大小为1024kb
JVM的栈和堆
JVM的栈存的实际是操作和中间结果以及局部值(数据的地址,指令),而堆存的才是数据本身.
栈帧(Stack Frame)
栈帧是一个虚拟机栈中的最小操作单元
每个运行中的方法都对应着一个栈帧
栈帧是一个内存区块,是一个数据集,他维系着方法运行所需的所有数据信息
栈帧中一般都有五部分,
- 局部变量表(An Array ofLocal Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 返回地址(Return Address)
- 一部分附加信息
局部变量表只在当前方法有效,当前方法结束局部变量表也就销毁
局部变量表
局部变量表是一个数字数组,存储着当前方法的方法参数和局部变量,
单个局部变量表槽可以存储 boolean, byte, char, short, int,
float, 对象引用, returnAddress.
一对局部变量表槽可以存储long和double类型
局部变量表的大小在编译期就决定了,保存在方法的code属性的local variables中
示例
代码如图:
局部变量表的数据示例:
对应字节码指令:
对于其中局部变量表的数据:
- start表示局部变量表对应字节码指令的行号(PCstart)
- length表示从start开始往下多少行(不包括length)为局部变量的作用域
- slot表示数据在局部变量表中的位置
- Name表示局部变量的名称
- Signature为局部变量的类型标志
slot
slot:是局部变量的最小存储单位,一个slot可以存储32位的长度(boolean, byte, char, short, int,
float, 对象引用, returnAddress),两个slot可以存储64位长度(long,double)
如果当前方法是构造方法和实例方法(非静态方法)时,局部变量表中的第一个元素会存放this对象.
所以在静态方法中不能使用this,因为当前方法的局部变量表中没有this,如图:
slot的位置是可以复用的,如果slot中的数据超过了作用域就可能会被回收
局部变量与成员变量
成员变量:需要经过初始化赋值
- 类变量:在类加载的linking阶段中prepare时会默认初始化赋值.在类加载过程中的initial阶段会根据静态代码块有无执行clinit来显示赋值
- 实例变量:在类创建实例的过程中初始化赋值并分配到堆中
局部变量:使用前必须要显示赋值
操作数栈
每一个独立的栈帧除了包含局部变量表外,还包含一个后进先出的操作数栈,可以称之为表达式栈
在方法的执行过程中,根据字节码指令,往栈中写入数据,称为入栈(push),提取数据,称为出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈
比如:执行复制、交换、求和等操作
操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存储空间。
操作数栈就是jvm执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈就是空的
这个时候数组是有长度的,因为数组一旦创建,那么长度就已经确定下来。
每一个操作数栈都会有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就已经定义好了。保存在方法的code属性中,为maxstack的值。
栈中内容
栈中的任何一个元素都可以是任意的java数据类型
32bit的类型占用一个栈的单位深度
64bit的类型占用两个栈单位的深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
栈顶缓存
在Hotspot中会将栈顶元素缓存在cpu的寄存器中提高读取速度
字节码示例
java代码示例
public void testMain(){
int i = 10;
int sum = getSum(i);
}
public int getSum(int i) {
int j=10;
int k = i+j;
return k;
}
javap反编译之后的字节码指令
public void testMain();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: aload_0
4: iload_1
5: invokevirtual #2 // Method getSum:(I)I
8: istore_2
9: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/wei/jvm/OperandStackTest;
3 7 1 i I
9 1 2 sum I
public int getSum(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: bipush 10
2: istore_2
3: iload_1
4: iload_2
5: iadd
6: istore_3
7: iload_3
8: ireturn
LineNumberTable:
line 16: 0
line 17: 3
line 18: 7
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/wei/jvm/OperandStackTest;
0 9 1 i I
3 6 2 j I
7 2 3 k I
字节码解释:
0: bipush 10
2: istore_1
3: aload_0
4: iload_1
5: invokevirtual #2 /Method getSum:(I)I
8: istore_2
9: return
当调用testMain()时,会创建testMain的栈帧,stack=2, locals=3
表示操作数栈深度为2局部变量表大小为3.
bipush 10
会将10
压入操作数栈中,pc寄存器改为0
istore_1
将栈顶数据10
弹出存储到slot1中,pc寄存器改为2
aload_0
将局部变量表的slot0即this
取出压入操作数栈,pc寄存器改为3
iload_1
将局部变量表的slot110
取出压入操作数栈,pc寄存器改为4
invokevirtual #2
弹出操作数栈中的数据this
与10
并调用getSum方法处理数据得到结果后将结果压入栈,pc寄存器改为5
istore_2
弹出栈顶数据存入局部变量表slot2中
return
返回
在invokevirtual #2
调用getSum方法时将会创建getSum的栈帧,局部变量表大小为4,操作数栈深度为2
0: bipush 10
2: istore_2
3: iload_1
4: iload_2
5: iadd
6: istore_3
7: iload_3
8: ireturn
bipush 10
将10
压入操作数栈,pc寄存器改为0
istore_2
将栈顶10
弹出放入局部变量表slot2,pc寄存器改为2
iload_1
将局部变量表slot1即i
弹出压入栈,pc寄存器改为3
iload_2
将局部变量表slot2即j
弹出压入栈,pc寄存器改为4
iadd
将栈顶依次取出俩数j
与i
做相加得出结果压入栈,pc寄存器改为5
istore_3
将栈顶数据k
弹出放入局部变量表slot3,pc寄存器改为6
iload_3
将slot3k
取出压入栈,pc寄存器改为7
ireturn
将栈顶数据k
取出,返回,pc寄存器改为8
方法结束,栈帧销毁
动态链接
动态链接就是当前栈帧方法在常量池中的引用地址
每一个栈帧内部都包含一个指向运行时常量池中的该栈帧所属方法的引用。而包含这个引用就是为了支持当前方法的代码能够实现动态链接。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olxTBtto-1657173882597)(9065B5D8D4B649E1914C6101AAE91DBF)]
在字节码文件中,所有变量和方法的引用都作为符号引用,保存在class文件的常量池。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
常量池提供一些符号和常量,便于指令的识别。
一个字节码文件里面如果代码量比较少,但是它包含的信息加载进内存后并不少,如String,类变量,System类,println打印流等信息,通过常量池,将这些信息通通放进去,需要使用的时候通过引用的方式。节省空间,提高效率。
方法的调用
相关概念
- 静态链接:字节码装载进JVM,如果被调用的目标方法编译器可知,且运行期不变,这时会将调用方法的符号引用转换为直接引用,这个过程成为静态链接。
- 动态链接:被调用的方法不能在编译器确定,运行时才将被调方法的符号引用转换为直接引用。称之为动态链接。
- 早期绑定:调用的方法在编译器可知,且运行期保持不变,这时就可直接将方法与所属的类型绑定,称为早期绑定。
- 晚期绑定:如果被调方法编译器不能确定下来,只能根据运行期实际的类型绑定相关方法,称为晚期绑定。晚期绑定保证了java语言的多态性
- 非虚方法:编译器就确定下来,运行期不变,这种方法为非虚方法,比如final的方法.对应指令为invokestatic、invokespecial指令。静态方法、私有方法、实例构造器、父类方法,这些只需要在类加载阶段就会确定
- 虚方法:在运行期确定的方法,对应指令为invokevirtual(除开final方法)、invokeinterface和invokedynamic指令。
为了提高性能,JVM采用类的方法区建立一个虚方法表来实现,使用索引表来代替查找。
何时创建?
在类加载的链接阶段被创建并初始化。当类变量初始值完成,JVM会把该类的方法表也初始化。
invokedynamic
jdk7中就出现了invokedynamic指令,但在jdk8出现了lambda表达式之后才开始在编译成class时使用invokedynamic来实现lambda的这种动态语言特性.
方法调用总结
方法的调用指的是找到调用的方法,即通过符号引用找到直接引用.
class文件在类加载时的解析阶段会将符号引用转换为直接引用,方法调用的解析是静态的.而方法调用的分派则可能是静态或动态的.
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。也就是方法的重载
无法依赖静态类型来定位方法执行版本的分派动作。只能在运行时才能根据虚方法表知道实际的方法,也就是方法的重写
返回地址
存放调用该方法的pc寄存器的值
调用方法只有两种方式退出
- 正常处理完退出
- 出现未处理的异常,退出
这两种方式退出都需要返回到方法被调用的地方
当正常退出时,会将调用方的pc寄存器的值作为返回地址,以便执行引擎找到下一条指令.
当异常退出时,不会返回任何值给调用方,返回的位置由异常表确认
本地方法接口
本地方法就是java调用非java语言的一个接口,为了融合不同的语言为java所用
用native修饰就是一个本地方法,native可以和其他的关键字联用,除了abstract.
本地方法栈
本地方法栈是用来管理本地方法的调用
和虚拟机栈一样,容量也可以固定或者扩展,本地方法栈也会在无法扩展栈深度时抛出StackOverFlow.在请求不到更多资源进行扩展时抛出OutOfMomery
当调用一个本地方法时,就进入了一个不受虚拟机限制的世界,本地方法可以通过本地方法接口访问虚拟机内部的运行时数据区
堆
堆区是运行时数据区中最为重要的一部分内存区域之一,在jvm启动时就创建了并确定好了大小.堆是进程私有线程共享的一块区域,但其中也有线程私有的区域TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区)可以实现并发
堆的物理地址是不连续的,但逻辑上认为是连续的
几乎所有的对象实例和数组都是在堆上分配内存的,当一个对象没有发生逃逸时他的内存空间可以交由栈上分配,但hotSpot没有实现栈上分配.
堆上的对象在方法使用完之后不会立马销毁除非被GC
堆的内存细分
现代的垃圾回收器大部分都基于分代收集理论.
jdk8之前分为新生代,老年代和永久区,jdk8之后分为新生代,老年代,元空间.逻辑上是划分成三部分,实际永久区或元空间是属于方法区.比如使用-Xms 10m
来设置堆空间的大小只管新生代和老年代
新生代==年轻代
-Xms
用来设置堆空间的初始大小,-Xmx
用来设置堆空间的最大大小,其中X
表示是JVM的运行参数,ms
是MemoryStart的意思
默认的堆空间起始大小为电脑物理内存的1/64,最大空间为物理内存的1/4
一般将-Xms
和-Xmx
设置成一样的,避免空间过小造成频繁的GC,而且也避免堆空间的扩容,造成不必要的系统压力
堆区为什么要分代
大部分的对象的生命周期都很短,70-90%的对象都是朝生夕死的.如果不进行分代,GC回收的效率会很低.
年轻代与老年代
其中年轻代又分为Eden区和Survivor0区和Survivor1区
默认年轻代与老年代的大小比例为1:2,通过-NewRatio 5
可以来设置比例为1:5.
年轻代中Eden和S0,S1的大小比例为8:1:1,通过-SurvivorRatio 5
可以来设置比例为5:1:1.一般这个比例不一定是8:1:1,因为存在自适应的内存分配策略,使用-XX:-UseAdaptiveSizePolicy
关闭自适应的内存分配机制
几乎所有对象都是在Eden区new出的,80%的对象都是在年轻代被销毁的
对象分配过程
- 对象分配初始在Eden区
- Eden区满会触发YGC/MinorGC,MinorGC会判断Eden区中的对象是否还在使用,如果在使用就把Eden区中的对象移到S0,并设置年龄为1
- Eden区满又触发MinorGC时,判断S0区的对象是否还在使用,就和Eden中的对象一起拿到S1,年龄加一.
- 当S0或S1中的对象年龄达到阈值(默认为15)时会晋升到老年代.
通过-XX:MaxTenuringThreshold=N
来设置阈值
MinorGC,MajorGC,FullGC
GC都会触发STW暂停其他用户的线程,垃圾回收结束后在恢复运行
Hotspot虚拟机中GC并不是对三个区域(新生代,老年代,永久区/元空间)都进行回收的,有部分回收和整堆回收两种
部分回收:
- MinorGC/YGC 主要针对新生代的三个区域
- MajorGC/OGC 主要针对老年代的回收
- 一般MajorGC和FullGC是混淆的,区别就在于是对老年代回收还是整堆回收
- MixedGC 收集整个新生代和部分老年代
- 基于region分区设计的GC会有这种行为,例如G1 GC
整堆回收:
- FullGC 收集整个堆空间和方法区
MinorGC:Eden区满时会触发MinorGC,在MinorGC时会回收S0和S1的空间,如果S0和S1不能存,就将S0或S1的对象移到老年代.S0/S1区满不会触发MinorGC
MajorGC:发生在老年代的GC,除了一些并行的垃圾回收器(parallel Scanvenge),一般都会先触发一次MinorGC空间不足再进行MajorGC.MajorGC的速度比Minor慢10倍以上
FullGC:老年代空间不足和方法区空间不足时会触发FullGC,或者调用System.gc()也会建议触发Full GC.
内存分配策略
- 优先分配到Eden区
- 大对象直接分配到老年代
- 长期存活的对象分配到老年代
- 如果survivor区中年龄相同的对象占了超过总大小的一半,把这些大于等于该年龄的对象放到老年区.
- 空间分配担保,survivor区MinorGC后无法存放下的放到老年代
空间分配担保:
-XX:HandlePromotionFailure true
可以设置是否设置空间分配担保.
在发生MinorGC之前,JVM会检查老年代的最大可用连续空间是否大于新生代中所有对象的空间.
- 如果大于,可以进行MinorGC
- 如果小于就看HandlePromotionFailure的值
- true:判断老年代的可用空间是否大于历届晋升到老年代的对象的平均大小
- 大于平均大小,进行MinorGC.
- 小于平均大小,进行FullGC
- false:直接进行FullGC
- true:判断老年代的可用空间是否大于历届晋升到老年代的对象的平均大小
在jdk7开始这个参数就不再使用了,默认的GC逻辑就是为true时的逻辑.
TLAB
实际在堆中有一小部分的空间是线程私有的,这一部分就是TLAB,TLAB的空间位于Eden中,默认仅占Eden的1%.为了提高JVM的吞吐和避免多线程安全问题.
TLAB是内存分配的首选,一般对象都会先尝试在TLAB的空间中分配,如果分配不下了再考虑Eden.对于Eden中的对象,只能通过加锁的机制来确保多线程的安全问题.
堆空间参数的设置
-XX:+PrintFlagsInitial
:查看所有参数的系统默认值
-XX:+PrintFlagsFinal
:查看所有参数的实际值
查看某个值的设置可以使用jps
查看pid之后再使用jinfo -flag 键值 pid
查看实际值
-Xms 100M
初始堆空间大小(默认物理内存的1/64)
-Xmx 100M
堆空间最大大小(默认物理内存的1/4)
-Xmn 100M
新生代的大小
-XX:NewRatio
新生代与老年代大小占比
-XX:SurviverRatio
新生代中Eden与S0/S1大小占比
-XX:MaxTenuringThreshold
新生代晋升老年代的年龄阈值
-XX:+PrintGCDetails
输出详细的GC处理结果
-XX:+PrintGC
输出简要的GC处理结果
-XX:HandlePromotionFailure
是否设置空间分配担保
堆外分配
随着JIT编译和逃逸分析技术的发展,堆上分配对象就不再是唯一的选择,如果没有发生逃逸,可能就直接就栈上分配了随着栈帧的出栈也就一并回收了。淘宝的JVM产品TAOBAOVM甚至有堆外分配的技术,将一些对象分配到堆外,不受GC管理提高GC的效率
逃逸分析
一个对象的作用范围位于一个方法中,而不会作为变量传递出去的话就认为没有发生逃逸。
JVM的代码优化
栈上分配:没有发生逃逸将对象直接分配到栈上。HotSpot并没有实现栈上分配
同步省略:对一个没有发生逃逸的对象加锁的操作将会被省略.
public void test(){
Item item = new Item();
synchronized(item){
item.toString();
}
}
将被优化为:
public void test(){
Item item = new Item();
item.toString();
}
标量替换:对于一个没有发生逃逸的对象可以考虑将其分解成若干个局部的标量(最小单位,即基本数据类型)并使用这些标量替换对象.
public void test(){
Item item = new Item(123,"手机");
System.out,print("item.id="+item.getId()+",item.name="+item.getName());
}
将被优化为:
public void test(){
int id = 123;
String name = "手机";
System.out,print("item.id="+id+",item.name="+手机);
}
如何避免和解决OOM
OOM在堆空间和方法区都会发生,而一般出现的OOM都是堆空间的.
一般都把发生OOM时的快照(dump文件)拿出分析是因为产生了内存泄露还是内存溢出
- 如果是内存泄露可以通过分析dump文件得到泄露对象与GC roots的引用链.掌握内存泄露对象是怎样和GC Roots关联导致无法GC以及对象的信息之后就可以确定产生泄露的代码
- 如果是内存溢出,则考虑增加堆空间,检查代码中的有较长生命周期的对象,考虑优化
方法区
在JVM规范中规定方法区在逻辑上是属于堆中的一部分,但在HotSpot中将方法区和堆空间独立区别开,甚至叫方法区为非堆
方法区和堆一样也是线程共享的区域,在JVM启动的时候就创建好了方法区,方法区的大小和堆一样可以固定或者可扩展,在定义过多类而无法存放时也会导致OOM.
对HotSpot虚拟机中,JDK8之前方法区的实现为永久代,JDK8之前方法区的实现为元空间
大小设置
jdk8以后:
-XX:MetaspaceSize=100m
设置元空间起始容量,windows默认为21m,一般要设置得比较高以避免FullGC
-XX:MaxMetaspaceSize=100m
设置元空间最大容量,windows默认为-1,即没有限制
jdk8以前:
-XX:PermSize=100m
设置永久代起始容量,windows默认为21m
-XX:MaxPermSize=100m
设置永久代最大容量,windows默认为-1,即没有限制
内部结构
方法区存放的是被jvm加载的类型信息,常量,静态变量,JIT编译后的代码缓存
- 类型信息主要是class文件中的类信息,方法信息,域信息,以及操作数栈和局部变量表大小,异常表,行号记录表等等,除此之外还包括了class文件中没有的类加载器信息.
- 常量,即运行时常量池
- 静态变量,static修饰的变量信息,无需对象实例也可以调用
- JIT编译后的代码缓存
运行时常量池
class文件中除了一些类的基本信息之外,还包括一个重要的部分那就是常量池表,其中包括了字面量,以及对类型、域、方法的符号引用.
class文件被类加载器加载到运行时数据区中时,就会整合到方法区中的运行时常量池里
HotSpot中方法区的演进
版本 | 变化 |
---|---|
jdk1.6及以前 | 将类型信息,运行时常量池,静态变量,JIT代码缓存都放到永久代中 |
jdk1.7 | 将运行时常量池中的字面量(String Table)和静态变量移到堆区,其他的还在永久代 |
jdk1.8及之后 | 将运行时常量池中的字面量和静态变量放在堆区,其他放到本地内存元空间 |
用元空间替换永久代的原因:
- 永久代的调优困难
- 永久代的大小不好确定
StringTable移到堆区的原因:
- StringTable在永久代时,只会因为FullGC才会被回收,而FullGC只在老年代不足或者永久代不足时才会被触发,这就导致了StringTable回收的效率很低.
- 移到堆区GC的机会会更多,效率也更高
方法区的GC
方法区的GC主要是回收不再使用的类型和废弃的常量
方法区的GC判断不再使用的类型的条件非常苛刻,要同时满足多个条件才可以被允许回收
- 类的所有实例都被回收,且子类实例也被回收
- 加载该类的类加载器已被回收
- 该类对应的java.lang.Class对象没有被引用
创建对象的方式
- new
- Class.newInstance() 只能通过反射调用无参构造器
- constructor.newInstance(XXX) 可以通过反射调用无参或有参构造器
- clone()使用拷贝复制一个对象
- 反序列化,将序列化的对象还原
- 使用三方的类库生成比如Objenesis
对象创建的过程
1.判断对象所属的类是否加载、链接、初始化,加载类元信息
首先会去找元空间中的常量池找这个类对应的符号引用,并且检查这个符号引用代表的类是否已经加载链接初始化即有无类元信息.如果没有,在使用双亲委派的模式下,当前类加载器使用ClassLoader+包名+类名为key查找是否有对应的class文件,没有文件则出现ClassNotFoundException.如果找到就进行类的加载,并生成Class对象
2.为对象分配内存
计算需要分配的内存,并在堆空间中划出空间给对象
- 如果内存规整(使用具有标记压缩算法解决碎片化的GC),则使用指针碰撞来分配.移动可用空间和已用空间分界点的指针空出相应大小.
- 如果内存不规整,使用空闲列表分配,从虚拟机维护的内存列表中找出一个合适的可用空间块,分配给对象并更新内存表
3.处理并发安全问题
采用CAS失败重试,区域加锁保证更新的原子性.
优先将对象分配到线程私有的TLAB区域,避免并发问题
4.默认初始化值
为对象中的属性默认初始化赋值
5.设置对象头
JVM设置对象头中的所属类信息,锁信息等
6.执行进行初始化
调用方法对类的属性显式赋值、代码块中的初始化、构造器中初始化.并将对象的首地址赋值给引用变量
对象的内存布局
对象头 | 实例数据 | 对齐填充 |
---|
对象头:
运行时元数据 | 类型指针 | (数组长度) |
---|
运行时元数据主要包括
- hashCode也就是局部变量表中对象引用指向的地址
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程Id
- 偏向时间戳
类型指针指向元空间中所属的类元数据InstanceKlass
如果是数组还要记录数组的长度
实例数据
将相同宽度的数据放到一起
将父类的变量放到前子类在后
对齐填充
仅做占位
内存布局示例
public class Account{
}
public class Customer {
private int id=1001;
private String name;
private Account acct;
{
name = "匿名客户"
}
public Customer(){
acct = new Account();
}
}
public class TestMain{
public static void main(String[] args){
Customer cust = new Customer();
}
}
内存布局图:
对象访问定位
通过使用栈帧中的局部变量表的引用值直接指针访问到对象