Java内存区域和GC机制篇

时间:2022-01-11 20:07:50

Java内存区域和GC机制
一、目录
1.Java垃圾回收概括
2.Java内存区域
3.Java对象的访问方式
4.Java内存访问机制
5.Java GC 机制
6.Java垃圾收集器

二、Java垃圾回收概括
1.Java GC 介绍:
  a) Garbage Collection 垃圾收集、垃圾回收机制;
  b) Java中不需要编写内存回收和垃圾清理代码,也不需要考虑内存泄漏和溢出的问题;
  c) 因为在Java虚拟机中存在自动内存管理和垃圾清理机制;
  d) 该机制会对JVM(Java 虚拟机)中的内存进行标记,并确定哪些内存需要回收,根据回收策略,自动的回收内存,永不停息的保证JVM中的内存空间,防止出现内存泄漏和溢出问题;
  e) 说明:关于JVM(Java虚拟机)是指HotSpot虚拟机

2.Java GC 主要做以下三件事:
  a) 确定哪些内存需要回收
  b) 确定什么时刻执行垃圾回收机制
  c) 如何执行垃圾回收机制

三、Java GC机制学习
1.学习方向有以下四个:
  a) 内存是如何分配的;
  b) 如何保证内存不被错误回收(即哪些内存需要被回收);
  c) 在什么情况下执行GC以及执行GC的方式;
  d) 如何监控和优化GC机制

2.Java内存区域(Java运行时内存划分)
  a) 程序计数器(program counter register)
    1.程序计数器是一个较小的内存区域;
    2.用于指示当前线程所有执行的字节码执行到了第几行;
    3.每个程序计数器只用来记录一个线程的行号,所以他是"线程私有"
    4.注:
      I ) 如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;
      II) 如果正在执行的是一个本地方法,则计数器的值为undefined;
      III) 由于程序计数器只是记录当前指令地址,所以不存在内存溢出,所以,程序计数器是JVM内存区域中唯一一个没有定义outofmemoryerror的区域;

  b) 虚拟机栈(JVM stack)
    1.一个线程的每个方法在执行的同时,都会创建一个栈帧;
    2.栈帧中存储的有局部变量表、操作站、动态链接、方法出口等;
    3.当方法被调用时,栈帧在JVM栈中入栈,当方法执行完之后,栈帧出栈;
    4.虚拟机栈中定义了两种异常:
      I ) 栈溢出:如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError;
      II) 内存溢出:由于大多数Java虚拟机都能允许动态扩张虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足而抛出OutOfMemoryError;
    5.每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的;

  c) 本地方法栈
    1.本地方法栈在"作用"、"运行机制"、"异常类型"等方面都与"虚拟主机栈"相同;
    2.唯一的区别是:虚拟主机栈是执行Java方法的,而本地方法栈是用来执行本地(native)方法的;

  d) 堆区
    1.在JVM所管理的内存中,堆区是最大的一块;
    2.堆区也是Java GC机制所管理的主要内存区域;
    3.堆区由所有线程共享,在虚拟机启动时创建;
    4.堆区的存在是为了存储"对象实例";
    5.所有的对象都在堆区上分配内存(也有在"栈"上分配内存的)
    6.堆区的大小是可以动态扩展的;
    7.如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap sapce异常;

  e) 方法区
    1.在Java虚拟机规范中,将"方法区"作为一个逻辑部分来对待,但"方法区"并不是"堆";
    2.方法区在物理上也不需要连续的,可以选择固定大小或可扩展大小;
    3.可以选择是否执行垃圾收集;但方法区上执行垃圾收集很少,这就是为什么称方法区为"永久代"的原因;
    4.方法区上面的"""垃圾收集"""主要是针对常亮"""常量池"""的内存回收和对已加载类的卸载;
    5.在方法区上执行垃圾收集很困难,所以不考虑;
    6.在方法区上定义了OutofMemoryError:PermGen space异常,在内存不足时抛出异常;
    7.运行池常量:
      I ) 用于存储编译期常量(编译时就产生的字面常量、符号引用)
      II) 还可以存储"""运行时间内"""产生的常量,目的是为了维护一个常量池,如果调用的字符串"abc"已经在常量池中,则返回池中的字符串地址;否则新建一个常量加入常量池中,并返回地址;

  f) 直接内存
    1.直接内存并不是JVM管理的内存,它是JVM以外的机器内存;
    2.比如物理内存是4G,JVM占用了1G,则剩下的3G就是直接内存;
    3.由于直接内存受到本机器内存的限制,也有可能出现OutOfMemoryError的异常;

四、Java对象的访问方式
一个Java的引用访问涉及到3个内存区域:JVM栈、堆、方法区
  1.通过句柄访问
  2.通过直接指针访问
    a) reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。

五、内存分配机制
  1.这里所说的内存分配是指在"""堆"""上的分配,一般的,对象的内存分配都是在堆上进行;
  2.Java内存分配和回收的机制概括的说就是:
    a) 分代分配
    b) 分代回收
  3.对象根据存活的时间被分为:
    a) 年轻时代
    b) 年老时代
    c) 永久时代(也就是方法区)

  4.年轻时代:
    a) 对象被创建时,内存的分配首先发生在年轻时代(大数据可以直接创建在年老代);
    b) 大部分的对象在创建后很快就不能再使用,于是被年轻的GC机制清理掉;
    c) 年轻代上的内存分配:
      1.Eden 内存首次分配区
      2.survivor0/1 两个存活区
      3.绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
      4.当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
      5.此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;
      6.当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的);
      7.当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

    d) 从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空;
    e) 经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。
    f) 因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)
    g) 使用了两种技术加快内存分配:
      1.这两种技术分别是bump-the-pointer和TLAB;
      2.对于bump-the-pointer:
      由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对 象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;

  3.对于TLAB:
    TLAB技术是对于多线程而言的,将Eden区分为若干 段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存;

  4.年老代:
    a) 对象如果在年轻代存活了足够长的时间而没有被清理掉,则会被复制到老年代;
    b) 老年代的特点是空间更大,可以存放更多对象,发生GC的次数更少;
    c) 当年老代内存不足时,执行Full GC;
    d) 可以采用动态控制策略,动态调整Java堆中各个区域的大小以及进入年老代的年龄;
    e) 如果对象比较大,且年轻代空间不足,则大对象会被直接分配到老年代上(但是大对象容易触发GC,应该少用);

六、每个分代的收集方法(GC机制的基本算法是:分代收集)
1.年轻代
2.老年代
3.方法区(永久代)