JVM虚拟机
拓展:类加载子系统的作用
PS:类加载子系统的作用如下
1、类加载子系统将磁盘中的.class文件加载到直接内存中,维护在class content对象中
2、类加载子系统根据.class文件规范将class content解析成instanceKlass对象放在方法区中
3、根据instanceKlass对象生成一个instanceKlassMirror对象(可以理解成Class对象)放在堆中
拓展:方法区存储的内容
PS:方法区存储的主要是.class文件的一些描述信息,主要是
1、运行时常量池
2、类信息
3、方法信息
4、字段信息
5、类加载器引用
6、Class实例引用(指向堆中的Class对象)
拓展:元空间使用直接内存的原因
1、GC的问题:jdk7中,方法区的实现是永久代,存放在堆区中,这就意味着堆中存放着类的元信息与对象,
一般类的元信息是不卸载的,这就导致GC算法要不断判断“几乎不回收”的类信息,影响GC的性能。
2、应用的问题:应用越来越大,即加载的类越来越多,所以容易OOM,因此jdk8的方法区实现是元空间,
采用的是直接内存。
拓展:内存溢出的原因
1、使用完不释放:这种情况就是由内存泄漏导致的OOM
2、GC效率低于内存使用效率:当并发量较大的时候,通常来说系统需要创建更多的对象处理请求,这就需要
不断申请内存,当申请内存速度大于GC回收内存的速度时,就容易导致OOM。
拓展:元空间大小的设置
1、一般来说64bits的机器最大可以设置的内存不是2^64,而是2^32,所以元空间最大可以设置为2^32
2、jvm规定元空间最小的内存大小为20.75MB
3、设置的规则
(1)最大值、最小值一样
(2)值= 机器内存/32(阿里的技术分享)
4、额外的规则(当应用程序的内存占用超过1/32,可以采用以下的工具设置大小)
(1)使用arthas(命令行窗口)或者 visualVM(图形化)读取元空间的大小
(2)预留20%~30%的空间(如程序跑起来元空间内存占用了100MB,需要设置130MB)
拓展:栈帧的结构
PS:主要的作用如下
1、局部变量表:存储方法执行过程中的局部变量
2、操作数栈:存储字节码执行执行过程中的操作数
3、动态链接:存储方法区中方法对象Method的内存地址
4、返回地址:用来恢复现场,用在方法调用其他方法的场景。
拓展:什么对象会进入老年代
1、分代年龄达到15的对象(因为对象头中的Markword中分代年龄只有4bit)
2、大小超过eden区一半的对象会直接进入到老年代
3、当s0、s1无法存放的对象会直接放入老年代(担保机制)
拓展:对象的内存布局
1、对象在内存中分为三大部分:对象头、实例数据、对齐填充
2、对象头又分为3个部分:Markword、类型指针、数组长度
PS:对齐填充是指,jvm要求对象按照8字节的整数倍存储,如一个对象只有14字节,需要额外填充2个字节
拓展:指针压缩
产生背景
1、32位的机器所支持的最大内存范围是4G;对于Java进程,在oop只有32位时,只能引用4G内存。
PS:注:oop(ordinary object pointer),即普通对象指针,是JVM中用于代表引用对象的句柄。
2、为了使得java进程支持更大的堆空间,即需要使用64位的jvm,这样oop为64位,引用的堆空间变大。
产生问题
将jvm升级为64位,意味着oop也需要变为64位,这样原本引用一个对象的开销从32位上升到了64位,
这样会挤压堆中对象的空间(因为oop的存储成本升高),导致GC频率上升。
解决思路
为了解决这样的问题,可以考虑对oop进行一个压缩,因此java中提出了“指针压缩”的解决方案,将
oop压缩为32位。
PS:开启、关闭指针压缩的方式
总结:指针压缩就是压缩java中的“对象引用/指针”,jdk7和jdk8中默认开启
拓展:空对象的大小
空对象的概念
空对象是指,没有普通属性的对象,因为statci属性、方法都在方法区中,只有实例属性才会在堆中,那么空对象
在堆中的大小是多少呢?
空对象的大小计算(分为两种情况)
PS:解释如下(64位的jvm)
对象的大小需要了解对象的内存布局,对象头(Mark word、类型指针、数组长度)、实例数据、对齐填充
1、开启指针压缩:对象头(8B + 4B + 0B)+ 实例数据0B + 对齐填充4B = 16B
2、关闭指针压缩:对象头(8B + 8B + 0B)+ 实例数据0B + 对齐填充0B = 16B
PS:综上,空对象在内存中的大小为16B
拓展:非空对象的大小
PS:对象的大小计算如下所示
1、开启指针压缩:对象头(8B + 4B + 0B)+ 实例数据8B + 对齐填充0B = 20B
2、关闭指针压缩:对象头(8B + 8B + 0B)+ 实例数据8B + 对齐填充0B = 24B
拓展:指针压缩案例
1、开启指针压缩:对象头(mark word8B + 指针压缩4B + 数组12B )+ 实例数据8B + 对齐填充0B = 32B
2、关闭指针压缩:对象头(8B + 8B + 12B + 对齐4B)+ 实例数据8B + 对齐填充0B = 40B
PS:当对象中存在数组,且关闭了指针压缩的时候,对象的内存分布就会出现两次padding对齐填充
PS:可以看到,开启指针压缩,节省了8个B的内存空间
拓展:指针压缩的原理
问题:为什么开启指针压缩之后,类型指针能够从8个字节压缩到4个字节,既然压缩到4个字节了, 那当要访问该类型的时候,又该怎么访问呢?
解释如下:假设现在使用的jvm是32位,有3个对象test1、test2、test3,3个对象的大小分别是16B、24B、48B,
那么这三个对象对应的内存地址分别是是
16B:0x 0000 0000
24B:0x 0001 0000
48B:0x 0001 1000
由于java中对象内存地址都是8B的整数倍,因此地址的最后面3位必然为0,指针压缩就是利用这一点。在存储对的时候,去掉3个0,当需要访问该类型指针的时候,再添加回来3个0,达到节省内存的目的;
拓展:指针压缩下,oop能表示的最大堆空间
我们知道,通过指针压缩,可以将oop末尾的3个0消除,当使用的使用再进行添加;同时,在开启指针压缩的情况下,对象头的类型指针占用4个字节,也就是32位,因此一个oop可以表示的最大堆空间为2^(32+3)次方
拓展:如何使得oop表示更大堆空间(扩容)
可以使用16字节对齐,这样在对象头中存储类型指针的时候就可以省略4个0,在使用的使用再添加进去。
PS:该情况只有当应用程序的大小大于 2^(32 + 5)的时候,才需要考虑扩容
拓展:为什么jvm使用8字节填充
字节填充是实现指针压缩的前提,通过填充少部分字节,使得指针压缩算法能够工作,来节省更多的空间。
事实上,16字节填充带来内存浪费弊端X,要大于基于16字节填充的指针压缩算法带来的节省内存的收益Y,
X是负数,代表浪费多少内存,Y是正数,代表节约多少内存,只有当X + Y >0才有意义,而使用16字节填充
会使得X + Y < 0,且经过oracle测试,采用8字节填充是比较合理的。
拓展:虚拟栈的默认深度
PS:默认的栈的深度为1024
PS:不同方法,圧入栈的栈帧的大小不同,也就导致了栈的深度不同
PS:可以通过以上参数设置栈的大小,栈的深度和栈的大小有关
拓展:内存的几种说法
PS:JMM内存模型 与 jvm内存模型的对应关系如下
1、主内存 = 堆 + 方法区
2、工作内存 = 虚拟机栈
另外:JVM内存模型和JAVA内存模型都属于 操作系统内存,只不过是操作系统内存按照某种规则划分的一部分。
拓展:volatile保证线程可见性
PS:如果不加volatile会处于死循环,加了之后会正常执行,涉及到JMM的原理
PS:以上的JMM的基本原理图
拓展:volatile保证执行有序性(禁止指令重排)
new对象的过程
instance = new DCLTest()这一行java代码的字节码指令实际上有4条,主要的功能是
1、new:在堆中创建空对象,然后将该空对象的引用圧入虚拟机栈
2、dup:复制栈顶数值,并将复制值圧入栈顶
3、invokespecial:执行DCLTest类的默认构造方法
4、putstatic:执行静态代码块
5、以上的4个步骤完成之后就会将初始化完成的对象引用赋值给instance
PS:并发情况下,cpu为了提高运行效率,可能会乱序执行指令可能会导致第5步在第3-4步之前执行,既返回一个对象引用,但是实际上该对象还没有初始化完成,这样当程序调用该对象的方法,就会出现空指针异常。
解决方案:可以使用volatile修饰instance,禁止指令重排
拓展:jvm如何判定共享变量是否被volatile修饰
PS:我们从字节码指令开始分析,可以发现,加不加volatile,编译生成的字节码指令都不会发生变化,那么
jvm是如何知道当前操作的是volatile修饰的共享变量呢?实际上是通过一个访问标识,Access flags来确定的
PS:当操作的是volatile修饰的共享变量,Access flags为0x49,否则为0x09