JavaWeb技术内幕八:JVM内存管理

简介: 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/81517099 这位大侠,这是我的公众号:程序员江湖。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/81517099

微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」 等关键字可以获取对应的免费学习资料。 


                     wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

欢迎阅读我的专栏:JavaWeb技术世界

’与其他高级语言不一样,在Java中基本上不会显示地调用分配内存的函数,我们甚至不用关心到底哪些程序指令需要分配内存,哪些不需要分配内存。

我们首先需要从操作系统层面理解物理内存的分配和Java运行的内存分配之间的关系。

物理内存与虚拟内存

1 物理内存就是RAM,还有一个存储单元叫做寄存器,连接处理器和RAM或者寄存器的是地址总线,这个地址总线的宽度影响了物理地址的索引范围,32位地址总线可以寻址4gb内存。

2 除了硬件程序或者驱动程序需要直接访问存储器外,大部分情况下都是通过操作系统提供的接口来访问内存,在java中甚至不需要写和内存相关的代码。

3 不管是在windows还是linux,我们运行程序都要向操作系统申请内存地址,通常操作系统管理内存的申请空间是按照进程来管理的。每个进程拥有一个独立的地址空间,逻辑上是隔离的。

4 但是由于系统内存有限,物理空间需要得到更有效的利用,于是进程在物理上是共享物理内存的。

5 为了实现这一目的,操作系统设计了虚拟内存的概念,每个进程有一个很大的虚拟地址空间,虚拟地址可以让进程共享物理内存,提高利用率,也可以让进程独享大段虚拟地址空间。

页面和页面置换

进程在不活动的时候,把物理内存中的数据存在磁盘文件中。在windows中就是页面文件,在linux中则是swap分区。真正高效的物理内存留给正在活动的程序使用。

当我们唤醒一个很长时间没有使用的程序时,磁盘会吱吱作响,因为此时在进行页面调度,这种情况需要避免经常出现,否则效率会很低。如果linux上的swap分区被频繁使用,则系统会非常慢,说明物理内存可能严重不足或者某些程序没有释放内存。

内核空间和用户空间

计算机有一定大小的内存空间,比如4GB,这些地址空间并不能被用户程序完全使用。因为有一部分空间是内核空间。

内核空间主要是操作系统运行时所使用的用于进程调度,虚拟内存的使用或者硬件资源等的程序逻辑。

为了保证系统的稳定性和安全性,必须划分为两个空间。

访问硬件资源如网络逻辑,执行IO读取磁盘数据等操作,都需要通过系统调用来完成,系统调用就是执行操作系统提供的接口。

而操作系统和硬件之间的交互一般是基于指令集来完成的,机器码根据指令集进行译码,cpu根据指令完成对应的操作,比如访问内存,执行IO操作,以及访问硬件等等。

系统调用

执行系统调用的时候,需要进行两个内存空间的切换,经常需要在两个内存空间之间进行内容复制。这牺牲了一部分效率,Linux系统sendfile文件传输方式,可以减少这种内核空间到用户空间的数据复制方式。

Java中哪些组件需要使用内存

java堆

java堆用于存储java对象,通过-xmx表示堆的最大值,xms表示初始大小,一旦分配完成,堆空间就固定了。不能重新申请。

线程

jvm运行程序的实体是线程,只有java这一个进程代表jvm实例。
线程需要要内存空间来存储一些必要数据,每个线程创建时jvm会为他创建一个堆栈,堆栈的大小根据不同的jvm实现而不同。

通常在256k到756k之间。

线程所占空间比堆空间来说比较小,但使用量可以非常大。

类和类加载器

永久代以前的实现是方法区,现在改为元数据区。

jvm是按需加载类的,如果要加载一个jar包是否把这个jar包中所有的类加载到内存中!??

如果是这样,那么一个很大的jar包,如果我们只使用一个类,却需要全部加载,也太浪费了把。

显然不是的,jvm只会加载那些在你的应用程序中明确使用的类到内存中,要查看jvm到底加载了哪些类,可以在启动参数后加上-verbose:class

类加载的内存泄漏

如果使用自定义类加载器加载类,可能会出现重复加载的情况,如果方法区不能对失效类进行卸载,则可能导致方法区内存泄漏。

一个类A能够被卸载的必要条件如下

1:在Java堆中没加载有A的classloader对象的引用。

2:java堆上没有加载A的classloader已加载的类的class对象引用

3:java堆上没有加载A的类加载器加载的任何类的对象

由于jvm创建的三个默认类加载器都不可能满足这些条件,所以任何系统类加载器加载的类都不能在运行时被释放。

NIO

java在1.4后添加了NIO

1 开始支持使用allocatedirect方法为bytebuffer分配内存。
这个方法分配内存使用的是本机内存而不是Java堆上的内存。

2 直接操作这个bytebuffer时不需要进行java内存和本机内存的复制,比起Java内存和内核空间的复制效率要快得多。

3 直接bytebuffer对象会自动清理已分配的本地内存(缓冲区内存),但这个过程只能作为java堆gc的一部分来执行,没办法单独执行。

4 只有在堆内存满时发生GC或者显示调用system.gc时才能释放这部分直接内存,所以会增加gc的次数。

5 如果进制system.gc,则有可能导致直接内存泄漏

JNI

JNI技术使得java代码可以调用本机代码(比如c语言程序)。
这部分用到了native memory,也就是本地内存。

JVM内存结构

pc寄存器(程序计数器)

由于Java是多线程应用。线程经常被中断,为了恢复时记住上次运行到哪,需要一个程序计数器。

Java栈

1 Java的栈和线程关联在一起。

2 每个线程有一个栈。

3 每运行一个方法就创建一个新栈帧。

4 栈帧包含了内部变量(方法内部的变量,不是方法参数,方法参数用调用者传来),操作数栈,方法返回值等信息。

5 每个方法执行完成时,每个栈帧都会弹出栈帧的元素作为方法的返回值。

6 java栈的栈顶就是当前的活动栈,pc寄存器会指向这个地址。

堆是存储对象的地方,每一个存储在堆中的对象都是这个对象类的一个副本,它会复制包括继承它的父类的所有非静态属性。
注意是非静态属性,静态属性编译时确定,存在类的元数据中,在方法区。

方法区

方法区是存储类结构信息的地方。

class文件被加到jvm中时,会被存储在不同的数据结构中。

其中类的常量池,域,方法数据,方法体,构造函数,类的静态方法等代码都存在这里。

方法区存储的数据比较稳定,不会被频繁回收。

运行时常量池

jvm中定义运行时常量池是在方法区中的一部分。

它代表着运行时每个class文件中的常量表,它包括几种常量,编译期的数字常量,方法或者域的引用(在运行时解析连接)。

本地方法栈

本地方法栈是伪jvm运行native方法准备的空间,和java栈的作用类似,也叫c栈,代码中的native方法调用会使用这个存储空间,在jvm利用JIT方法编译为native代码后,也会通过这个栈来跟踪和执行方法。

jvm内存分配策略

通常分配策略

操作系统把内存分配策略分为三种

1 静态内存分配

编译期确定分配空间

2 栈内存分配

内存需求完全未知,动态分配

3 堆内存分配

堆内存分配会划分一定空间为堆内存,然后根据代码需要动态分配空间

java中的内存分配详解

1 java的分配和线程绑定在一起,栈的本地变量表通过slot数组来分配,可以复用,由于栈帧的大小确定,所以编译期就可以确定大小,进行内存分配。

栈中主要存放基本类型的变量数据,和对象引用,读取速度较快,仅次于寄存器。

2 应用程序所创建的UI想或者数组都在堆中,并且能够共享。
建立对象时在堆中分配实例,在对应栈中分配引用。

由于堆要动态分配内存,所以存取速度较慢。

jvm内存回收策略

静态内存分配和回溯

java的静态内存在栈上分配,方法运行结束后对应栈帧也就消失了,所以静态内存空间也就回收了。

动态内存分配和回收

对象不再被使用时会被回收,这是垃圾回收器要解决的问题。

如何检测垃圾

可达性分析:从GC ROOT出发标记活动集合,剩下的不可达对象就是垃圾

1 方法内部变量对对象的引用

2 java栈中的对象引用

3 常量池中的对象引用,比如对字符串对象的引用

4 本地方法中持有的对象引用

总结一下就是从四个方面找到正在活动的对象引用,比如方法内部,栈中引用,常量池中引用,以及本地方法的引用。

基于分代的垃圾收集算法

1 young区分为eden和两个survivor区。初始分配到eden区,存活对象进入survivor一个区,这两个survivor保证有一个是空的。

2 old区存放young区的survivor满触发minor gc后仍然存活的对象。如果old区也满则触发full gc。

3 方法区存放类的元数据信息,可以被full gc回收。

visualvm工具的visualgc插件可以观察到不同代的垃圾回收情况。比如占用率,空闲内存,回收次数,耗时情况等。

一般建议young区为整个堆的1/5;survivor和eden是2:8的关系,也就是eden:from:to = 8:1:1

image

垃圾收集器

serial collector

parallel collector

CMS collector

内存问题分析

gc日志分析

有时候我们不知道何时会发生内存溢出,但是发生时我们不知道原因,所以在jvm就加上一个参数可以记下一些当时的情况。

gc日志输出如下参数

-verbose:gc,可以辅助输出gc详细信息

-XX:+printGCDetails,输出gc的详细信息

一般会输出

1 回收器名称

2 各区内存的使用情况和gc后的使用情况

3 各区进行gc的停顿时间

4 整个堆在gc前后的内存

5 gc过程中jvm暂停的总时间

jvm自带工具分析

jstat -gcutil
可以分析一些GC的情况

1 各分区的使用空间情况

2 永久代内存情况

3 young gc和 full gc的次数和所用时间

堆快照文件分析

通过jmap -dump 可以用来记下堆的文件快照。然后利用第三方工具如mat来分析整个Heap的对象关联情况

jvm crash日志分析

jvm有时会因为一些原因垮掉,因为jvm也是一个程序,可能会出bug导致异常退出。jvm退出会在工作目录产生一个日志文件。使用-XX:ErrorFile可以转储该文件。

一般有三种原因导致退出

1 exception——access——violation
jvm运行自己的代码时出错。

2 sigsegv
jvm正在执行JNI代码时出错

3 exception——stack——overflow
这个栈溢出不是java线程的栈溢出,因为线程的栈溢出只会抛出Stack Overflow exception。

这个主要是因为jvm的代码是c++代码,如果自身运行时栈溢出也会导致jvm退出。

实例分析1:内存泄漏

问题:

1 系统负载偏高,达到6,平时基本为1

2 整个系统响应较慢,重启之后恢复正常

解决思路:

PS科普:CMS GC时出现promotion failed跟concurrent mode failure

对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。

promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

所以在别一次性占用太多的内存,如果是读文件,可以采用拆分的方法。或者把GC内存调大点

1 查看GC,FGC明显异常,过程中出现了concurrent mode failure。说明在old区分配内存时出现了失败的情况。(多个线程触发young gc,导致多个对象同时进入老年代失败)

2 整个内存占用达到了6gb,超出了平时的4gb,得出可能是出现了内存泄漏的问题。

3 通过jmap -dump 命令查看heap,再通过mat工具分析。
发现一个最大的对象占用了900M内存,显然有问题。

根据mat给出的说明,整个map集合占用了55%的空间

4 再看一下这个对象持有了哪些对象。

仔细查看后发现map持有一个大对象,它的大小没有超出我们预期,但是仔细看其他集合,没有发现所持对象有什么不对的地方。

但是仔细计算整个对象集合的大小发现,对象全都存在,但是大小比正常多了将近一倍,于是想到可能是持有了相同的对象。

5 搜索完集合后发现确实如此,原来是因为业务逻辑要求每天凌晨更新一次老对象,更新后老对象自动释放,但是我们的新引擎是要保存这些对象,以便做编译优化,不能及时释放老对象,导致大对象保存了两份。

实例分析2:jvm bug

问题:

1 淘宝某应用突然导致线上机器报警,Java内存使用验证,达到6gb,超过4gb,而且有几台运行一段时间后导致OOM,JVM退出。

2 观察重启后的机器,发现应用恢复正常,但是发现JVM进程占用的内存一直在增加,大体推断是有内存泄漏。

解决思路:

1 检查了一下最近一周代码更新没有发现内存泄漏的代码。

2 同时检查gc日志,发现 有问题的机器的full gc没有异常,甚至比平时的full gc次数少,cms gc也很少。

3 为了进一步确认gc是否正常,我们找出jvm的heap,用mat分析对文件,整个heap只有不到1g的空间。最大的对象和符合预期,所以不是jvm的堆内存有问题,但是既然堆占用的内存并不多,那为什么java进程占用这么多内存?

4 于是想到了可能是堆外内存的泄漏,也就是本地内存,JIT编译需要本地内存,jvm栈需要本地内存,JNI调用本地代码也需要本地内存存,NIO也会使用Direct buffer申请本地内存。

5 我们用到了direct buffer,于是怀疑它。因为上次引入了mina包,使用了direct buffer,但是为什么direct buffer会回收异常呢。

6 这是想到了一个jvm的bug。
如果使用 -XX:+DisableExplicitGC参数启动jvm,但是又用到了direct buffer,会导致system.gc失效,与此同时我们应用自己触发的gc次数很少,导致directbuffer没有机会被回收,
其分配的本地内存无法被释放。

7 解决办法就是出去 -XX:+DisableExplicitGC,换上-XX:+DisableGCInvokesConcurrent,使得外部的system.gc能够调用成功。

PS:降低每次Full gc的时间

通过ExplicitGCInvokesConcurrent选项,可以使用CMS收集器来触发Full gc

如果系统中大量使用了DirectByteBuffer,需要定期地对native堆做清理,清理时可以使用Full gc,也可以使用CMS,视QPS情况而定;

可以使用-Dsun.rmi.dgc.server.gcInterval=7200000与-Dsun.rmi.dgc.client.gcInterval=7200000 选项控制Full gc时间间隔;

可以使用-XX:DisableExplicitGC选项禁用显示调用Full gc;
如果希望Full gc有更少的停机时间,可以启用-XX:+ExplicitGCInvokesConcurrent或-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses选项。

实例分析3:另一个NIO direct buffer内存泄漏

问题:

1 系统运行20到30分钟后,系统swap区突增,直到swap区使用率达到100%,机器死机。

2 系统内存达到3.5g,已经超过了heap堆设置的上限,但是gc缺很少。

3 old区的空间也几乎没有变化

解决思路:

1 Java进程内存增长非常迅速,进行压测20分钟后就将2gb内存耗光,并且内存耗光后开始使用swap区,很快消耗了swap区的空间,最终导致机器死机,所以可以肯定是内存泄漏。

2 通过jstat分析jvm heap情况和gc统计信息,发现gc很少,full gc几乎没有,jvm堆内存被耗光的话,Full gc应该非常频繁,所以初步判断这次内存泄漏不在堆中

3 进一步排除是jvm堆内存问题,使用jmap dump出内存快照,通过mat分析内存情况。

4 可以得出要么是nio 本地内存泄漏,要么是native memory泄漏。
使用工具检测direct memory占用的空间大小。
发现并不是很多,所以怀疑是native memory出现泄漏。

5 使用oprofier热点分析工具分析当然系统执行的热点代码,如果是当前的native memory泄漏,那么肯定会出现分配内存的代码是热点的情况。

6 发现和预想的情况并不吻合,于是进行功能拆分,查看到底是哪个模块导致泄漏。拆分几次后发现时mina框架给varnish发送失败请求时导致的,而且发送的请求频率越高内存泄漏越严重。但是mina框架没有使用native memory的地方。

科普ps:Varnish
 Varnish与一般服务器软件类似,就是一个web缓存代理服务器,分为master(management)进程和child(worker,主要 做cache的工作)进程。master进程读入命令,进行一些初始化,然后fork并监控child进程。child进程分配若干线程进行工作,主要包 括一些管理线程和很多woker线程。

7 使用perftools分析jvm的native memory分配情况,通过perftools得到分析结果,没有发现问题。

8 又回到Java代码,发现代码中使用mina的一个类进行发送和序列化发送数据,使用的是direct buffer,于是改为使用jvm heap来存放数据。

9 按照这个思路,把所有可能出问题的direct buffer转变成Haep堆内存泄漏,如果这个代码有问题,必然会产生jvm heap暴涨。

10 修改代码后运行,果然内存暴涨,gc频繁,分析一下堆,发现堆空间都被socketsessionimpl的writerequestqueue队列所持有,这个对了是mina的写队列,也就是mina不能及时发送数据,导致堵在了这个队列里,所以还是mina导致了direct memory 泄漏。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
40 2
|
1月前
|
存储 算法 安全
【JVM】深入理解JVM对象内存分配方式
【JVM】深入理解JVM对象内存分配方式
26 0
|
1月前
|
Java 程序员
探讨JVM垃圾回收机制与内存泄漏
探讨JVM垃圾回收机制与内存泄漏
|
25天前
|
存储 缓存 Java
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
34 1
|
25天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
67 0
|
11天前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
44 0
|
15天前
|
存储 算法 安全
深度解析JVM世界:JVM内存分配
深度解析JVM世界:JVM内存分配
|
1月前
|
缓存 算法 编译器
C/C++编译器内存优化技术:内存优化关注程序对内存的访问和使用,以提高内存访问速度和减少内存占用。
C/C++编译器内存优化技术:内存优化关注程序对内存的访问和使用,以提高内存访问速度和减少内存占用。
39 0
|
1月前
|
存储 缓存 安全
[Java基础]——JVM内存模型
[Java基础]——JVM内存模型
|
1月前
|
存储 安全 Java
【JVM】Java堆 :深入理解内存中的对象世界
【JVM】Java堆 :深入理解内存中的对象世界
50 0

热门文章

最新文章