前言
在接手一个项目的时候,在运行的时候不断的报堆heap溢出,原来是这个项目的运行时好的,那就是说不是代码的问题,就是纯粹的虚拟机内存溢出了。后面解决的方式是扩大虚拟机的内存,这是治标不治本的方式。
但是也不是说不是代码问题,应该是说逻辑上的运行的没问题的,但是这里面就涉及到虚拟机的的调优问题了,但是目前这个技术不是我所掌握的,但是因此我被引出的学习虚拟的的兴趣。
jvm内存结构
-
程序计数器
- 程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。
- 为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。
- 如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
- 程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。
-
虚拟机栈
- 也是线程私有的,它的生命周期与线程相同
- 虚拟机栈描述的是Java方法执行的内存模型: 每个方法被执行时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息. 每个方法被调用至返回的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(VM提供了-Xss来指定线程的最大栈空间, 该参数也直接决定了函数调用的最大深度).
- 局部变量表存放了编译器可知的各种基本数据类型、对象引用、returnAddress类型。
-
本地方法栈
- Native Method Stack与虚拟机栈的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法。
- HotSpot直接把本地方法栈和虚拟机栈合二为一。本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
-
堆
- Java Heap是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例(Java虚拟机规范中的描述时:所有的对象实例以及数组都要在堆上分配)。
- 堆内存被所有线程共享
- Java堆是GC的主要区域,因此很多时候也被称为GC堆。
- Java堆分为年轻代(Young Generation)和老年代(Old Generation)(1.8之前还有持久代);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
- 存在OutOfMemoryError异常。
-
本地方法区
- 方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
- 存在OutOfMemoryError异常。
jvm内存泄漏和溢出
定义
- 内存泄漏:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
- 内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。
内存泄漏的场景
- 机器的连接数和关闭时间设置
- 长生命周期的对象持有短生命周期对象的引用:这是内存泄露最常见的场景,也是代码设计中经常出现的问题。例如:在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。
内存溢出的场景
-
堆内存溢出(outOfMemoryError:Java heap space)
- 在jvm规范中,堆中的内存是用来生成对象实例和数组的。
如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。
当生成新对象时,内存的申请过程如下:
a、jvm先尝试在eden区分配新建对象所需的内存;
b、如果内存大小足够,申请结束,否则下一步;
c、jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
d、Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
e、 当OLD区空间不够时,JVM会在OLD区进行full GC;
f、full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”: outOfMemoryError:java heap space
- 在jvm规范中,堆中的内存是用来生成对象实例和数组的。
-
方法区内存溢出(outOfMemoryError:permgem space)
- 在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。
所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出,一般该区发生内存溢出时的错误信息为: outOfMemoryError:permgem space
- 在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。
-
线程栈溢出(java.lang.StackOverflowError)
- 线程栈时线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。
一般线程栈溢出是由于递归太深或方法调用层级过多导致的。
发生栈溢出的错误信息为:java.lang.StackOverflowError
- 线程栈时线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。
堆的分代模型
-
JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
-
年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
-
堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
-
非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
-
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:
- MetaspaceSize :初始化元空间大小,控制发生GC阈值
- MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
观察:linux命令的使用
- jstat -heap pid 堆内存使用情况
- jmap -gc pid time gc情况
分代概念
- 当我们在Java里面使用new关键字创建一个对象的时候,JVM会在Eden区分配一块内存来存储这个对象。当Eden区的内存空间不足的时候,就会触发Minor GC(新生代GC)进行对象回收。那些因为存在引用关系而无法回收的对象,JVM会把他们转移到Survivor区。
- Survivor内部分为From区和To区,刚从Eden区转移过来的对象会分配到From区,每经历一次Young GC,这些没被回收的对象就会在From区和To区来回移动,每移动一次,这个对象的GC年龄就加1。默认情况下GC年龄达到15的时候,JVM就会把这个对象移动到Old generation。
jvm垃圾回收机制
- 垃圾回收一般指堆
- 堆主要分为三个部分:新生代、老年代和永久代(1.8后改成元空间)
- Java主要把堆分为新生代、老年代
- 永久代就是方法区,存放常量和静态变量等
- 分代之后,将GC划分了三种Minor GC、Major GC、Full GC
- Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。
- Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。
- Full GC是清理整个堆空间,包括年轻代和老年代
- Minor GC 触发条件一般为:
1、eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
2、 新创建的对象大小 > Eden所剩空间 - Major GC和Full GC 触发条件一般为:Major GC通常是跟full GC是等价的
1、每次晋升到老年代的对象平均大小>老年代剩余空间
2、MinorGC后存活的对象超过了老年代剩余空间
3、永久代空间不足
4、执行System.gc()
5、CMS GC异常
6、堆内存分配很大的对象
- 垃圾回收机制策略(也称为GC的算法)
- 引用计数算法。
- 大概就是对象每拥有一个引用,计数器就加一。
- 有一个循环引用的巨大缺陷
- 标记–清除算法
- 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。
- 分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
- 会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到),
- 该算法一般应用于老年代,因为老年代的对象生命周期比较长。
- 标记–整理算法
- 为了解决 标记–清除算法 会造成内存碎片的缺陷
- 复制算法
- 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
- 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差
- 复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。不过jvm在应用复制算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的jvm都是8:1。
- 引用计数算法。