JVM 对象创建内存分配以及常见的OOM问题

走进java

因为写程序本身就是一个不断追求完美的过程

优点

摆脱硬件限制

​ 这个原理是为不同的硬件提供 不同的虚拟机,但是要求编译器提供相同的字节码,这个思想和JDBC如出一辙,向上提供接口,向下屏蔽差异,这对我们接口的设计要求还是比较高的,首先我们得统一出一套接口,这些接口再每个情况下都可以实现,然后这些接口又要足够的万能,程序员可以使用这些接口完成他们想操作的任何事情。但是,我们完全可以抽象出一个接口,来简化我们对接口的用运,没有哪个成功的开源框架可以离开接口的易用性

​ 而且,不得不提的是,接口在更新迭代的过程中,你要保证向下兼容,因为你的产品一旦上线,就会有人在使用,如果你在更新过后,被人发现你把某个函数删除了,或者接口的逻辑改了,那必然是不会用你的产品。就像MP一样,它可以承诺用户,mybatis可以无缝升级到MP。或者,你在版本迭代无法避免的情况下,你加上注解@Deprecated,同时,你的文档必须要指明,怎么用新的接口来代替

其他

​ 内容访问安全,指针不会轻易越界。实现了热点代码运行时编译和优化,可以让java能随着运行时间的增长而获得更高的性能

技术体系

JDK(Java Development Kit)

​ Kit的含义为成套工具,可以让你完成java开发的整套流程

JRE(Java Runtime Environment)

​ java类库中的API和java虚拟机,java的运行时环境

java历史

​ 没什么好记的,不过比看历史书有意思多了,同时,我还发现,我以为很多厉害的功能,在很早的版本就已经出现了

java虚拟机

Sun Classic

​ 第一个商用的java虚拟机,只能用纯解释器的方式来执行java代码,如果用了外挂的编译器,那就不能用解析器了。

​ 什么是解释器,什么是编译器,我想到老师说过,c语言是一种编译后执行的语言,而python是解释执行的语言,python不需要关注其他地方的代码,因此,它可以有ipynb这种一边敲代码,一边执行的运行方法,明显,你能感受到这种方法在运行的时候慢,“编译的时候快”(也许不需要什么编译,因为我在一个地方写错误的代码,它也不会检测出来,直到执行到那行代码才会报错)。

​ 那我们直接编译,把高速交给用户不好吗?那是你没有编译过10w行代码的项目,编译测试一次,速度就慢的离谱,10多分钟都是有可能出现的,所以java可以编译经常运行的热点代码,然后解释那些普通的代码

​ 基于Handle的垃圾回收,因为垃圾回收后对象可能会被移动位置,所以,Handle相当是一个稳定引用值,虽然没什么问题,但是架不住这些底层的代码被调用的次数多,所以会显得很慢

Exact VM

​ 准确的内存管理,它可以知道某块内存的数据是什么类型,而且能够分辨出它是内存还是引用类型,这些都是对垃圾回收算法有利的。这个时候它就已经有热点探测和编译器和解释器混合工作的模式了

HotSpot

​ 这个虚拟机已经重要到,我用标题来表示了

​ hotSpot:热点代码探测

热点代码探测

​ 通过执行计数器找出最具有编译价值的代码,然后通知即时编译器方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准即时编译栈上替换编译。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间(解释)与最佳执行性能(编译)中取得平衡,而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更复杂的代码优化技术,输出质量更高的本地代码。

JRockit

​ 一款专门为服务器硬件和服务端应用场景高度优化的虚拟机,由于专注于服务端应用,它可以不太关注于程序启动速度,因此JRockit内部不包含解释器实现,全部代码都靠即时编译器编译后执行。

IBM J9|Open J9

​ 模块化和通用化,它的源码比HotSpot写的更加适合我们去学习,同时,根据我的线上项目体验,它的内存占用比HotSpot会少很多

Liquid VM

​ 有的虚拟机还是跨平台的,它完成了线程调度,文件系统,网络支持之类的一些功能,这样的效率跟高。所以,我在想,跨平台的意义是否真的大吗?这是java的优点,但同时,它为了跨平台,也牺牲掉了一些性能。而我们编写的好多应用都是要运行在Linux服务器上面的,最多也就是在Window或Mac上编写代码测试

Zing

​ 低延迟,快速预热,而且他的C4收集器,可以轻易的支持TB级别java堆内存,可以说目前以我的见识,只见过一台TB内存级别的服务器,打算在这台服务器上测试高并发的项目。暂停时间维持在10ms之内,我为响应速度做优化是有经验的,那种快乐,真的好比于实现了一个复杂的功能。

​ Zing的ReadyNow !功能可以利用之前运行时收集到的性能监控数据,引导虚拟机在启动后快速达到稳定的高性能水平,减少启动后从解释执行到即时编译的等待时间。Zing自带的ZVision/ZVRobot功能可以方便用户监控Java虚拟机的运行状态,从找出代码热点到对象分配监控、锁竞争监控等。Zing能让普通用户无须了解垃圾收集等底层调优,就可以使得Java应用享有低延迟、快速预热、易于监控的功能,这是Zing的核心价值和卖点,很多Java应用都可以通过长期努力在应用、框架层面优化来提升性能,但使用Zing的话就可以把精力更多集中在业务方面。

Dalvik

​ Dalvik虚拟机并不是一个Java虚拟机,它没有遵循《Java虚拟机规范》,不能直接执行Java的Class文件,使用寄存器架构而不是Java虚拟机中常见的栈架构。但是它与Java却又有着千丝万缕的联系,它执行的DEX (Dalvik Executable)文件可以通过Class文件转化而来,使用Java语法编写应用程序,可以直接使用绝大部分的Java API等。

其他

​ 可见技术的发展离不开它给互联网带来的价值,但很多时候,也因为商业,技术的发展被停滞甚至抵制。

​ Meta 元:就相当于数据库的原数据,mybatis里面的MetaObject,很多时候,你会发现,理解了一个单词的含义,你竟然能对编程的理解更深一层,所以,我打算遇到不认识的时候,就查询一下单词的含义,而不是仅仅会使用它,我发现这个真的很重要,毕竟,别人在起名字的时候,也经历了大量的思考。

展望

Graal VM

​ 让所用的语言都共用一个虚拟机,甚至包括了C和C++,如果这项技术实现了,那我们可以自由的完成跨语言的调用。为什么要这样做呢,因为每种语言都有适合它的作用领域,比如python对应的人工智能,go对应的微服务,如果我们能利用不同的语言,编写一套适应各自领域的代码,然后他们之间的互相调用就像调用原生api一样方便。

​ 而这种令人兴奋的,及富有难度的项目,在高校的实验室进行,我不仅对此感到非常的羡慕,也许我去不了那样的顶级高校,但我想为自己的实验室也做出牛逼的开源项目

即使编译

​ 对需要长时间运行的应用来说,由于经过充分预热,热点代码会被HotSpot的探测机制准确定位捕获,并将其编译为物理硬件可直接执行的机器码,在这类应用中Java的运行效率很大程度上取决于即时编译器所输出的代码质量。
​ HotSpot虚拟机中含有两个即时编译器,分别是编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)以及编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2),通常它们会在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统。

​ Graal编译器是一个和C2类似,当发展前景好于C2的编译器

微服务时代

​ 在微服务时代,对系统的抗压能力要没有那么高,因为都被分散到集群处理了,但是同时也产生了一个问题,就是java启动时间比较长,需要很长时间的预热,更何况现在的ServerLess。

​ 所以!java准备了类共享机制,他甚至可以为用户的类做优化,你想一想,我们的spring boot在启动时候,加载了多少它自己的类,所以性能的提升必然是客观的。然后是垃圾回收,如果一个函数1s内调用完,但是你在这一秒内,进行了垃圾回收,那他的运行时间一定会超过1s,所以,我们在运行完这个函数的时候再一次性的回收我们的垃圾。

​ 提前编译,为什么java不使用这种技术,因为一旦使用了,就没有办法做到一次编译到处运行。而且很多技术的实现是很复杂的,比如Substrate VM会捕捉你程序的入口,已经从入口出发那些最可能执行的代码,然后其他把他们加载到内存中,在面对纯粹的文本,然后用编译原理的知识去分析,想想就觉得它的难度大的离谱

其他

​ java想要避免线程在核心态和用户态度切换开销,比如go语言的天然并发

​ 提供基本类型的泛型,你不需要用包装类型来支持泛型了,而且可以避免大量自动装箱和拆线带来的性能消耗

​ 不可变类型的支持,这个对并发性能的提高极为友好

​ JNI技术使得java可以调用C++的本地代码,但是呢,这个效率很低,性能开销也很大,Panama想让java代码和本地代码带来更好的调用和数据传输,这在人工智能时代python大量使用的情况下,是极为有用的,而且可以更加方便的整合高性能的c++代码

自己编译JDK

OracleJDK和OpenJDK的区别

​ 总之,他们极大部分的代码都是相同的,Oracle的好像更好一些,不过,那些不同的代码也与我们无关。

​ 学习到了一个开发习惯,我发现git上面的项目是不仅有一个分支的,我们在开发新功能或者修复bug的过程中,通常应该是最新的分支进行(注意,不话一定是主分支),等稳定之后,我们才会把它退给稳定的分支

编译环境

​ 要选择在linux或者mac上编译,这样简单,同时你要注意一点,就是你可以在Clion之类的编译器打开这个源码的文件

自动内存管理与垃圾回收

​ 虚拟机很少会出现内存显露和溢出,但如果真的发生了这种情况,那排查的难度肯定是要比c或c++大的,就像是使用了某个框架,如果框架出了问题,那你就得对框架有很深入的理解才能改变。又或者说,别人框架的代码你不好改变,它就像是一个虚拟机一样,而自己写的工具类你可以随时改,出了问题,自己debug就行

java内存区域

程序计数器(Program Counter Register)

​ 可以看作当前线程所执行的字节码行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

​ 由于Java虚拟机的多线程是通过线程轮流切换分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。

​ 所以,我们在理解并发的时候,可以认为java一瞬间只能执行某个线程其中的一条代码。因为执行的复杂性,所以多线程真不见的不单线程效率高,关键是看你的多线程怎么写

​ 如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。

Java虚拟机栈(Java Virtual Machine Stack)

线程私有,而且生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧 ( Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

​ 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型( boolean、byte、 char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

​ 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

​ StackOverflowError 就是线程请求的栈深度大于虚拟机涌入的深入,那如果我虚拟容量可以动态扩展呢,这个时候,如果扩展的时候也无法申请到足够的内存,那麻烦的问题就来了,OutOfMemoryError。不过,因为HotSpot不会动态扩展,OOM就是指的这个,但是并不代码他不会出现这种异常,当你的内存申请失败就会报这种异常

本地方法栈(Native Method Stacks)

​ 与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

Java堆(Heap)

​ 是虚拟机管理内存最大的一块,是被创建的所有线程共享的一块内存区域。它在虚拟机启动的时候被创建,几乎所有的对象和数组都在这里被分配内存。它也是javaGC管理的区域,GC(Garbage Collected)

​ 如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区( Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

方法区(Method Area)

​ 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息常量(final)、静态变量(static)、即时编译器编译后的代码缓存等数据。

​ 因为数据回收的条件比较苛刻,所以很容易出现内存OOM的问题

运行时常量池(Runtime Constant Pool)

​ 它是方法区的一部分,Class文件中除了有类的版本字段方法接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

​ 在运行的时候,也能补充新的常量,所以叫运行时常量池。比如intern方法

​ 在调用”ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。

直接内存(Direct Memory)

​ NIO (NewInput/Output)类,引入了一种基于通道

( Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

​ 但是你要注意不要让这块内存加上你的java内存超过实际了实际的物理内存区域

对象的创建

​ 当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载解析初始化过。如果没有,那必须先执行相应的类加载过程。

​ 也就是说,第一次碰到是会有检查的。当一个类加载检查通过后,接下来就是为他分配内存了。那他需要多大的内存呢。你放心,这个在类加载的时候就已经被完全的确定好了。那为什么叫分配内存呢,因为这个过程就相当于把一块确定大小的内存从java堆中划分出来。

指针碰撞(Bump The Pointer)

​ 假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。

空闲列表(Free List)

​ 但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为。

​ 选用上面的哪种方式明显是要堆内存是否规整,而且这个又取决于所采用的垃圾回收算法是否带有压缩整理(Compact)的能力来决定的。明显第一种是要好一些的,但是没有办法,你可以很明显的看出来,这样对垃圾回收来说,是有难度的,因为你不仅要清除内存,还要整理内存。所以很多时候,编辑是一个取舍的问题。我们只能权衡特定的情况下,谁更完美。

​ 但是呢,你有没有想过,在java这种语言中,对象的创建是非常频繁的,同时,有没有可能出现两个对象同时被创建,指针指的步长混在一起的情况呢。

对象分配在多线程中的解决方案

​ 一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性。

​ 就比如我一开始指针是5,我的大小是4,分配完成后是9,那我分配完就看看,是不是9,如果是9,那就差不多,否则就说明失败了,我再重新试一试。我现在不是很清楚,虚拟机是怎么解决这个ABA问题的,是否也会有一个version。但是这个长度是不确定的,我也不清楚该怎么办。你根本不知道把version放在哪里?


​ 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

​ 上面这种方法是避免了多线程共享资源的问题,所以我们在同步操作一个集合的时候,能不能学习这种方法,给每个线程单独处理的空间,如果不够了再另说。这种没有锁的并发才是我们真正想要的高效。

属性赋"零"值

​ 当内存分配结束后,会把对象的属性初始化成值,但是你要注意一点,我们常用的引用类型,都会被设置为null,而我最常出现的问题的就是,集合类型的处理,他是null,不是一个空集合,在添加的时候,是会出错的。

必要的设置

​ 接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。

​ 这些信息存放在对象的对象头(ObjectHeader)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

​ 从虚拟机的角度来看,一个新的对象就已经产生了。但是从java程序的角度来看,对象的创建才刚刚开始,就比如构造函数。

对象的内存布局

对象头(Header)

​ 第一类是用于存储对象自身的运行时数据,如**哈希码(HashCode) **、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

​ 原来哈希码和线程锁是在这里存储的!

​ 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”。但,对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。


​ 另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

​ 给你一个object,就可以通过反射或instence of确定正确的类型,所以对于你来说是object,而且对于虚拟机他就是实际的类型。

​ 并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。

​ 此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。(就是为了推断内存的大小

实例数据(Instance Data)

​ 对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

​ 这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、
bytes/booleans、oops (Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。

​ Compact:袖珍的

​ 所以说,继承从这个角度来看,和复制父类的代码到之类也没有什么区别,总之,如果你只是简单的使用一个对象,那从实现的效果来看,确实没有什么区别

填充对齐

​ 对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

​ 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象的访问定位

​ 访问对象肯定是用我们的引用类型reference数据来访问的。

句柄访问

​ Java堆中会划分一块内存在作为句柄池,那句柄中有什么呢?它里面包含了对象的实例数据和类型数据各自具体的地址信息。

​ 这里要注意两点,句柄仅仅是地址的指针,他被java栈本地变量表reference(这个reference和int,short这种基本类型是同级的)来指向,然后句柄中指向对象实例数据的指针指向了和句柄所以都java堆中的实例池。他两个都在java堆中。然后执行对象类型的指针,指的是方法区。

直接指针访问

​ 和句柄那种间接访问方式不同的是,它自己直接访问了数据示例。但同时,你得想清楚,如何在java对象的内存布局,就是我们刚刚讲的,里面放置访问类型数据的相关信息。但,你要是只是访问这个对象的值,那它的类型如何其实完全不重要。

​ 那句柄访问有什么好处呢?当对象被移动的时候,比如我们常见的垃圾回收,就只需要改句柄中的值,就相当于多加了一层

OOM异常解决

-Xms 堆内存最小值 -Xmx 堆内存最大值

​ 将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展)

-XX:+HeapDumpOnOutOf-MemoryError

​ 通过参数-XX:+HeapDumpOnOutOf-MemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析。

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

怎么在idea中设置

​ 点击启动应用绿色小箭头左边的,选中我们要设置的项目,如果你在里面没有发现VM参数,那就看看有没有高级配置

快照结果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14844.hprof ...
Heap dump file created [31553372 bytes in 0.153 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at JVM.main(JVM.java:9)

如何解决

​ 先要分清楚,这个是内存泄露(Memory Leak)还是内存溢出(Memory Overflow)

​ 如果是内存泄露,可以通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是怎么通过引用路径,与哪些GC Roots相关联,才导致垃圾回收器无法回收它们。从这里,你也可以看出,什么叫内存泄露,就是说,垃圾回收器无法回收了。

​ 如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。简单而言,就是内存扩容。

​ 再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。这就是那种对象特别特别大,而且存活的时间还特别长那种。

-Xss 栈容量 -Xoss 本地方法栈(HotSpot不起作用)

​ 对Hotspot来说,它是不区分虚拟机栈和本地方法栈的。

​ 两种情况,方法调用,比如递归调用太多,或者你变量定义的太多了,不过定义的太多,也确实牛逼,spring那么多层代码调用也不会出现栈的问题。那,互联网编程中最常见的,并发呢?

​ 我们可以通过创建特别多的线程来导致OOM出现,如果我们给一个线程分配的可用内存比较大,因为总内存的数量是有限的,所以我们能创建的线程就少,所以!我在这里发现了一个调优的方法,为了让我们的程序能够创建更多的线程,我可以把-Xss调整到一个线程所占用的可能的最大内存。

​ 关于函数栈调用,一般情况下1000-2000是完全没有问题的,而你自己写的可以也就4-10这种的调用栈,spring估计也不会超过200,所以你不需要考虑这种问题。同时我们也可以减少最大堆内存和栈容量来换取更多的线程。

方法区和运行时常量池溢出

​ 运行时常量池是方法区的一部分。

-XX:PermSize=6M -XX:MaxPermSize=6M

​ 常量池,永久代,就是在程序出现之后,永远不会消亡的变量

​ permanent 这个单词是永恒的意思

​ 这里我学习到了一个让字符串变成常量池内的方法,intern。看来我对这个类的了解还是太少了。jdk7之后的话,字符串常量在堆内存中,因为字符串实在太多了,这个我觉得很合理。

再看方法区

​ 方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

​ 你这样看到的话,方法区和反射的关系就特别的大

​ 比如我们的spring会创建大量的代理对象,你创建的类,在aop中几乎做了翻倍的处理。

永久代->元空间

​ JDK8以后,永久代退出了历史舞台

​ -XX: MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
​ -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize (如果设置了的话)的情况下,适当提高该值。
​ -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。

​ -XX: Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

​ 所以说,元空间和永久代的区别是什么呢,作者好像没说。

​ 我查了一些资料,首先是关于字符串常量,在元空间中,字符串常量在堆内存中,然后是垃圾回收,因为方法区的垃圾回收比较苛刻。最后是类信息的大小不好确定,你应该还记得对象内部局部的补齐吧。

​ 然后呢,我觉得最重要的改变就是,元空间用的是本地内存,然后上面的那些参数可以限制一下你在本地空间的大小。

本地直接内存溢出

​ -XX:MaxDirectMemorySize 如果不指定,那就和-Xmx一样

​ 这个一般是NIO的时候会出现的异常,这样的异常你不容易在heap里面捕获到

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值