JVM内存区域
JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:Java堆、方法区、运行时常量池
1.程序计数器(线程私有)
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器;
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空;
每个线程都需要有一个独立的程序计数器,各条线程间的程序计数器互不影响,独立存储,称这类内存线程私有内存;
2.java虚拟机栈(线程私有)
- 线程私有区域,生命周期和线程相同
- 用途:描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
- 局部变量表:存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的在方法运行期间不会改变局部变量表的大小;
- 这个区域规定了两种异常状况:
- StackOverflowError异常:线程申请的栈深度大于虚拟机所允许的深度;
- OutOfMemoryError异常:虚拟机栈动态扩展时无法申请到足够的内存;
3.本地方法栈(线程私有)
- 本地方法栈和Java虚拟机栈非常相似;
- 本地方法栈为虚拟机使用到的Native方法服务,虚拟机栈为虚拟机执行Java方法服务;
4.Java堆(所有线程共享的一块内存区域)
-
Java堆基本是Java虚拟机所管理内存中最大的一块;
-
Java堆的唯一目的:存放对象实例,几乎所有的对象实例都在这里分配内存;
-
Java堆是垃圾收集器管理的主要区域;
-
Java堆可以处于物理上不连续的内存空间中;
-
这个区域规定了一个异常状况:
- OutOfMemoryError异常:在堆中已经没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出此异常
5.方法区(所有线程共享的一块内存区域)
- 目的:用于存储已被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码等数据;(jdk6之前)
(在虚拟机进行类加载的加载阶段,就是将类的二进制流所代表的静态存储结构转化为方法区的运行时数据结构) - 方法区可以选择不实现垃圾收集,这个区域的回收条件比较苛刻,但还是必须的,有些bug就是因为对方法区的回收没有做好;
- 这个区域规定了一个异常状况:
- OutOfMemoryError异常:当方法区无法满足内存分配需求 ;
备注:方法区是虚拟机规范中对运行时数据区划分的一个内存区域,不同的虚拟机厂商可以有不同的实现,而HotSpot虚拟机以永久代来实现方法区,所以方法区是一个规范,而永久代则是其中的一种实现方式。
Java6和6之前,常量池是存放在方法区中的;
Java7,将常量池存放到了堆中,常量池就相当于是在永久代中,所以永久代存放在堆中;
Java8之后,取消了整个永久代区域,取而代之的是元空间(metaSpace),没有再对常量池进行调整;
6.运行时常量池(方法区的一部分)
作用:用于存放编译器生成的字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中;
内存泄漏
**内存泄露:**堆内存中不再使用的对象,垃圾回收器无法从内存中删除他们,无法释放已申请的内存空间,这种情况会耗尽内存资源并降低系统性能,最终以Out of Memory终止;
内存泄露的症状:
- 应用程序长时间连续运行时性能严重下降;
- 应用程序中的OutOfMemoryError堆错误;
- 自发且奇怪的应用程序崩溃;
- 应用程序偶尔会耗尽连接对象。
Java语言中判断一个内存空间是否符合垃圾回收的标准:
1.给对象赋予了null,以后再没有使用过;
2.给对象赋予了新值,重新分配了内存空间;
Java语言中,内存泄漏的原因:
1.静态集合类:例如static HashMap 和 static Vector,由于它们的生命周期与程序一致,那么容器中的对象在程序结束前不能被释放;
- 解决办法:最大限度的减少静态变量的使用;单例模式时,依赖于延迟加载对象而不是立即加载方式;
2.各种连接:例如数据库连接、网络连接和IO连接等,当不再使用时,需调用close()方法来释放连接;每当创建连接或者打开流时,JVM都会为这些资源分配内存,如果没有关闭连接会导致持续占有内存,如果不处理会降低性能,甚至OOM;
- 解决办法:使用finally快关闭资源;关闭资源的代码不应该有异常;
3.变量不合理的作用域:一个变量定义的作用范围大于其适用范围(例如一个本可以定义为方法内局部变量的变量,却被定义为程序对象内的全局变量),并且使用后没有及时设为null;
4.如果我们读取一个很大的String对象,并调用了inter(),那么它会放到字符串池中位于PermGen中,只要应用程序运行该字符串就会保留,就会占用内存,可能造成OOM;
- 解决办法:增加PermGen大小 ;升级Java版本,JDK1.7之后字符串池转移到了堆中
5.使用ThreadLocal造成内存泄漏
- 使用ThreadLocal时,每个线程只要处于存活状态就可保留对其ThreadLocal变量副本的隐式调用,且保留自己的副本,使用不当会造成内存泄漏;
- 一旦线程不在存在,ThreadLocals就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程不会被垃圾回收器回收;所以使用到ThreadLocals来保留线程池中线程的变量副本时,ThreadLocals没有显式删除时,就会一直保留在内存中,不会被垃圾回收;
- 解决办法:不再使用ThreadLocal时,调用remove()方法,该方法删除了此变量的当前线程值;不要使用ThreadLocal.set(null),它只是查找与当前线程关联的Map并将键值对设置为null;
内存溢出
内存溢出:指程序在申请内存时,没有足够的内存空间;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出;
引起内存溢出的原因:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小;
内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
如何判断对象已死
1.引用计数法
给对象增加一个计数器,当有引用它时,计数器就加一,当引用失效时,计数器就减一;
JVM并没有采用这种方式;
原因:循环引用会导致引用计数法失效,循环引用就是A类中一个属性引用了B类对象,B类中一个属性引用了A类对象,这样一来,就算你把A类和B类的实例对象引用置为null,它们还是不会被回收;
2.可达性分析法
java用这种方法来判断是否需要回收对象
核心思想:通过一系列称为”GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为”引用链“,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达时),证明此对象不可用;
可作为GC Roots的对象有以下几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(Native方法)引用的对象
5.已启动且未停止的Java线程
可达性分析可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象 a 和 b 相互引用,只要从 GC Roots 出发无法到达 a 或者 b,那么可达性分析便不会将它们加入存活对象合集之中;
可达性分析法存在的问题:
在多线程环境下,其他线程可能会更新已经访问过的对象的引用,从而造成漏报(将引用设置为null)或者误报(将引用设置为未被访问过的对象)
- 漏报没有什么伤害,Java虚拟机至多损失了部分垃圾回收机会。即经过GC Roots链查找后,某个对象有到这个GC Roots的可达,不应该被回收,但现在GC Roots这个引用置空了,即不再引用那个对象了,那个对象则现在应该被回收,但这次并没有被判断为要被回收,这种情况就是这次回收没回收到它而已,即损失了一次回收,影响不大,下次总能回收它;
- 误报比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存;一旦从原引用访问已经被回收的对象,则有可能直接导致虚拟机崩溃;
垃圾回收算法
1.标记——清除法
将需要回收的对象进行标记,然后再标记完所有该回收的对象后进行统一回收;
不足:
- 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集;
- 标记和清除这两个过程效率较低
2.复制算法(新生代回收算法)
一分为二思想,每次将内存一分为二,每次只使用一半,当发生垃圾回收时,我们将这一半中的存活对象依次复制到另一半中,然后对这一半内存进行一次性清除;
以这种算法为思想,稍作一点改进,就是我们的新生代回收算法;
具体改进:
- 内存不按一比一进行划分,而是将内存(新生代内存)划分为一块较大的Eden(伊甸园)空间,和两块较小的survivor(幸存者)空间;
- 两块survivor空间,一块叫From区,一块叫To区;
- Eden区与survivor区的比例为8:1;
- Eden区与From区用来使用,此时可使用空间提升到了90%;
回收具体步骤:
- 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直复制到To区域,并将Eden和From区域清空。
- 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
- 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
这样一来,只有10%的内存留给我们的存活对象,这难道不会不够吗?
答:之所以叫新生代,这里的对象大都具备朝生夕死的特点,所以存活下来的很少;
3.标记——整理算法(老年代回收算法(Full GC))
在老年带中,这里的大多数对象都不会被回收,采用复制算法的话会带来大量的复制操作,所以采用上面的算法是显然不可行的;
这里我们采用标记整理算法,这种算法就是对标记清除法进行了一个改进:
在进行回收的时候,不是标记完后进行统一回收,而是让所有存活对象向一端移动,然后直接清理掉剩余的空间;
4.分代收集算法(Java虚拟机所采用)
分代收集算法其实就是结合了复制算法和标记整理算法;
当前JVM垃圾收集都是采用的这种算法 ,即根据对象的存活时间不同,将内存划分为几个区域;
一般是把Java堆分为新生代和老年代 。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用”标记-清理”或者”标记-整理”算法;
参考:https://www.cnblogs.com/dyh004/p/8296958.html