JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:Java堆、方法区、运行时常量池
1.程序计数器(线程私有)
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器;
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空;
每个线程都需要有一个独立的程序计数器,各条线程间的程序计数器互不影响,独立存储,称这类内存线程私有内存;
2.java虚拟机栈(线程私有)
3.本地方法栈(线程私有)
4.Java堆(所有线程共享的一块内存区域)
Java堆基本是Java虚拟机所管理内存中最大的一块;
Java堆的唯一目的:存放对象实例,几乎所有的对象实例都在这里分配内存;
Java堆是垃圾收集器管理的主要区域;
Java堆可以处于物理上不连续的内存空间中;
这个区域规定了一个异常状况:
5.方法区(所有线程共享的一块内存区域)
备注:方法区是虚拟机规范中对运行时数据区划分的一个内存区域,不同的虚拟机厂商可以有不同的实现,而HotSpot虚拟机以永久代来实现方法区,所以方法区是一个规范,而永久代则是其中的一种实现方式。
Java6和6之前,常量池是存放在方法区中的;
Java7,将常量池存放到了堆中,常量池就相当于是在永久代中,所以永久代存放在堆中;
Java8之后,取消了整个永久代区域,取而代之的是元空间(metaSpace),没有再对常量池进行调整;
6.运行时常量池(方法区的一部分)
作用:用于存放编译器生成的字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中;
**内存泄露:**堆内存中不再使用的对象,垃圾回收器无法从内存中删除他们,无法释放已申请的内存空间,这种情况会耗尽内存资源并降低系统性能,最终以Out of Memory终止;
内存泄露的症状:
Java语言中判断一个内存空间是否符合垃圾回收的标准:
1.给对象赋予了null,以后再没有使用过;
2.给对象赋予了新值,重新分配了内存空间;
Java语言中,内存泄漏的原因:
1.静态集合类:例如static HashMap 和 static Vector,由于它们的生命周期与程序一致,那么容器中的对象在程序结束前不能被释放;
2.各种连接:例如数据库连接、网络连接和IO连接等,当不再使用时,需调用close()方法来释放连接;每当创建连接或者打开流时,JVM都会为这些资源分配内存,如果没有关闭连接会导致持续占有内存,如果不处理会降低性能,甚至OOM;
3.变量不合理的作用域:一个变量定义的作用范围大于其适用范围(例如一个本可以定义为方法内局部变量的变量,却被定义为程序对象内的全局变量),并且使用后没有及时设为null;
4.如果我们读取一个很大的String对象,并调用了inter(),那么它会放到字符串池中位于PermGen中,只要应用程序运行该字符串就会保留,就会占用内存,可能造成OOM;
5.使用ThreadLocal造成内存泄漏
内存溢出:指程序在申请内存时,没有足够的内存空间;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出;
引起内存溢出的原因:
内存溢出的解决方案:
第一步,修改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)或者误报(将引用设置为未被访问过的对象)
1.标记——清除法
将需要回收的对象进行标记,然后再标记完所有该回收的对象后进行统一回收;
不足:
2.复制算法(新生代回收算法)
一分为二思想,每次将内存一分为二,每次只使用一半,当发生垃圾回收时,我们将这一半中的存活对象依次复制到另一半中,然后对这一半内存进行一次性清除;
以这种算法为思想,稍作一点改进,就是我们的新生代回收算法;
具体改进:
回收具体步骤:
这样一来,只有10%的内存留给我们的存活对象,这难道不会不够吗?
答:之所以叫新生代,这里的对象大都具备朝生夕死的特点,所以存活下来的很少;
3.标记——整理算法(老年代回收算法(Full GC))
在老年带中,这里的大多数对象都不会被回收,采用复制算法的话会带来大量的复制操作,所以采用上面的算法是显然不可行的;
这里我们采用标记整理算法,这种算法就是对标记清除法进行了一个改进:
在进行回收的时候,不是标记完后进行统一回收,而是让所有存活对象向一端移动,然后直接清理掉剩余的空间;
4.分代收集算法(Java虚拟机所采用)
分代收集算法其实就是结合了复制算法和标记整理算法;
当前JVM垃圾收集都是采用的这种算法 ,即根据对象的存活时间不同,将内存划分为几个区域;
一般是把Java堆分为新生代和老年代 。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用”标记-清理”或者”标记-整理”算法;
参考:https://www.cnblogs.com/dyh004/p/8296958.html