JVM 进阶知识
Java的跨平台性
Java具有跨平台性,无疑是JVM底层翻译出来的汇编指令的不同,Unix和Window系统的汇编指令是不同的,Windows派系采用的是Intel汇编,Unix派系采用的是AT&T汇编。无论在哪个平台上编写的Java文件,编译后的class文件,放在哪个平台上都可以执行,只要下载平台相对应的JDK,都可以执行这个class文件,从而翻译出跟平台相对应的汇编指令
class文件、class content、class对象、对象四个基础概念
class文件: 这个应该不用多说,就是java文件编译后的class文件
class content: 首先来看一张类加载的图
我们的class文件是存在在硬盘上的,需要将文件从硬盘读取到内存中,通过类加载将class文件内容加载到内存中,则会在类加载子系统中形成一块class content表示class 文件内容
class对象:用过反射的网友应该清楚Class<T>这个对象,这就是class对象,类加载器通过解析class content内容则会在方法区(永久代)中形成Class<T>对象,像这个class对象的字段,方法的描述信息都会放在方法区中
对象:对象则就是指我们new出来生成的对象
JDK1.8后元空间替代永久代
1. 元空间使用的是直接内存,操作系统的内存。永久代会产生OOM
2. 硬件的发展,之前的计算机硬件比较落后,没多少内存使用。如果还让JVM使用直接内存的话,会导致没有多少内存供其它程序使用。随着硬件的发展,现在的机器能够达到32G 64G这样的,有足够的内存使用。32位的机器最大的内存就是4G,64位的机器最大的内存可以达到2的48次方,256T。64bit = 16(保留位) + 48
元空间默认大小
最小20.75M,最大256T,相当于无限大。元空间调优技巧:最小最大设置成一样大,防止忽大忽小,动态扩展。一般设置成物理内存的1 / 32。
虚拟机栈
局部变量表:存放方法中声明的局部变量
操作数栈:变量需要进行运算所要用到的操作数栈
动态链接:指向方法在方法区的内存地址,也就是指向下图中的main方法对象或者add方法对象的内存地址
返回地址:恢复现场。如果main方法调用了add方法,add方法执行完后,需要返回到main方法前面执行到的指令,则add方法中的返回地址则记录了main方法中程序计数器所执行到字节码指令
一个方法执行完JVM需要做的事情
1. 恢复局部变量表指针
2. 恢复操作数栈的指针
3. 恢复程序计数器
4. 如果方法有返回地址,则需要返回
5. 清理栈帧(程序计数器做的)
new操作不是原子操作
右边的图中我们可以看到new Test()所生成的字节码指令,并不是只有一条指令,而是由四条指令组成的,所以它不是一个原子操作。
0 new
3 dup
4 invokespecial #调用构造函数,初始化
7 astore_1
这就是为什么在单例模式的双检查锁DCL中,为什么要加volatile关键字了,防止这四个指令的重排序问题
每个非静态方法,局部变量表index=0的位置永远存放的都是this指针,有点类似于python里面每个方法的第一个参数是self的意思,this指针在执行dup指令时完成赋值,调用构造init方法需要用到
堆的默认大小
默认大小是物理内存的1 / 64,最大是1 / 4
虚拟机栈 -> 方法区的联系
动态链接
堆 -> 方法区
klass Pointer类型指针:对象执行类的Class对象的内存地址
方法区 -> 堆
静态变量:例如public static Test test = new Test(), 静态属性在方法区,对象在堆中
指针压缩
未开启指针压缩:内存地址占8字节。开启指针压缩:内存地址占4位
如何计算一个对象的大小
要计算一个对象的大小,首先需要了解一下对象在JVM中的一个内存布局
Mark Word:
32位机:4个字节
64位机:8个字节
指针压缩:
开启:4个字节
未开启:8个字节
对齐填充:按8个字节对齐填充
首先看一个简单的对象,计算它的对象大小(引入jol-core包,可以查看对象大小)
开启指针压缩的情况下,像这种没有普通属性的对象被称为空对象,来看看它的对象大小16B是怎么算出来的
首先是Mark Word = 8B,因为是64位的机器上
其次是类型指针,默认开启指针压缩,则类型指针 = 4B
数组长度 = 0B
实例数据 = 0B
对齐填充:8B + 4B不是8B对齐,需要填充4B,对齐填充= 4B
最后,Test对象大小 = 8B + 4B + 0B + 0B + 4B = 16B
关闭指针压缩的情况下:Test对象大小还是16B = 8B + 8B + 0B + 0B + 0B
来看一个具有普通属性的对象
我们可以看到该Test类开启了指针压缩,具有一个普通属性a,short占2B,最后得出来的对象大小为16B = 8B(Mark Word) + 4B(类型指针) + 0B(数组长度) + 2B(实例数据) + 2B(对齐填充)
关闭指针压缩
发现该Test对象大小为24B = 8B + 8B + 0B + 2B + 6B
来看一下数组对象所占的内存大小
我们可以看到开启指针压缩的情况下,arr数组对象占32B
我们可以看到arr数组对象关闭指针压缩的情况下,对象大小占40B,并且数组对象会有2个padding(对齐填充).
什么情况下对象的内存布局会产生2个padding
对象是数组对象并且关闭指针压缩的情况下,会产生2个padding
开启指针压缩的优势
1. 节省JVM空间
2. 提升JVM运行效率
JVM怎么做到在指针压缩的情况下还能运行
假设有三个对象test1 = 16B、test2 = 32B、test3 = 24B。
在内存中的地址分别为:test1 = 0x00000、test2 = 0x10000、test3 = 0x30000
因为JVM采用8字节对齐1000,后面三位一定是000。所以可以将test1, test2, test3后面三位抹掉,那么在储存时就会变为test1= 0x00、test2 = 0x10、 test3 = 0x30,而在使用时,则将地址后面的三位000补充回来
开启指针压缩的情况下,一个oop(对象指针)能表示最大堆空间是多少
我们知道开启指针压缩的情况下,类型指针占4字节,也就是32位,上一小节说到JVM储存时会抹掉后面的3位,也就是可以保存32 + 3 = 35位,最大内存空间也就是2的35次方,32G
oop(对象指针)如何扩容
前面说到JVM采用8字节对齐,会抹掉后面的3位,如果我们让它采用16字节对齐,那么是不是可以抹掉最后面的4位,oop(对象指针)所能表示的堆空间则为2的32 + 4次方
JVM为什么不是16字节对齐
如果采用16字节对齐的话,在使用时对齐填充浪费内存空间
虚拟机栈默认大小
可以通过命令java -XX:+PrintFlagsFinal -version | grep ThreadStack栈大小为1024KB
虚拟机栈最小为160KB
JVM对虚拟机栈做了最小的限制,限制为160KB
我们可以看到通过-Xss参数设置虚拟机栈大小为100KB,发现控制台报错,最小为160KB