JVM 内存分配与垃圾回收

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013360790/article/details/89510941

一、JVM内存分配

Java虚拟机运行时数据区.jpg

1. 堆(Heap)

  • 线程共享区域,在虚拟机启动时创建;
  • 主要存放对象的实例,Java虚拟机规范中描述:所有的对象实例以及数组都要在堆上分配;
  • Java虚拟机规范,堆可以处于物理上不连续的空间,只要逻辑上连续就行;
  • 垃圾收集器基本采用分代收集法,所以可分为新生代和老年代;
  • 新生代(YoungGen)
    • 占堆大小的1/3,几乎所有的对象创建时分配的空间都在新生代中;
    • Minor GC就是作用在新生代中,采用的复制算法;新生代时GC最频繁的区域;
    • Eden空间占新生代的8/10,From Survivor和To Survivor各占1/10;
    • Eden和From中的对象经过一次Minor GC后,如果对象还活着并且To 空间能容纳下它,将会被复制到To空间中;然后再清理Eden和From空间;
    • 每经过一次Minor GC,对象年龄就会+1,默认年龄达到15时,便会复制到老年代中;特别大的对象也有可能直接分配到老年代中;
  • 老年代(OldGen)
    • 占堆大小的2/3,存放经过多次GC后存活的对象,或者是占用内存较大的对象;
    • Full GC是发生在老年代垃圾收集动作,大多数采用标记-清除算法
    • Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
    • 标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
  • 堆中没有内存分配实例,并且堆也无法再扩展了,将会抛出OutOfMemoryError异常;

2. 方法区(Method Area)

  • 线程共享区域;
  • 主要存放类信息、常量、静态变量、即时编译器编译后的代码,JDK1.7以前字符串常量也存放在方法区;
  • Java虚拟机规范,堆方法区限制非常宽松,内存可不连续并可扩展,并且可以不实现垃圾收集,因此也叫永久代;
  • 运行时常量池
    • 主要存放在编译期生成的各种字面量和符号引用;
    • 符号引用所翻译出来的直接应用也会存在常量池中;
    • 运行期间产生的新的常量也会存在常量池中,例如:String.intern();
    • 方法区内存不足时也将会抛出OutOfMemoryError异常;

3. 虚拟机栈(JVM Stacks)

  • 线程私有;
  • 栈桢是方法运行时的基础数据结构;方法的调用和执行完成,就对应一个栈桢,在虚拟机栈中入栈和出栈;
  • 局部变量表(Local Variable Table)(存局部变量)
  • 存放编译期已知的各种基本数据类型(boolean、byte、char、short、int、float、long、double);
    • 存放对象引用(reference 类型,可能是指向对象起始地址的引用指针,也可能是代表对象的句柄);
    • 存放returnAddress类型(指向一条字节码指令的地址);
  • 操作数栈(Operand Stack) 在编译的时候写入到Code属性的max_stacks数据项中;操作栈最大深度不能超过max_stacks;
  • 动态链接(Dynamic Linking)
    静态解析是指方法调用指令是以常量池中指向方法的符号引用作为参数,符号引用一部分在类加载阶段或者第一次使用时转化成直接引用;
    • 能被静态解析的方法叫非虚方法;能调用非虚方法的指令有invokestatic``````invokespcial
    • 能被invokestatic``````invokespcial方法调用字节指令的都可以在解析阶段确定唯一的调用版本,符合条件的方法有静态方法、私有方法、实例构造器、父类方法;
      动态链接指的是另一部分在运行期间转化直接引用;
    • 动态链接的分派调用过程也是多态的表现形式,其中静态分派主要表现为重写,而动态分派主要表现为重载;
    • invokevirtal只能调用虚方法,和final修饰的方法,但是final修饰方法由于不能重载所以final修饰的方法是非虚方法;
    • 虚方法主要表现在重写和重载;
  • 方法返回地址(Return Address)
    • 一种退出方法的方式是执行引擎遇到任意一个方法返回的字节码指令,也就是要么有返回值,要么没有返回值;这是正常退出;
    • 另一种退出方式是出现异常,不会有返回值;
    • 两种退出方式都会返回到方法调用的位置,并将可能有的返回值压栈;
  • 附加信息 指的是 比如调试相关的信息;
  • 若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  • 若虚拟机可以动态扩展(当前大部分Java虚拟机都可动态扩展,只不过Java虚拟机规范也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

4. 本地方法栈(Native Stacks)

  • 线程私有;
  • 基本和虚拟机栈功能一样,只是虚拟机栈是执行Java字节码,而本地方法栈是执行Native方法;

5. 程序计数器(Program Counter Register)

  • 线程私有;
  • 可以理解为字节码的行号指示器;
  • 不同虚拟机有不同实现,主要是想通过程序计数器来选择下一条要执行的指令以及分支、循环、跳转、异常处理、线程恢复等;

二、JVM垃圾收集器

判断对象是否存活

1. 引用计数法

  • 为对象添加一个引用计数;对象创建时为其分配一个变量,计数为1;
  • 每当有一个地方引用它,计数器就+1;当应用失效后计数器-1;
  • 计数器为0时,判定该对象可被回收
  • Java虚拟机没有选用此方法

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0

//举个例子
//这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将object1和object2赋值为null,
//也就是说object1和object2指向的对象已经不可能再被访问,
//但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们
public class ReferenceFindTest {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
          
        object1.object = object2;
        object2.object = object1;
          
        object1 = null;
        object2 = null;
    }
}

2. 可达性分析算法

可达性算法主要通过一系列的GC Roots对象为起点,从这GC Root向下搜索,所走过的路径称为引用链;如果一个对象到GC Root没有任何引用链,也就是不可达,则证明这个对象是不可用的;

真正要回收对象,至少要经过两次标记过程:

第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。
确定死亡:第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活

GC Roots.png

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表);
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(Native方法)引用的对象。

垃圾回收算法

1. 标记-清除算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收

2. 复制算法

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

3. 标记-整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题


回收方法区

永久代主要回收两部分内容:废弃的常量和无用的类;

1. 废弃的常量

  • 例如常量池中的子面量的回收为例,假如字符串常量"123",没有任何String对象引用该字符串常量,并且没有任何地方引用该字面量,那么这个"123"就可已被回收;

2. 无用的类

  • 该类的所有实例都已经被回收,堆中不存在任何该类的实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的Class没有任何地方被引用,也没有任何地方通过反射访问该Class;

三、GC Log

/**
 * 0.192:JVM启动到GC时间
 * GC (System.gc()):新生代触发GC操作
 * Full GC (System.gc():老年代触发GC操作
 * PSYoungGen[5243K->784K(76288K)]:PS表示 Parallel Scavenge并行垃圾回收器;GC前后新生代占用大小;GC后Eden空间为0,所以784k也表示Survivor大小;76288K 表示新生代占用总大小
 * ParOldGen:Par表示Parallel垃圾回收器;
 * Metaspace:元空间也就是方法区的GC情况
 * 5243K->792K(251392K):表示堆的GC前后占用大小和堆的总大小;
 * 0.0021529 secs:GC耗时
 * [Times: user=0.01 sys=0.00, real=0.00 secs] :CPU时间使用情况,User:用户模式垃圾收集消耗的CPU时间,即运行在JVM中的时间;Sys:垃圾收集器消耗系统态的时间;Real:垃圾收集器消耗的实际时间;
 */
 0.192: [GC (System.gc()) [PSYoungGen: 5243K->784K(76288K)] 5243K->792K(251392K), 0.0021529 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
 //老年代GC
 0.194: [Full GC (System.gc()) [PSYoungGen: 784K->0K(76288K)] [ParOldGen: 8K->680K(175104K)] 792K->680K(251392K), [Metaspace: 3132K->3132K(1056768K)], 0.0044673 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

 //堆使用情况
 Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
 eden space 65536K, 3% used [0x000000076ab00000,0x000000076aceb9e0,0x000000076eb00000)
 from space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
 to   space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
 ParOldGen       total 175104K, used 680K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
 object space 175104K, 0% used [0x00000006c0000000,0x00000006c00aa228,0x00000006cab00000)
 Metaspace       used 3191K, capacity 4564K, committed 4864K, reserved 1056768K
 class space    used 342K, capacity 388K, committed 512K, reserved 1048576K

四、对象头 Mark Word设计

可参考:

五、JVM配置参数

1. 堆和栈

  • 设置初始堆大小 -Xms如:-Xms256m
  • 设置最大堆大小 -Xmx如:-Xmx5120m
  • 设置新生代大小 -Xmn通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%
  • 设置堆栈大小 -XssJDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的
  • 设置新生代与老年代的比例 -XX:NewRatio如: –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
  • 设置新生代中 Eden 与 Survivor 的比值 -XX:SurvivorRatio 默认值为 8即, Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
  • 设置永久代(方法区)的初始大小 -XX:PermSize
  • 设置永久代(方法区)的最大值 -XX:MaxPermSize

2. GC收集器参数

  • 打印GC基本信息 -XX:+PrintGC
  • 打印 GC 详细信息 -XX:+PrintGCDetails
  • 打印 GC 时间 -XX:+PrintGCTimeStamps
  • DefNew:新生代,老年代都使用串行回收收集器 XX:+UseSerialGC
  • ParNew:新生代使用并行收集器,老年代使用串行回收收集器-XX:+UseParNewGC或 新生代使用并行收集器,老年代使用CMS -XX:+UseConcMarkSweepGC
  • PSYoungGen:新生代,老年代都使用并行回收收集器-XX:+UseParallelOldGC或 新生代使用并行回收收集器,老年代使用串行收集器 -XX:+UseParallelGC
  • 将日志输出到文件xx(默认位置为桌面)-Xloggc:gcc.log
  • garbage-first heap:是使用-XX:+UseG1GC(G1收集器)
  • 设置虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用 -XX:+HeapDumpOnOutOfMemoryError

猜你喜欢

转载自blog.csdn.net/u013360790/article/details/89510941