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的对象包括以下几种:

  1. 虚拟机栈中引用的对象(main()方法中用到的对象 比如在main方法中:List a = new ArrayList(); 这个a就是根对象)
  2. 方法区中类静态变量引用的对象(class load到内存之后 会立刻对静态变量初始化,所以静态变量能够访问的到的对象 叫根对象)
  3. 常量池引用的对象(这个class会用到其他class那些类的对象,这些事根对象)
  4. 本地方法栈中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自带用来监控内存的可视化工具的详细使用,更新中。。。。。。

 

posted @ 2018-07-21 19:09  palapala  阅读(609)  评论(1编辑  收藏  举报