JVM内存分配和垃圾收集策略
java内存区域
1)程序计数器
因为java可以多线程并发执行,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器。记录正在执行的虚拟机字节码指令的地址。
这个区域不会产生内存溢出异常。
2)栈
java虚拟机栈
栈中主要存放了编译期可知的四类八种基本数据类型存(逻辑型 boolean、文本型char、整数型byte、short、int、float、浮点数型double、long),对象引用类型,和对象引用类型(reference)。
本地方法栈
本地方法栈和java虚拟机栈所发挥的作用非常相似,他们之前的区别是虚拟机栈为虚拟机执行java方法服务。而本地方法栈是为虚拟机使用到的Native方法服务。
-Xss参数可以设置本地方法栈的内存上限。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
程序中 递归如果找不到出口(或者递归过深)也会抛出此异常(不断的进行入栈操作)
3)堆
用于存放对象的实列
可通过-Xms和Xmx来扩展堆的大小。因为现在的收集器基本都采用分代收集算法,所以java堆中还可以细分:新生代和老年代
如果在堆中没有内存完成实列分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
-Xmx可设置堆内存的上限 -Xms 可设置内存初始化的大小(-X代表非标参数 m代表memory,x代表max)
堆的内存细节:https://www.cnblogs.com/ssskkk/p/13063234.html#_label0
4)方法区MethodArea是一个逻辑上的概念 (也叫非堆 ) 1.8以前是Perm Space实现 1.8以后是Meta Space实现
用于储存已被虚拟机加载的类的信息(编译后的class 包括动态代理产生的Class)、常量、静态变量、即时编译器编译后的代码等数据,Class在被Loader时就会被放到方法区中,加载类的类加载器本身也存在这里。
垃圾收集行为在这个区比较少出现的,有点类似它的名字,永久代(1.8以后叫元数据区)
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(后面会跟PermGen(MetaSpace) space字符串)异常。
PermSpace(<1.8)
字符串常量池位于PermSpace
FGC不会清理
大小启动的时候指定,不能变
MetaSpace(>=1.8)
字符串常量池位于堆 (一直创建字符串常量,观察堆和Mataspace) 观察内存变化
会触发FGC清理
不设定的话 最大就是物理内存
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。
之所以方法区的实现改成元空间是因为:
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
常量池 (方法区的一部分JDK1.8以后 已经移动到了堆中)之所以这么做是因为
字符串存在永久代中,容易出现性能问题和内存溢出。
判断对象是否需要回收
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加1;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能在被使用的。
这种方法实现简单,判定效率也很高,但是主流的java虚拟机里面没有采用这种方法,因为它很难解决循环引用的问题。如图:
ABC三个对象循环引用,但是没有其他引用指向它们。ABC一团垃圾,无法回收
根可达性分析算法(主流的商业语言所采用的算法)
这个算法的基本思路就是通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在JAVA语言中,可作为GC Roots的对象包括以下几种:
- 虚拟机栈中引用的对象(main()方法中用到的对象 比如在main方法中:List a = new ArrayList(); 这个a就是根对象)
- 方法区中类静态变量引用的对象(class load到内存之后 会立刻对静态变量初始化,所以静态变量能够访问的到的对象 叫根对象)
- 常量池引用的对象(这个class会用到其他class那些类的对象,这些事根对象)
- 本地方法栈中JNI引用的对象(C C++本地方法用到的类、对象)
JVM垃圾收集算法
标记——清除算法(Mark-Sweep)
你把它标出来,然后清掉 就这么简单
缺点:两遍扫描。产生大量不连续的内存碎片。以后程序运行过程中产生较大的对象时,无法找到足够连续的内存 而不得不提前触发另一次垃圾收集动作。
存活对象比较多的情况下,效率比较高
复制算法(Copying)
将内存按容量划分大小相等的两块,每次只使用一块。当这一块的内存用完了,就将还存活的对象复制到另外一块内存上面。
然后将该内存空间清空,交换两个内存的角色。
缺点:没有碎片,但将内存的使用率缩小了一半
适用于存活对象比较少的情况
标记——整理算法(Mark-Compact)
首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动。然后直接清理掉端边界以外的内存
缺点:没有碎片,扫描两次,需要移动对象,效率偏低
分代收集算法(Generational Collection)
我们jdk采用的应该就是分代收集算法。根据对象存活周期的不同将内存划分为几块,针对每一块使用不同的算法
新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要对少量存活的对象复制就可以进行收集。
老年代中(新生代中经过多次垃圾回收仍然存活的对象,例如缓存对象),因为对象存货率高,没有额外空间对它进行分配担保,就需要使用 标记——整理或者标记——清除算法。
演示堆内存溢出和解决办法
@Controller public class OutOfMemoryController { List<TestVO> list = new ArrayList<>(); @RequestMapping(value="/heapMemory",method=RequestMethod.GET) @ResponseBody public void heapMemory() { while(true) { list.add(new TestVO(UUID.randomUUID().toString())); } } }
因为new出来的对象是放在堆上面的,所以这样死循环的时候 会报错堆内存溢出(建议通过-Xmx和-Xms把堆内存设置的小一点 容易报错)
Exception in thread "ContainerBackgroundProcessor[StandardEngine[Catalina]]" java.lang.OutOfMemoryError: GC overhead limit exceeded
1.jps(虚拟机进程状况工具)查到进程的id
这个命令是不是很像linux的ps -ef
jsl命令只有在jdk的bin目录下才生效暂时不知为啥(可能是没path没配置好 不过不影响这篇博客的学习)
2.根据进程id使用jmap(java内存映像工具)命令导出内存
jmap(Memory Map for java)可以用于生成堆存储快照
上面打印了导出内存的路径和名称(默认当前目录)
3.使用MAT分析内存存储快照
https://www.cnblogs.com/onelikeone/p/7131793.html 这里写的比较简单 附上一篇MAT下载和使用教程
上面怀疑的OutOfMemoryController和我们测试的类名一样
4.也可以正则匹配搜索一下我们的类,进一步定位
思考
以上测试代码是演示了堆内存溢出,怎么演示非堆内存溢出(也就是方法区)
记得有个面试题是要快速填满方法区。 这个就要先看上面 方法区存放的是什么东西了
上文提到了方法区:用于储存已被虚拟机加载的类的信息(编译后的class)、常量、静态变量、即时编译器编译后的代码等数据
动态代理会不断的产生新的class对象,可以快速填满方法区。
基于JVisualVM的可视化监控
jdk的安装目录bin文件下有jvisualvm.exe可执行文件。可以用来监控虚拟机的性能和故障处理。
这款JDK自带用来监控内存的可视化工具的详细使用,更新中。。。。。。