序言
对象内存布局
java对象在内存中包含三部分:对象头、实例数据、对齐填充。
小端存储
便于数据之间的类型转换,高位地址部分的数据直接截掉。
大端存储
最低地址数据位符号位,便于数据类型的符号判断,直接判断数据的正负号。
java使用的是大端存储
内存模型设计之——Class Pointer
句柄池访问
使用句柄池访问对象,会在堆中开辟一块内存作为句柄池,句柄中存储了对象实例数据(属性值结构体)的内存地址,访问类型数据的内存地址(类信息、方法信息),对象实例数据一般也在堆中开辟,类型数据一般存储在方法区。
优点
reference存储的是稳定的句柄地址,在对象被移动(垃圾收集是移动对象是非常普遍的行为)只会改变句柄中的实例数据指针,而reference本身不需要改变。
缺点
增加了一次指针定位的时间开销。
直接指针访问
直接指针访问方式指reference中直接存储对象在heap中的内存地址,但对应的类型数据访问地址需要在实例中存储。
优点
节省了一次指针定位的开销。
缺点
在对象被移动时(如进行GC后的内存重新排列),reference本省需要被修改。
内存模型设计之——指针压缩
指针压缩的目的
- 为了保证CPU普通对象指针(oop)缓存
- 为了减少GC的发生,应为指针不压缩时8字节,这样在64位操作系统的堆上其他资源空间就减少了。
计算机操作系统分为了32位和64位,这个位指的是cpu在内存中寻址能力。
64位操作系统中,内存>4G默认开启指针压缩技术,内存4G<4G,默认32位操作系统默认不开启。内存操作32G指针压缩失效,所以通常在部署服务时,JVM内存不要超过32G内存,因为超过32G就无法开启指针压缩了。
内存>32G指针压缩失效,因为指针压缩到了4byte,也就是32bit,用排列组合的方式可以识别2^32个对象,也就是4G对象。非简单对象都是以8byte对齐的。因此,能够识别的最大内存就是4G*8byte=32G;
关于指针压缩的参数
-XX:+UseCompressedClassPointers,压缩类指针,每一个对象都有一个类型数据指针,64位的java虚拟机中默认是启动压缩。
-XX:+UseCompressedOops,压缩普通对象指针,表示是否使用普通对象指针压缩,Oops是指Ordinary Object Pointers的缩写,就是任何指向一个在堆中的对象的指针,默认也是启动压缩。
引入maven依赖,用于打印对象内存布局情况
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.13</version>
</dependency>
自定义一个对象,并打印该对象的内存布局
如下图是默认使用类指针压缩和普通对象指针压缩的情况下,对象的内存占用情况
关闭类指针压缩后,对象的内存占用情况
对象头已经有三部分变成四部分,其中类指针由原来的4byte变成8byte
关闭普通对象指针压缩,对象的内存占用情况
String由原来的4byte变成了8byte
Java内存模型
运行时数据区中存储对象的区域重点要关注堆和方法区(非堆),所以内存的设计着重从这两方面展开(这两个区域都是线程共享),而虚拟机栈、本地方法栈、程序计数器都是线程私有。
JVM运行时数据区时一种规范,而JVM内存模型是对该规范的实现。
对象在内存中的分配过程
一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大对象会直接被分配到Old区。
Gc的悲观策略:在某些情况下有些对象未达到分代年龄,直接进入老年代。
相同年龄的所有对象大小总和小于S区其中一个区域的一半,年龄大于或等于这个年龄的对象,可以直接进入老年代。
触发Full GC的时机
- 之前每次晋升的对象平均大小>老年代的剩余空间,基于历史平均水平。
- youngGC之后,存活对象超过了老年代的剩余空间,基于下一次可能的剩余空间。
- MetaSpace区域空间不足。
- 显式使用System.gc()。
常见问题
Full GC 包含的区域
Full GC=young GC + Old GC + Metaspace GC
新生代的划分比例
新生代中开用内存,复制算法用来担保的内存为9:1,可用内存中Eden:S1区为8:1,即新生代中Eden:S1:S2=8:1:1,现代商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代的对象98%都是朝生夕死的,即在一次young GC之后就会被垃圾收集器回收。
Surivior区存在的意义
如果没有Survivor区,每次进行一次Minor GC,存活的对象就会被送到老年代,导致老年代很快被填满,触发Major GC(也可以看作式Full GC)。由于老年代的内存空间较大,执行一次Full GC的时间比Minor GC长的多,这会影响大型程序的执行和响应速度。
那么如果增加老年代空间呢?这样可以降低Full GC的频率,但是一旦发生Full GC,执行时间就会更长。如果减少老年代空间呢?如此可以减少Full GC所需的时间,但是老年代很多就会被填满,触发Full GC的频率增加。
因此,Survivor区的存在意义在于减少被送到老年代的对象,进而减少Full GC的发生。Survivor区通过预筛选,只有经过一定次数的Minor GC后仍在新生代存活的对象才能被送到老年代,这样可以有效的降低Full GC发生的频率。
Survivor区划分为两块的意义
Survivor区被划分为两块可以解决内存碎片化问题。
假设只有一个Surivivor区,当新建的对象在Eden区时,一旦Eden区被填满,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区。这样循环下去,Eden区和Surivivor区中都会有一些存活的对象,如果把Eden区的对象直接放到Surivivor区,这两个区域的内存都是不连续的,这样就会导致内存碎片化严重。
而如果划分了两个Surivivor区,一个Surivivor区永远都是空的,另外一个Surivivor区是非空的,并且不会同时发生对象移动,这样就保证了Surivivor区不会出现内存碎片化情况。
堆内存中的线程私有区域——TLAB
JVM默认为每个线程Eden区开辟一个buffer区域,用来加速对象的分配,称之为TLAB(Thread Local Allocation Buffer)。对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
visualvm的使用
某些版本jdk会自带visualvm,在bin目录下就可以找到,在配置好jdk环境变量后,可以在cmd窗口输入jvisualvm来启动。
针对于一些没有visualvm的jdk,可以通过官网下载VisualVM: Downloadhttps://visualvm.github.io/download.html
我们可以通过此工具来堆java程序的内存进行检测
指定最大堆内存和初始化堆内存:-Xmx20M -Xms20M
@RestController
public class JvmController {
private static List<Object> list = new ArrayList<>();
@GetMapping("/oom")
public void oom(){
for (;;) {
list.add(new Object());
}
}
}
如下是java程序内存的使用情况(Visual GC是插件,需要自行下载)
老年代和Eden区已经被填满
栈可以容纳多少栈帧
public static int count;
public static void stack(int i){
System.out.println(count++);
stack(i);
}
public static void main(String[] args) {
stack(1);
}
默认栈的大小是1M,可以发现栈中大约可以容纳9k左右的栈帧。