【JVM】亿级流量调优(一)

亿级流量调优

oop模型

在这里插入图片描述

前面的klass模型,它是Java类的元信息在JVM中的存在形式。这个oop模型是Java对象在JVM中的存在形式

内存分配策略:

  • 1.空闲列表
  • 2.指针碰撞(jvm采用的)
    2.1 top指针:执行的是可用内存的起始位置
    2.2 采用CAS的方式
  • 3.TLAB 线程私有堆
  • 4.PLAB 老年代的线程私有堆

对象的创建

Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(引用类型的对象)的创建又是一个怎样的过程呢?
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号一弄,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行响应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针想空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为"指针碰撞"(Bump the Pointer)。如果Java堆中的内存并不是规整的,已使用内存和空闲的内存相互交错,那就没有办法简单进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为"空闲列表"(Free List).选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法时指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表
(内存分配算法也跟对象的存活周期有关,新生代大部分对象都朝生夕死,复制算法进行GC完之后,内存就是规整的,而老年代,存活对象相比新生代来说存活率要高,内存不太容易规整,如果不带整理的话,使用指针碰撞失败的概率会高很多。所以老年代采用空闲列表)
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案,一种是堆分配内存空间的动作进行同步处理——实际上迅即采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用了TLAB,可以通过-XX:+/-UseTLAB参数来设定

内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机要对对象进行必要的设置,例如这个对象时哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上面的工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——方法还没有执行,所有的字段都还为0,所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行方法,把对象按照程序员的医院进行初始化,这样一个真正可用的对象才算完全生成出来

HotSpot源码,_new字节码指令分析

在这里插入图片描述

  • 1.确保常量池中存放的是已解释的类
  • 2.确保对象所属类型已经经过初始化阶段
  • 3.取对象长度
  • 4.记录是否需要将对象所有字段置为零
  • 5.是否在TLAB中分配对象,否则直接在Eden中分配对象.cmpxchg是x86中的CAS指令,这里是一个C++方法,通过CAS方式分配空间,如果并发失败,转到retry中充实,知道成功分配为止
  • 6…如果需要,则为对象初始化零值
  • 7.根据是否启用偏向锁来设置对象头信息
  • 8.将对象引用入栈,继续执行下一条指令

1.空闲列表

在这里插入图片描述

OS把不常用的内存写到硬盘上,如果有进程需要读取引发缺页异常,进而会去硬盘上读
空闲列表机制
在操作系统中,内存分配策略的空闲列表机制是一种管理内存资源的方法。以下是其基本原理和步骤

  • 1.基本原理:

  • 1.1 内存块管理:操作系统将内存划分为多个块(block),每个块可以是空闲的,也可以是已分配的

  • 1.2 空闲列表:操作系统维护一个记录所有空闲内存块的列表,称为空闲列表。这个列表通常会记录每个空闲块的大小和起始地址。

  • 2.步骤:

  • 2.1 初始化:当系统启动时,除了操作系统本身占用的内存外,其余的内存都被视为一个大的空闲快,并被加入到空闲列表中

  • 2.2 分配内存:

  • 2.2.1 当一个进程请求内存时,操作系统会根据请求的大小在空闲列表中查找何时的空闲块

  • 2.2.2 查找策略可以是首次适配(first fit)、最佳适配(best fit)或最坏适配(worst fit)

  • 2.2.3 一旦找到合适的空闲块,操作系统会从空闲列表中移除该块,并将其标记为已分配,然后将内存分配给请求的进程

  • 2.3 内存释放

  • 2.3.1 当进程释放内存时,操作系统会回收这块内存,并将其标记为空闲

  • 2.3.2 操作系统可能会将这块空闲与周围的空闲块合并,形成一个更大的空闲块,以减少内存碎片
    2.3.3 合并后的空闲块或新的空闲块会被重新加入到空闲列表中

  • 2.4 碎片整理

  • 2.4.1 随着内存的分配和释放,内存可能会出现碎片化,即空闲内存分散在各个角落,导致无法满足大的内存请求

  • 2.4.2 空闲列表机制可能会通过移动已分配的内存块来整理碎片,但这在实际操作中可能比较复杂且耗时

  • 优点:
    简单性:空闲列表机制相对简单,易于实现
    灵活性:可以根据不同的内存分配策略(如首次适配、最佳适配等)来优化内存使用

  • 缺点:
    维护开销:随着内存分配和释放的频繁进行,空闲列表的维护可能会带来一定的开销
    内存碎片:可能导致内存碎片,尤其时当空闲块和已分配块的大小频繁变动时。

操作系统为什么不采用指针碰撞机制进行内存分配?

操作系统不采用指针碰撞的机制进行内存分配,主要是因为操作系统的内存管理需要面对复杂和多样化的环境。以下是一些关键原因:

  • 1.多任务和多用户
    操作系统:必须支持多个进程和线程的运行,每个进程或线程可能需要不同大小的内存,且分配和释内存的时间点是随机的
    指针碰撞:适用于单一连续内存分配的场景,不适合处理多任务环境下的复杂内存需求
  • 2.内存碎片
    操作系统:需要处理内存碎片问题,因为不同大小的内存块被分配和释放后,内存中可能会留下无法被利用的小空闲块
    指针碰撞:不擅长处理内存碎片,因为它假设内存分配是连续的,如果内存碎片严重,指针碰撞机制将无法有效工作
  • 3.内存分配的灵活性
    操作系统:需要能够分配任意大小的内存块以满足不同进程的需求
    指针碰撞:通常需要一个连续的内存区域,并且当内存区域不足以容纳新分配的内存块是,需要额外的机制来处理这种情况
  • 4.物理内存与虚拟内存
    操作系统:使用虚拟内存技术,将物理内存与虚拟内存地址映射起来,这使得内存分配更加复杂
    指针碰撞:适用于简单的物理内存分配,不涉及复杂的地址映射
  • 5.安全性和隔离性
    操作系统:需要确保不同进程的内存是隔离的,防止一个进程访问或修改另一个进程的内存
    指针碰撞:需要额外的机制来保证内存的安全性和隔离性
  • 6.性能考量
    操作系统:必须高效地管理内存以满足性能需求,这通常意味着需要一个能够快速响应的内存分配策略
    指针碰撞:虽然分配速度快,但在多任务环境下,它可能导致内存利用率低下,因为它可能留下很多小的空闲内存块
  • 7.系统调用和API
    操作系统:提供了系统调用和API供应用程序请求和释放内存,这些调用需要能够处理各种复杂的内存分配请求
    指针碰撞:无法直接适应这些系统调用和API的需求

2.指针碰撞(CAS)

在这里插入图片描述

如图所示,bottom指向内存区域的头部,end指向内存区域的尾部,top指针开始指向头部(可用内存的起始位置)。如果new_top = top(当前top) + 对象大小满足这个等式,则分配成功,并top=new_top

为什么JVM不采用空闲列表的内存分配策略而是采用指针碰撞的形式?

Java虚拟机(JVM)内存分配策略与操作系统分配策略的不同,主要由以下几个因素决定的:

  • 1.内存管理的抽象层级不同:
    操作系统:操作系统负责管理物理内存,直接与硬件交互,需要处理多种复杂情况,如内存碎片、多进程/线程的内存需求等
    JVM:JVM运行在操作系统之上,主要负责管理Java程序的运行时内存,它对内存的管理更加抽象化,并且通常不需要处理硬件级别的内存碎片问题
  • 2.内存分配的特点
    空闲列表:适用于需要频繁分配和释放不同大小内存的场景,且物理内存可能存在碎片
    指针碰撞:适用于对象大小相对一致且频繁创建和销毁的场景,如JVM中的对象分配
  • 3.JVM内存分配的具体考虑
    效率:指针碰撞时一种非常高效的内存分配方式。在JVM中,当一个新的对象需要被分配时,只需要移动以下指针(分配指针),而不需要遍历整个空闲列表来查找合适的内存块。这大大减少了内存分配的开销
    内存连续性:指针碰撞可以保证分配的内存是连续的,这对于提高缓存命中率有好处,因为连续的内存访问往往能更好地利用CPU缓存
    内存碎片:由于JVM通常会分配和释放大量大小相似的对象,内存碎片问题不像在操作系统中那么严重。JVM通过垃圾回收来管理内存,看可以在GC过程中重新整理内存,减少碎片
    垃圾回收:JVM采用垃圾回收机制来自动管理内存。当对象不再被引用时,垃圾回收期会自动回收他们所占用的内存。这种方式与空闲列表的内存分配策略相比,减少了手动内存释放的复杂性,并且通过不同的垃圾回收u算法来优化没存使用
  • 4.JVM的内存模型:
    堆空间:JVM的堆空间是用于存储Java对象的地方,通常分为老年代和新生代等,不同代的内存管理策略不同。新生代采用复制算法,老年代采用标记-清除或标记-整理算法。这些算法与指针碰撞的内存分配策略更为契合。

JVM选择指针碰撞的内存分配策略,而不是空闲列表,是因为这种策略更符合JVM内存管理的需求,能够提供更高的内存分配和回收效率,并且与JVM的垃圾回收机制更为兼容

3.TLAB线程私有堆(新生代)

JVM里面如果用锁来控制对象内存的分配的话,会比较繁琐,它是堆中某一块私有内存区域,如果用完了再还给JVM,重新盛情一块更大的内存

4.PLAB(TLAB的老年代)

对象的内存布局

在这里插入图片描述

  • 1.MarkWord:
    32位机器下占4B,64位机器下占8B
  • 2.类型指针:
    开启指针压缩占4B,关闭:占8B
  • 3.数组长度
    如果是数组对象:4B,非数组对象的话是没有的
  • 4.对象头中的对齐填充(数组对象才有)对象头尾部
  • 5.实例数据:非静态属性数据
    boolean:1B
    byte:1B
    char:Java(2B) C++(1B)
    int:4B
    float:4B
    long:8B
    double:8B
    引用类型:开启指针压缩:4B 关闭8B
  • 6.对齐填充区域中的对齐填充
    跟jvm规范有关,所有的对象大小都必须能被8整除,也叫8字节对齐

计算对象大小

没有属性的对象

在这里插入图片描述

MarkWord:(64位)8B
类型指针:(默认是开启指针压缩的)4B
由于8B+4B=12B,不够被8整除,所以对齐填充4B
对象大小:8B+4B+4B=16B

有属性的对象

Mark Word:8B
类型指针:4B
实例数据:4B+4B
对象大小

数组对象

在这里插入图片描述

Mark Word:8B
类型指针:注意这里的数组类型是int类型,也就是说它的OopDesc是TypeArrayOopDesc,4B
数组长度:4B
实例数据:4B+4B+4B
8B+4B+4B+4B+4B+4B=28B,还是不能被8整除,所以对齐填充需要补4B
8B+4B+4B+4B+4B+4B+4B=32B

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/408645.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用DropZone+SpringBoot实现图片的上传和浏览

经常在项目中需要使用上传文件功能,找了不少前端上传组件,都不是很好用,今天尝试了一下DropZone,发现不错,顺便记录一下使用过程,方便后续查阅。在做开发的时候,经常需要调研一些技术&#xff0…

C# 运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C# 有丰富的内置运算符,分为一下六类: 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 杂项运算符 算术运算符 C# 支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值…

安全面试常见问题任意文件下载

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 1.1 任意文件下…

旅游社交小程序的设计

管理员账户功能包括:系统首页,个人中心,用户管理,每日签到管理,景点推荐管理,景点分类管理,防疫查询管理,美食推荐管理,酒店推荐管理,周边推荐管理 微信端账…

《数据结构》顺序表+算法代码+动画演示-C语言版

目录 顺序表概念 顺序表初始化 顺序表销毁 顺序表尾插 顺序表尾删 顺序表头删 顺序表头插 顺序表pos位置插入 顺序表pos位置删除 顺序表全部代码如下: 顺序表概念 顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下…

多个程序监听不同网卡的相同端口、相同网卡不同IP的相同端口

1 概述 一个主机上的多个程序监听同一个端口,是否一定存在冲突?如果是多网卡、单网卡多IP的情景下,多个程序是可以独立监听的。 2 多个程序监听不同网卡的相同端口 3 多个程序监听同一个网卡不同IP的相同端口 4 小结 多个程序监听同一个网…

【C语言】常见文件操作

文件的常见操作 #include<stdio.h>// 由于devc代码编码为ANCI&#xff0c;故读取的文件中若有中文&#xff0c;请设置文件编码为ANCI&#xff0c;否则会乱码 // 读文件 void test1() {char ch;FILE *fp; // 创建文件指针fp fopen("./file.txt", "r"…

pycharm修改文件大小限制

场景&#xff1a; 方法&#xff1a; 打开pycharm 安装目录下的idea.properties 增加配置项&#xff1a;idea.max.intellisense.filesize99999

java后端请求与响应总结

get 请求&#xff1a;将参数写在请求路径中&#xff08;请求路径跟一个&#xff1f;后面跟参数多个参数之间用&连接&#xff09; post 请求&#xff1a;将参数写在请求体中中 一、请求 1.简单参数 如 传一个或两个字符串、整数等 例如串一个用户名和密码 如果传入的数…

【自动驾驶】控制算法(四)坐标变换与横向误差微分方程

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

从法律风险的角度来看,项目经理遇到不清楚或不明确问题时的处理

大家好&#xff0c;我是不会魔法的兔子&#xff0c;在北京从事律师工作&#xff0c;日常分享项目管理风险预防方面的内容。 序言 在项目开展过程中&#xff0c;有时候会遇到一些不清楚或不明确的状况&#xff0c;但碍于项目进度的紧迫性&#xff0c;不得不硬着头皮做决策&…

Golang | Leetcode Golang题解之第368题最大整除子集

题目&#xff1a; 题解&#xff1a; func largestDivisibleSubset(nums []int) (res []int) {sort.Ints(nums)// 第 1 步&#xff1a;动态规划找出最大子集的个数、最大子集中的最大整数n : len(nums)dp : make([]int, n)for i : range dp {dp[i] 1}maxSize, maxVal : 1, 1fo…

CMake构建学习笔记4-libjpeg库的构建

libjpeg是一个广泛使用的开源库&#xff0c;用于处理JPEG&#xff08;Joint Photographic Experts Group&#xff09;图像格式的编码、解码、压缩和解压缩功能&#xff0c;是许多图像处理软件和库的基础。 libjpeg本身的构建没什么特别的&#xff0c;不过值得说道的是libjpeg存…

[Other]-安装ruby、ascli、ascp

最近新接到这样一个需求&#xff0c;将生物原始数据上传到某中心&#xff0c;其中用到ascp命令&#xff0c;阴差阳错的装了ruby、ascli&#xff0c;这里就都一并介绍下安装方式&#xff0c;由于服务器老旧默认安装时ruby2.0&#xff0c;又 升级到2.7等引发的一系列问题&#xf…

力扣(动态规划)-343整数拆分;96不同的二叉搜索树

整数拆分 题目&#xff1a; 给定⼀个正整数 n&#xff0c;将其拆分为⾄少两个正整数的和&#xff0c;并使这些整数的乘积最⼤化。 返回你可以获得的最⼤乘积。 示例 1: 输⼊: 2 输出: 1 解释: 2 1 1, 1 1 1。 示例 2: 输⼊: 10 输出: 36 解释: 10 3 3 4, 3 3…

Python 递归(recursion) 和 迭代(iteration)

递归 (recursion) 是指在函数的定义中使用函数自身的方法&#xff0c;直观上来看&#xff0c;就是某个函数自己调用自己。递归的基本思想就是把规模大的问题转化为规模小的相同的子问题来解决。 在函数实现时&#xff0c;因为大问题和小问题是一样的问题&#xff0c;因此大问题…

一款人性化的终端用户界面工具

A collection of human friendly terminal user interface. Screenshot • Installation • Usage • Configuration • Thanks 截图 历史文件预览 注意: find file 依赖 fzf. file browser依赖 ranger / lf / … 安装 git clone https://github.com/StubbornVegeta/Sta…

3秒内搞定服务器端口扫描!用RustScan快速查看开放端口

文章目录 3秒内搞定服务器端口扫描&#xff01;用RustScan快速查看开放端口1. RustScan简介2. RustScan特点3. RustScan的基本使用3.1 创建alias别名3.2 基本用法3.3 常用参数说明3.4 示例4. 注意事项 最近开始公众号文章也开始同步更新了&#xff0c;对Java、大数据、人工智能…

字节微前端框架Garfish

Garfish 是字节跳动开源的微前端框架&#xff0c;旨在应对现代 Web 应用在前端生态繁荣与应用日益复杂化背景下的挑战。本文将介绍如何使用 Garfish&#xff0c;提供代码示例&#xff0c;并与另一流行的微前端框架 Qiankun 进行对比分析。 安装 Garfish 首先&#xff0c;安装…

大模型对齐:DPO vs PPO

现在这些大型语言模型&#xff08;LLMs&#xff09;&#xff0c;可真是火得不行&#xff0c;各行各业都离不开它们了。它们能处理和写出跟我们差不多的文本&#xff0c;这让自然语言处理、写东西、还有客服这些领域都焕然一新。不过呢&#xff0c;这技术进步的同时也带来了一个…