JVM之对象内存详解

对象内存分配

JVM中对象创建流程

User user = new User();

1. new指令
2. 常量池检查:检查这个指令是否能在常量池中定位到一个类的符号引用
3. 此时判断是否加载过该类User如果没有或者检查不通过则触发类的加载
4. 分配内存空间 1.指针碰撞 2.分配器加锁(空闲列表)
指针碰撞 分配器加锁(空闲列表)接下来详讲
6. 内存空间初始化为零值
7. 必要信息设置(不是set方法而是JVM内部需要做的 比如该类是否有资格进入老年代 对象头相关的信息)
8. init方法调用

在这里插入图片描述

对象内存分配方式

内存分配方式和垃圾收集器息息相关!

指针碰撞

在这里插入图片描述
图3
当有新的对象需要分配内存时(新生代),分界指示器会移动以保证内存的连续性,由于新生代的对象的大小在JVM分配内存空间时就是固定的因此有助于内存分配的优化。同时存在一个问题?此时会有线程安全问题吗? 如果两个线程都需要这个指针移动 JVM是怎么解决这个问题的呢?

  1. 内存地址是连续的
  2. 适用于新生代
  3. 当有新的对象需要分配内存的时候只需要将指针(分界指示器)转向还未使用到的内存,指针指向多少取决于该对象的大小,而该对象的大小是根据元数据来定义的,此时JVM是可以计算出来的

空闲列表

在这里插入图片描述

  1. 内存地址是不连续的
  2. 适用于老年代
  3. 由于老年代的对象比较大也比较老
    以至于占用空间不能连续当该对象被回收时,该对象会被回收,但是该内存不会被回收

对比

在这里插入图片描述

内存分配安全问题

问题概述

当两个线程同时去一块内存时会出现内存碰撞问题

解决方式

CAS:乐观锁(采用自旋失败重试的方式保证操作的原子性)
TLAB:本地线程分配缓存(为每一个线程预先分配一块内存)

流程

首先适用CAS进行TLAB分配。
当对象大于TLAB中的剩余内存或者TLAB的内存已经用尽时再i采用上述的CAS进行内存分配

对象内存分配以及GC详细流程-非常重要

  1. 第一次gc将Eden区中不可达(需要回收)的对象进行清楚,将可达(不可回收)的对象复制到s0,此时整个新生代只有s0区域有数据
  2. 第二次gc将Eden区和s0区域中不可达的对象进行清除,将可达的对象复制到s1区,此时只有s1区域有数据
  3. 第三次gc同理,但是有可能Eden区域+s1区域的可达对象复制到s0的时候发现s0的区域不够存下这么多的数据,此时这些对象会进行老年代晋升,则申请晋升老年代?
  4. 如果申请成功进行老年代分配对象内存
  5. 如果申请失败则进行FGC-full gc(整个堆进行GC)
  6. 如果FGC完还不能够存放得下
  7. 发生著名的OOM!

进入老年代的条件-非常重要

  1. 存活年龄过大:默认gc15次会进入老年代(-XX:MaxTenuringThreshold)
  2. 动态年龄判断:YGC之后发现s0或者s1中的一批对象总大小大于这块s区域内存大小的50%(这个参数可以指定-XX:TargetSurvivorRatio),那么此时大于等于这批对象中年龄最大的对象可以直接进入老年代。这么做的目的是希望把那些有可能是长期存活的对象尽早放入老年代
  3. 大对象直接进入老年代 1.前提:Serial或ParNew垃圾收集器 2.字符串或者数组-XX:PretenureSizeThreshold 一般设置成1M
  4. YGC(Minor GC)后存活对象太多无法放入s0或者s1区
  5. 空间担保机制:YGC前判断老年代可用空间是否已经小于整个新生代,如果小于则判断之前每次YGC进入老年代对象的平均大小,如果连续两次成功则会冒险尝试进行一次FGC,否则进行YGC

空间担保机制补充:
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

进入老年代的条件测试

//大家可以准备这样一段代码进行测试
//参数
//-Xms60m -Xmx60m -XX:+PrintGCDetails  -XX:+UseParallelGC  jdk1.8默认的垃圾回收器
//-Xms60m -Xmx60m -XX:+PrintGCDetails -XX:+UseParNewGC
public class TestJVM {

    private byte[] bytes = new byte[1024*1024*10];

    public static void main(String[] args) {
        TestJVM testJVM = new TestJVM();
        System.out.println("完毕");
    }
}

测试结果如下
======================================================================
测试1:使用parnew垃圾回收器 -XX:+UseParNewGC
20M新生代 15M没有进入老年代 16M进入了老年代
10M新生代 7M没有进入老年代 8M进入了老年代
总结:对象到达了新生代的80%就会进入老年代

============================================================
**测试2:使用jdk8默认的回收器 -XX:+UseParallelGC
20M新生代 14M没有进入老年代 15M进入了老年代
10M新生代 6M没有进入老年代 7M进入了老年代
总结:对象到达了新生代的70%就会进入老年代

====================================================================
测试3:测试3:使用-XX:PretenureSizeThreshold=2m -XX:+UseParNewGC 使得2M的对象会直接进入老年代**
================================================================
总结:内存分配方式随着垃圾回收器的变化而变化

空间机制担保测试:

public class TestJVM {

    private static  final int _1MB = 1024*1024;

    public static void main(String[] args) {
//        byte[] bytes = new byte[1024*1024*3];
//        System.out.println("完毕");

        byte[] allocation1, allocation2, allocation3,
                allocation4;
        allocation1 = new byte[2 * _1MB];//2M
        allocation2 = new byte[2 * _1MB];//2M
        allocation3 = new byte[2 * _1MB];//2M
        allocation4 = new byte[8 * _1MB];//4M
        System.out.println("完毕");
    }
}

参数:
//-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails 
//-XX:SurvivorRatio=8 -XX:+UseSerialGC
//堆空间20M
//年轻代10M
//老年代10M
//打印GC
//Eden:S0:S1=8M:1M:1M 

在这里插入图片描述

在这里插入图片描述
结论:

1. 当Eden区存储不下新分配的4M的对象时会触发一次minorGC(YGC)
2. GC之后还存活的对象正常逻辑应该进入幸存区(S0或者s1)
3. 但此时幸存区的空间不足够 存放新对象因此触发了内存担保机制
4. 将Eden区GC之后还存活的对象放入老年代也就是3个2M的对象,后来新的4M的对象放入到Eden区

ps:如果此时第四个进来的是一个8M的对象 此时不会触发担保 此时判断该对象已经达到了新生代的80% 此时这个8m的对象会直接进入 老年代

对象内存布局

对象内部组成结构

1.对象头
a.markword
b.类型指针
i:作用:通过反射找到Class。一个类的class类对象的内存地址
ii:开启指针压缩4字节 未开启8字节 jdk.1.6之后默认开启
c.数组长度(如果对象是数组则记录数组的长度)
2. 数据区域(具体存储实例变量)
3. 对其填充(满足8个字节 不够补充)

Markword

概述

1.64位的Markwod占8字节 2.java中并发编程都是在对象头实现的 想要搞懂synchronizied原理 必然要懂对象头中的Markword

这里不详细展开讲Markword 在并发编程章节会详细讲 只需要混个脸熟 知道几种锁态都是记录在Markword中的即可
在这里插入图片描述

直接内存(堆外内存)

ps:IO操作在整个系统层面的流程:

java.io->用户内存(java进程、redis、mysql…)->内核内存/直接内存(cpu以及系统需要占用的内存)->磁盘驱动->写回java进程
直接内存也叫堆外内存。并不是JVM数据区的一部分。属于内核内存。
这也是为什么IO在直接内存会如此之快的原因,netty、nginx等作为高性能框架多处使用了对外内存
jdk1.4之后提供了NIO。 引入DirectByteBuffer对象对这块内存进行操作。

直接内存和堆内存比较

  • 直接内存申请空间消耗更多的性能
  • 直接内存IO读写要比堆内存性能要好
  • 直接内存不受限于java堆内存的大小。受到物理机内存大小限制
  • 配置JVM参数不要忽略这个参数 以防止直接内存OOM - List item

程序计数器

  • 字节码指示器(存储下一次执行的字节码指令的行号)
  • 不会触发OOM
  • 线程私有的
  • 内存占用很小几乎可以忽略不计
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM8的内存结构图如下所示:\[1\] - 程序计数器 - 虚拟机栈(JVM Stack) - 本地方法栈 - 元空间(MetaSpace) - Java堆(Heap) 其中,程序计数器用于记录当前线程执行的字节码指令的地址;虚拟机栈用于存储方法调用的局部变量、操作数栈、动态链接、方法出口等信息;本地方法栈用于支持本地方法的调用;元空间用于存储类的元数据信息,取代了JDK1.8之前的永久代(PermGen);Java堆用于存储对象实例和数组。 此外,JVM8还有直接内存,它是独立于JVM内存之外的内存,可以直接和NIO接口交互,提升了程序性能。\[2\] 在Java堆内存中,内存需要划分成新生代和老年代。新生代又分为eden、from和to三块区域,默认比例是8:1:1。每次创建对象时,对象会先存储到eden区域,当eden区域满了后,会触发minor GC回收该区域,未回收的对象会放入from或to区域。每经过一次GC,from和to两块空间的对象会进行一次移动,未回收的对象年龄也会增加1。当对象年龄达到一定阈值(默认为15岁),就会被晋升到老年代。当老年代满了时,会触发Full GC回收。如果堆内存不足,就会出现OutOfMemoryError。可以通过配置JVM参数(如-Xmx)来设置最大堆内存大小。\[3\] #### 引用[.reference_title] - *1* [JVM内存结构详解](https://blog.csdn.net/weixin_42173451/article/details/105805231)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [最简单的JVM内存结构图](https://blog.csdn.net/duyabc/article/details/114679595)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值