从零开始第一期——所有JAVA后端程序员必须充电!!!!深入理解Linux内存管理

本文探讨了Java程序员常常忽视的内存管理,通过剖析Linux源码,揭示了进程间通信的管道、SystemVIPC和用户态/内核态切换。特别关注了内存管理的基础概念、实现过程和底层源码,以及与系统调用、中断异常和计算机原理的关联。

Java之所以被程序员们吐槽为入门门槛低,就是因为JVM垃圾回收器帮助管理了JVM内存。但也有一句话,天下没有免费的午餐,吃的时候有多豪横,吃完就有多难受。你真觉得你不用管理内存?那你是想多了,作为后端程序员,JVM你得管,linux你得管,没有一样能跑的掉。

今天我们就从linux源码上走一走linux内存管理。

当然本来我是想把这期做在《细读经典》系列里去过一遍《深入理解linux虚拟内存管理》,但是限于个人精力(还有之前托更了好多东西)先做一个简化版,当然也不算简化版,算是一个比较深入的阐述吧。

前戏

在开始内存之前,我可能要说一些其他东西。

1、进程间通信

(1)管道:进程通过共享管道实现进程通信,实际上就是在物理内存空间内开辟出一段缓存空间。分有名管道和无名管道

无名管道,需要以文件的方式进行操作(读写),而操作文件,就需要文件描述符(fd)进行读写,文件描述符通过调用pipe方法得到(非open方法,一般而言通过调用open方法打开文件获取文件描述符),因为它没有文件名,就无法用open方法得到文件描述符,也正是因为它是一个没有名称的管道文件,所以叫无名管道,且只用于亲缘进程之间的通信(因为没有文件描述符,只能通过亲缘关系传递管道的文件描述符,例如通过fork子进程)。当然,这里的亲缘进程,可以是子进程,孙进程,孙子进程,子子孙孙无穷匮也。

无名管道API

//创建pipe通道, 传入fd数组,其中fd[0]为数据读管道描述符,fd[1]为数据写管道描述符
int pipe(int fd[2]);
//通过管道描述符从管道中读取数据  
ssize_t read(int fd, void * buf, size_t count);  
//通过管道描述符向管道中写入数据
ssize_t write (int fd, const void *buf, size_t count);  
//关闭通道的接口应用
int close(int fd);

有名管道,当然就是有文件名,可以用open函数打开文件,然后用该文件描述符进行管道建立并进行通信。所以很明显,有名管道既可以用于亲缘进程之间的通信,也可以用于非亲缘进程之间的通信。

有名管道API

//用于创建fifo管道的应用实现
int mkfifo (const char *__path, __mode_t __mode);
//打开FIFO管道,获取后续使用的描述符
int open(const char *pathname, int oflag,...);  
//从FIFO管道中读取数据  
ssize_t read(int fd, void * buf, size_t count);  
//向FIFO管道写入数据  
ssize_t write (int fd, const void * buf, size_t count);  
//关闭FIFO管道
int close(int fd);  
//移除FIFO管道
int unlink (const char *__name);  

管道还牵扯到单向通信和双向通信,我就不展开了,现在还用不到

(2)System V IPC(System linux V(罗马字字母)版本的Inter-Process Communication, 人话就是linux第五版本进程间通信)

管道是一种很原始的进程通信方式,早在linux设计之初就存在管道。随着linux版本升级到第五版本,提供了新的进程间通信方式,包含消息队列,信号量和共享内存

消息队列:看API比看文字清晰,msqid作为消息队列标识符,利用msgsnd发送,利用msgrcv接受消息,利用msgctl操作消息队列上的内容,提供删除操作。调用过程就不展开了

消息队列API

//创建消息队列
int msgget(key_t key, int oflg);
//从消息队列里读取数据
ssize_t msgrcv(int msqid, void *ptr, size_t length, long type, int flag);
//创建一个新的消息队列或访问一个已存在的消息队列
int msgsnd(int msqid, const void *ptr, size_t length, int flag);
//提供在一个消息队列上的各种控制操作
int msgctl(int msqid, int cmd, struct msqid_ds *buff);

共享内存:还是在物理内存上开辟一段缓存空间,不过与管道不同的是,使用共享内存是通过直接使用地址来共享读写的,而不需要经过系统调用。

共享内存API

//key程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1;
//size以字节为单位指定需要共享的内存容量;
//shmflg是权限标志,它的作用与open函数的mode参数一样
int shmget(key_t key, size_t size, int shmflg);
//shm_id是由shmget()函数返回的共享内存标识。
//shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
//shm_flg是一组标志位,通常为0。
void *shmat(int shm_id, const void *shm_addr, int shmflg);
//参数shmaddr是shmat()函数返回的地址指针,调用成功时返回0,失败时返回-1.
int shmdt(const void *shmaddr);
//shm_id是shmget()函数返回的共享内存标识符。
//command是要采取的操作,它可以取下面的三个值 :
//IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
//IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
//IPC_RMID:删除共享内存段
//buf是一个结构指针,它指向共享内存模式和访问权限的结构。
int shmctl(int shm_id, int command, struct shmid_ds *buf);

信号量:java信号量同理,所以比较重要,进程和线程的信号量是对所操作资源的保护。

这里当然要先说以下同步和互斥的关系,互斥是指资源访问的排他性,和访问顺序无关,同步是在互斥的基础上,实现有序访问。

信号量就是指定同一时间能够访问临界区资源的进程的数量。

信号量API看似简单,其实还是很精妙的,在JAVA里,信号量底层是AQS,AQS底层又是一些unsafe类,我之前也有文章讲unsafe类的,传送门:好文笔记——unsafe类_u014783007的博客-优快云博客

//key:一个键值,和消息队列生成键值的方式一样
//nsems:表示信号量集合中信号量的个数
//semflg:一个位掩码
int semget(key_t key, int nsems, int semflg);
//semid:信号量集合的标识符
//semnum:信号量的序号,置0表示忽视该参数
//cmd:指定了相关控制操作
int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);

2、用户态和内核态

我们之前一直在说系统调用,包括刚刚在说进程通信时也放了一大堆API,那么系统调用到底是什么呢?

一般而言,程序要不运行在用户态(用户交互),要不运行在内核态(系统调用)。

用户态切换到内核态是在程序申请外部资源(内存条,网卡,声卡,usb等等等等)时进行切换。一般而言,当程序在进行系统调用,或产生中断,或产生异常时就会从用户态向内核态进行切换。

例如读一个文件,我们会调用open创建fd,再用fd去进行write或read的API系统调用。例如我们分配内存时调用malloc,就会调用系统函数brk或mmap进行系统调用;又例如我们常听的缺页中断等等,程序中存在大量的系统调用(当然你映射到JAVA,new对象也是系统调用)。

系统调用,再linux中输入:

man syscalls

会显示所有的系统调用,这块就留个大家自己开发了。

这里我再举个例子,以多路复用IO为例,select和poll在进行系统调用的时候,需要把所有的fd从用户态拷贝到内核态进行遍历,本次调用结束之后,再把fd拷贝回来,而epoll则只需要在第一次进行系统调用的时候拷贝一次所有的fd,之后将用户关系的文件描述符的事件存放到内核的一个事件表中,这样只需要增量维护事件表,从而提升性能。当然epoll还会牵扯到mmap,红黑树(事件链表),等等,还会牵扯到LT,ET效率问题(LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。 ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的)。你会发现其实系统函数是一个很有趣的部分,也是更贴近我们理论上所学习的各个模型的。

中断和异常不展开,有兴趣的同学自行脑补。

3、计算机组成原理(简化版)

//TODO

正戏

1、基本概念

2、实现过程

3、底层源码

### 回答1: Java后端程序员可以通过以下方式来成长: 1. 不断学习和掌握新的技术和框架,如Spring, Hibernate等。 2. 完成项目,并且不断总结和提高。 3. 参加技术社区活动,和其他程序员交流和学习。 4. 阅读相关技术文章和书籍,不断提高自己的理解能力。 5. 注意代码质量和可维护性,并且不断提高自己的编码能力。 ### 回答2: 要成为一名优秀的Java后端程序员,以下是一些成长的关键点: 1. 扎实的基础知识:掌握Java语言的基础知识,如语法、面向对象编程、异常处理等。还要了解常用的数据结构和算法,以便在解决问题时能够选择合适的解决方案。 2. 学习框架和工具:深入学习常用的Java后端框架,如Spring、Hibernate等,理解它们的原理和使用方式。同时掌握常用的开发工具,如Eclipse、IntelliJ IDEA等,提高开发效率。 3. 实践项目经验:参与实际项目开发,不断积累实战经验。通过实践,了解项目开发流程、团队协作和版本控制等,同时也可以发现和解决一些实际问题。 4. 深入学习数据库:掌握常用的关系型数据库和NoSQL数据库,如MySQL、MongoDB等。了解数据库的原理和优化技巧,能够设计和优化数据库模型。 5. 持续学习和自我提升:Java后端是一个快速发展的领域,需要保持学习的热情和积极性。关注新技术和行业动态,参加技术交流活动,阅读相关书籍和文章,不断提升自己的技术水平。 6. 理解业务需求:作为一名优秀的Java后端程序员,需要与产品和需求团队紧密合作,理解业务需求并转化为可行的技术方案。同时注重与前端开发人员的协作,保证整个系统的协同工作。 7. 代码质量和可维护性:编写高质量的代码是成长的关键。注重编码规范,使用设计模式和合理的架构,编写具有可读性、可维护性和可扩展性的代码。 8. 锻炼解决问题的能力:Java后端开发过程中,经常会面临各种问题和挑战,要有解决问题的能力。培养良好的问题分析和解决思路,能够快速定位问题并给出解决方法。 总之,作为Java后端程序员,成长的关键在于持续学习、不断实践和提升自我的能力。只有通过不断地努力和不断提高,才能成为一名优秀的Java后端程序员。 ### 回答3: 作为一名 Java 后端程序员,要想不断成长,以下几点是很关键的: 1. 深入学习和掌握 Java 技术:Java 是一门广泛应用于后端开发的编程语言,了解核心概念和基础知识是成长的第一步。通过学习官方文档、书籍、在线教程等,掌握 Java 的语法、面向对象编程、多线程、集合等核心技术。 2. 实践项目经验:通过参与实际项目,积累实践经验是成长的重要途径。可以通过参与个人项目、开源项目,或者是向公司申请承担一些有挑战性的工作任务,来提升解决问题的能力和技术广度。 3. 学习框架和工具:Java 后端开发中使用了许多优秀的框架,如 Spring、MyBatis、Hibernate 等。深入了解这些框架的原理和使用方法,可以提高开发效率和代码质量。 4. 关注行业动态和技术趋势:技术日新月异,了解当前行业的动态和技术趋势对于 Java 后端程序员至关重要。可以通过关注技术博客、参与技术社区等方式,及时了解新的技术和工具,不断拓宽技术视野。 5. 不断学习和自我更新:作为一名程序员,持续学习和自我更新是非常重要的。可以通过定期参加培训课程、参与技术社区讨论、阅读技术书籍等方式,不断提升自己的技术水平和专业素养。 6. 提高解决问题的方法和思维:作为一名 Java 后端程序员,解决问题是日常工作的重要一环。要学会运用科学的方法和合理的思维来解决问题,善于分析、排查和调试代码,提高代码的可维护性和性能。 7. 不断挑战自我:面对技术难题和挑战时,不要畏惧,要积极主动地接受挑战并解决问题。通过接触一些新领域和技术,接触一些有挑战性的项目,可以不断挑战自我,从中获得成长。 总之,Java 后端程序员要想不断成长,需要不断学习新技术,实践项目经验,关注行业动态,提升解决问题的能力和思维方式,并时刻保持学习的状态和自我挑战的心态。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值