Java对象内存模型

序言

对象内存布局

java对象在内存中包含三部分:对象头、实例数据、对齐填充。

小端存储

便于数据之间的类型转换,高位地址部分的数据直接截掉。

大端存储

最低地址数据位符号位,便于数据类型的符号判断,直接判断数据的正负号。

java使用的是大端存储

 

内存模型设计之——Class Pointer

句柄池访问

使用句柄池访问对象,会在堆中开辟一块内存作为句柄池,句柄中存储了对象实例数据(属性值结构体)的内存地址,访问类型数据的内存地址(类信息、方法信息),对象实例数据一般也在堆中开辟,类型数据一般存储在方法区。

优点

reference存储的是稳定的句柄地址,在对象被移动(垃圾收集是移动对象是非常普遍的行为)只会改变句柄中的实例数据指针,而reference本身不需要改变。

缺点

增加了一次指针定位的时间开销。

直接指针访问

直接指针访问方式指reference中直接存储对象在heap中的内存地址,但对应的类型数据访问地址需要在实例中存储。

优点

节省了一次指针定位的开销。

缺点

在对象被移动时(如进行GC后的内存重新排列),reference本省需要被修改。

内存模型设计之——指针压缩

指针压缩的目的

  1. 为了保证CPU普通对象指针(oop)缓存
  2. 为了减少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的时机

  1. 之前每次晋升的对象平均大小>老年代的剩余空间,基于历史平均水平。
  2. youngGC之后,存活对象超过了老年代的剩余空间,基于下一次可能的剩余空间。
  3. MetaSpace区域空间不足。
  4. 显式使用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左右的栈帧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值