天天看点

JVM的内存结构、垃圾回收算法和垃圾收集器的详解

运行时数据区(Run-Time Data Areas)

JVM的内存结构、垃圾回收算法和垃圾收集器的详解
  • 本地方法区:方法区和堆一样,也是被所有线程共享的内存区域。它存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码数据。
  • 虚拟机栈(JVM Stacks):每个Java虚拟机线程都有一个私有Java虚拟机栈,与线程同时创建。每个方法执行的同时都会创建一个栈,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到直至完成的过程,对应着一个栈在虚拟机中入栈到出栈的过程。
  • 本地方法栈:与虚拟机栈发挥的作用类似,他们之间的区别是:虚拟机栈是为虚拟机执行Java方法提供服务;本地方法栈是为虚拟机执行native方法服务。
  • 堆(Heap ):堆是所有线程之间共享的内存区域。堆是运行时数据区,从中分配所有类实例和数组的内存。堆是在虚拟机启动时创建的。
  • 程序计数器(PC Register):JVM支持多线程同时执行,每个线程都有自己的PC Register,线程正在执行的方法叫当前方法。如果该方法不是 native方法,则PC Register包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行该方法是native方法,则Java虚拟机PC Register的值未定义。
  • 运行时常量池:是方法区的一部分。Class文件除了版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的字面量和符号的引用,这部分内容将在类加载后进入方法区的运行时常量池存放。

Java的内存结构

JVM的内存结构、垃圾回收算法和垃圾收集器的详解

常用JVM配置参数

  • -Xms jvm初始的堆内存大小,默认为物理内存的1/64,等价于 -XX:InitialHeapSize=
  • -Xmx jvm最大的堆内存大小,默认为物理内存的1/4,等价于 -XX:MaxHeapSize=
  • -Xss 设置单个线程栈的大小,默认512k-1024k,等价于 -XX:ThreadStackSize
  • -Xmn 设置新生代的大小,默认为堆空间的1/3
  • -XX:MetaSpaceSize:设置元空间的大小,元空间本质和永久代类似,都是对JVM规范中本地方法区的实现。不过元空间与永久代最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受物理内存大小的限制。
  • -XX:SurvivorRatio:设置新生代中eden(survivorFrom)和S0/S1(survivorTo)的比例

    默认-XX:SurvivorRatio=8,Eden:S0:S1=8:0:0

    SurvivorRatio的值就是默认eden的比例占比,S0和S1占比相同

  • -XX:NewRatio:配置新生代和老年代在堆中空间大小的比例

    默认-XX:NewRatio=2,新生代占1,老年代占2,新生代占整个堆内存的1/3

    假如-XX:NewRatio=4,新生代占1,老年代占4,新生代占整个堆内存的1/5

    NewRatio就是设置老年代的占比值,剩下的1给新生代

  • -XX:MaxTunuringThreshold 设置垃圾的最大年龄,java8默认为15,如果设置值的话必须在0-15如果设置为0,则年轻代不经过Survivor区,直接进入年老代,对于老年代比较多的应用,可以提高效率如果设置的的比较大,则年轻代会在Survivor区进行多次复制,这样增加 对象在年轻代的存活时间,增加在年轻代即被回收的概率。
  • -XX:+PrintGCDetails 打印GC详细信息
  • -XX:+PrintGCTimeStamps 打印GC日志的发生时间
  • -XX:+PrintGCDateStamps 打印GC日志的发生的日期
  • -XX:+PrintHeapAtGC 每次GC前后打印堆信息
  • -XLoggc:/logs/gc.log 将GC日志以文件形式输出
  • -XX:+DisableExplicitGC 禁止手动GC:System.gc();
  • -XX:+HeapDumpOnOutOfMemoryError 当JVM发生OOM时,自动生成DUMP文件
  • -XX:HeapDumpPath=${目录} 生成DUMP文件的路径,也可以指定文件名称,例如:

    -XX:HeapDumpPath={目录}/java_heapdump.hprof。如果不指定文件名,默认为:java_pid_date_time_heapDump.hprof

垃圾回收算法

1. 什么是垃圾?

简单来说就是内存中不可能再被使用到的对象空间就是垃圾。

2 如何确定一个对象是垃圾?可以回收

  • 引用计数法:如果一个对象的引用为0,则可以回收。但是如果两个对象相互引用,则这两个对象永远不可回收,所以现在基本不使用该方法判断。
  • 枚举根节点,可达性分析,Java虚拟机采用的算法。基本思路就是通过一系列名为“GC Roots”的对象作为起始点,开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即,给一个集合的引用为根出发,通过引用关系遍历图,能被遍历到的就判定为存活,没有被遍历到的自然判定为死亡。

Java中可以作为GC Roots对象有:

  • 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中Native方法引用的对象。
  • 线程

3. 常用的垃圾回收算法

复制算法

java堆从GC角度还可以细分为:新生代(Eden区,From Survivor区和To Survivor区,占用1/3的堆空间)和老年代(占据2/3的堆空间)。

Minitor GC的过程(复制-清空-互换)

Eden、SurvivorFrom复制到SurvivorTo,年龄+1;

  • 首先,当Eden区满的时候,第一次触发GC,把还活着的对象拷贝到SuvivorFrom区,当Eden区满再次触发GC的时候会扫描Eden区和SurvivorFrom区,对这两个区域进行垃圾回收,经过这次回收还活着的对象,则直接复制到SuvivorTo区域(如果对象的年龄达到了老年代,则赋值到老年代),同时把这些对象的年龄+1;
  • 清空Eden和SurvivorFrom中的对象,也即复制之后交换,谁空谁是To;
  • SurvivorFrom和SurvivorTo互换
  • 最后,SurvivorFrom和SurvivorTo互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From区域和To区域复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15)
  • 最终如果还是存活,就存入老年代。
    优点:整体复制,没有产生内存碎片。
      缺点:浪费空间,大对象复制会耗时。
               
标记清除

先标记出要回收的对象,再统一回收这些要清除的对象。

优点:没有大面积的复制,节约内存空间
  缺点:产生内存碎片。
           
标记整理

标记要回收的对象,再次扫描,并往一端滑动存活的对象。

优点:没有内存碎片,可以使用bump.
  缺点:移动存活对象的成本,耗时间。
           

主流的三种算法各有优缺点,综合来看,使用分代收集,复制算法用于年轻代,标记清除,标记整理用于老年代。

GC垃圾回收算法和垃圾收集器的关系?

GC算法(引用计数,复制,标记整理,标记清除)是内存垃圾回收的方法论。垃圾收集器就是GC算法的落地实现。

因为目前为止还没有出现完美的垃圾回收器出现,更加没有万能的收集器,只有针对具体的应用最合适和垃圾回收器,进行分代收集。

4种主要的垃圾收集器
  • serial 串行垃圾回收器:他为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程所以不适合服务器环境。
  • paraller 并行垃圾回收器:多个垃圾收集线程并行工作,此时用户的线程是暂停的,适用于科学计算,大数据处理首台处理等弱交互的场景。
  • CMS 并发垃圾回收器:用户线程和垃圾回收线程同时执行(不一定是并行,有可能交替进行),不需要停顿用户线程互联网公司多使用他,对于响应有时间限制的场景。
  • G1垃圾回收器:将堆内存分割为不同的区域,然后并发的对其进行垃圾回收。
如何选用合适的垃圾收集器?

组合的选择:

1、单核的CPU或者小内存,单机程序

-XX:UseSerialGC

2、多CPU,需要最大吞吐量,如后台计算型应用

-XX:UseParallelGC 或者-XX:UseParallelOldGC

3、多CPU,追求最小停顿时间,最快速响应,如互联网应用

-XX:+UserConcMarkSweepGC

-XX:+ParNewGC

参考链接:JVM官方规范

继续阅读