jvm五大内存模型

  1. 程序计数器:
    1. 空间较小, 线程私有
    2. 当前线程的行号指示器, 字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码. 分支, 循环, 跳转, 异常处理, 线程恢复等基础功能都需要通过计数器来完成
    3. 执行方法时, 计数器记录虚拟机字节码指令的地址
    4. 如果是native(本地方法), 那么计数器为空
  2. java栈(虚拟机栈):
    1. 线程私有, 每个方法被执行的时候都会创建一个栈帧用于存储局部变量表, 操作栈, 动态链接, 方法出口等信息
    2. 每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程
    3. 线程请求的栈深度大于虚拟机允许的栈深度, 将抛出StackOverflowError; 虚拟机栈空间可以动态扩展, 当动态扩展是无法申请到足够的空间时, 抛出OutOfMemory异常
  3. 本地方法栈:

线程私有, 与虚拟机栈类似, 区别为本地方法栈为虚拟机使用到的native方法服务

  1. 堆:
    1. 线程共享, 用于分配空间存放实例, 如果堆中没有内存完成实例分配, 而且堆无法扩展将报OutOfMemoryError错误.
    2. 堆是GC所管理的主要区域, 又由于现在收集器常使用分代算法, java堆中还可以细分为新生代(伊甸区,幸存者区1,幸存者区2)和老年代.
      java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。
      即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)
      逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
  2. 方法区 | 元空间: (JDK1.7方法区,JDK1.8元空间)线程共享, 用于存储.class 文件数据结构(元数据)、已被虚拟机加载的类信息、常量、静态变量

    java类加载过程

    1. 加载

    1. 通过类的全限定名获取定义此类的二进制字节流文件
    2. 将二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构
    3. 该数据存储数据结构由虚拟机实现自行定义在内存中生成一个代表这个类的java.lang.Class对象

      2. 链接

    4. 验证

目的 : 确保Class文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机自身的安全
由于它对程序运行期没有影响,并不一定必要,可以考虑使用-Xverify:none

  1. 1. 格式验证 : 验证是否符合class文件格式规范
  2. 1. 元数据验证(语义验证) : 对字节码描述的信息进行语义分析, 确保描述的信息符合java语言规范的要求(父类和子类之间没有不兼容的一些方法声明)
  3. 1. 字节码验证 : 保证程序语义的合理性(比如类型转换上是否合理)
  4. 1. 符号引用验证 : 检查是否通过符号引用中描述的全限定名定位到指定类型上, 以及类成员信息的访问修饰符是否允许访问等(通常在解析阶段执行)
  1. 准备

为类变量(静态变量)分配内存 :
因为这里的变量是由方法区分配内存的, 所以仅包括类变量而不包括实例变量, 后者将会在对象实例化时随着对象一起分配在Java堆中
设置类变量默认值 :
基本类型为0, 引用类型为null, 常量值为代码所赋的值

  1. 解析

将常量池中的符号引用转为直接引用(得到类或者字段, 方法在内存中的指针或者偏移量, 以便直接调用该方法), 这个可以在初始化之后再执行
可以认为是一些静态绑定的会被解析, 动态绑定则只会在运行是进行解析, 静态绑定包括一些final方法(不可以重写) ,static方法(只会属于当前类), 构造器(不会被重写)

3. 初始化

对类变量初始化, 设置类变量初始值

类加载器

1.启动类加载器(bootstrap class loader)

由C++语言实现, 是虚拟机自身的一部分
负责加载存放在<JAVA_HOME>\lib\rt.jar目录中, 或被-Xbootclasspath参数所指定路径中的, 且可被虚拟机识别的类库, 无法被Java程序直接引用
如果自定义类加载器想要把加载请求委派给引导类加载器的话可直接用null代替

2.其他类加载器

由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader,可被Java程序直接引用。常见几种:
扩展类加载器(extensions class loade)

  1. 由 sun.misc.Launcher$ExtClassLoader 实现
  2. 负责加载<JAVA_HOME>\lib\ext目录中的、或者被java.ext.dirs系统变量所指定的路径中的所有类库
  3. 父加载器为null

应用程序类加载器(system class loader)

  1. 是默认的类加载器, 是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类加载器
  2. 由sun.misc.Launcher$App-ClassLoader实现
  3. 负责加载用户类路径上所指定的类库
  4. 父加载器为ExtClassLoader

自定义类加载器
如果以上类加载起不能满足需求,可自定义,需要注意的是:虽然数组类不通过类加载器创建而是由JVM直接创建的,但仍与类加载器有密切关系,因为数组类的元素类型最终还要靠类加载器去创建
这里的父加载器并非继承关系而是组合关系

双亲委派模型

双亲委托模型 :
当一个类加载器接收到一个类加载的任务时, 不会立即加载, 而是将加载任务委托给它的父类加载器去执行, 若父类加载器也存在父类加载器则继续向上委托, 直至委托给启动类加载器为止.如果父类加载器无法完成加载任务, 便将类的加载任务退回给下一级类加载器去执行加载.
(父加载器并不是指的继承的父子关系, 而是 parent 指向的组合关系)
使用双亲委托机制的好处是 :
能够有效确保一个类的全局唯一性, 当程序中出现多个限定名相同的类时, 类加载器在执行加载时, 始终只会加载其中的某一个类.
Java类随着它的类加载器一起具备了一种带有优先级的层次关系(即类的全限定名相同时, lib\rt.jar > lib\ext*.jar)
实现逻辑 :
实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中
先检查是否已经被加载过, 若没有加载则调用父类加载器的loadClass()方法, 若父加载器为空则默认使用启动类加载器作为父加载器. 如果父类加载器加载失败, 抛出ClassNotFoundException异常后, 再调用自己的findClass方法进行加载
如何打破

  1. 继承java.lang.ClassLoader类重写loadClass()方法
  2. 通过线程上下文类加载器的传递性, 让父类加载器中调用子类加载器的加载动作

    GC垃圾判定

  1. 引用计数法

引用计数法就是如果一个对象没有被任何引用指向, 则可视之为垃圾
这种方法的缺点就是不能检测到环的存在(A中有B,B中有A那么他们的引用计数值永远不会清零)。

  1. 根搜索算法

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
可以作为GC根节点的对象 :

  1. 虚拟机栈的栈帧的局部变量表所引用的对象
  2. 本地方法栈的JNI所引用的对象
  3. 元数据区的静态变量和常量所引用的对象

    GC常用算法

    1. Mark-Sweep(标记-清除算法):

  1. 对不能和Roots构成引用链的对象进行标记, 没有被标记的划入可回收集合, 进入需要执行finalize()方法的队列中
  2. 若在执行finalize()方法过程中, 与Roots对象又构成引用链, 则该对象实例会被移出可回收集合

    2. Copying(复制算法):

  3. 将可用内存区域划分为两份, 每次使用一个区域(区域大小非1:1)

  4. 在使用区域内存满了之后进行回收, 然后将仍然存活的对象逐一复制到另一部分内存上
  5. 对回收后的区域进行清除内存工作

    3. Mark-Compact(标记-整理算法):

  6. 在标记-清除算法的基础上不直接清除而是将存活对象往一侧移动, 然后清除边界外的内存区域

    4. Generational Collection(分代收集算法):内存分成三个区域: 新生代, 老年代, 元数据区(原是永久代)

  7. 新生代:

存放新生的对象, 特点 : 创建对象频繁, 但存活率低.
分成Eden,ServivorFrom,ServivorTo三个区域.

  1. Eden区: 每次创建对象分配空间的区域(但是如果对象所占空间太大, 也可能会在老年区分配)
  2. ServivorFrom区: 存放上次GC回收后,存活的对象
  3. ServivorTo区: 存放当前GC回收后,存活的对象(进入该区域的对象年龄会+1)

新生代GC回收算法: 复制算法 新生代GC方式:MinorGC

  1. 每次创建对象会在Eden中分配空间, 若Eden空间不足, 则调用MinorGC进行回收.
  2. 将Eden区和ServivorFrom区进行标记, 接着将存活对象复制到ServivorTo区上
  3. 存活的对象满足年龄条件或者ServivorTo空间不足时, 会将对象复制到老年代区域
  4. 清除Eden区和ServivorFrom区
  5. 交换ServivorFrom区域和ServivorTo区域
    1. 老年代 :

存放生命周期长的对象, 特点: 存活率高
老年代GC回收算法: 标记整理 老年代GC方式: MajorGC

  1. 1. 仅在调用MinorGC后, 有对象从新生代进入老年代时, 才会调用MajorGC
  2. 1. 该区域的GC回收, 通常会使用标记-整理
  3. 1. 该区域若空间不足以分给, 会抛出OutOfMemory错误

GC回收器

  1. Serial Collector (串行回收器)

它是最古老的垃圾收集器, “Serial”体现在其收集工作是单线程的, 并且在进行垃圾收集过程中, 会进入”Stop-The-World”状态.
单线程设计也意味着精简的 GC 实现, 无需维护复杂的数据结构, 初始化也简单, 所以一直是 Client 模式下 JVM 的默认选项
可以细分为两个回收器, 分别提供给新生代和老年代 :

  1. 在新生代 : 使用复制算法进行垃圾回收
  2. 在老年代 : 使用标记-整理算法进行垃圾回收
    1. 并行回收器(Paraller Collector )

又称throughput collector, 是jvm默认的回收器, Serial的升级版本, 多线程进行GC. 常见应用场景是配合老年代CMS GC工作

  1. 并发标记扫描收回器(CMS Collector)

管理新生代方式和Paralel和Serial GC相同。而是在老年代中并发处理。尽量减少停顿时间。
CMS收集器(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器,是基于“标记-清除”算法。
CMS的整个过程有4个步骤:
初始标记——并发标记——重新标记——并发清除
初始标记:CMS initial mark仅仅是标记一下GC Roots能直接关联的对象,速度快;需要stop the world
并发标记:CMS concurrent mark是进行GC Roots Tracing的过程;
重新标记:CMS remark是修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记长,比并发标记短;需要stop the world
并发清除:CMS concurrent sweep,清除算法会在收集结束时产生大量空间碎片,有可能导致没有足够大的连续空间来分配当前对象而触发一次Full GC。
缺点:1)CMS收集器对CPU资源非常敏感; 2)CMS收集器无法处理浮动垃圾,可能出现”Concurrent Mode Failure”失败(备选用Serial Old)而导致另一次Full GC的产生; 3)CMS是一款基于“标记-清除”算法的收集器,在收集结束后会产生大量空间碎片。
优点:并发收集;低停顿(并发低停顿收集器)
XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

  1. G1 垃圾回收器G1 (Garbage first )

是JDK7的新特性。jdk7以上都可以自主设置JVM GC类型。G1会将堆内存划分成相互独立的区块(默认1024),每一块都可能是不连续的 O(old区),Y(young区)区块(相对于CMS中O,Y区块是连续的)。G1会第一时间处理垃圾最多的区块。这个是garbage First的原因之一。
优点:
并发与并行、分代收集、空间整合、可预测的停顿
G1收集器运作的步骤:
初始标记——并发标记——最终标记——筛选回收

  • 初始标记:initial marking,标记一下GC Roots能直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,需要停顿线程,耗时短;
  • 并发标记:concurrent marking,从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时长,但可与用户程序并发执行;
  • 最终标记:final marking,修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,对象变化记录存在线程Remember Set Logs中,然后把这些数据合并到Remember Set中,该阶段停顿线程,但是可并行执行;
  • 筛选回收:live data counting and evacuation,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。

    Java中的四种引用

  1. 强引用如果一个对象具有强引用, 它就不会被垃圾回收器回收
  2. 软引用在使用软引用时, 如果内存的空间足够,软引用就能继续被使用
  3. 弱引用 弱引用对象,无论当前内存空间是否充足,都会将弱引用回收
  4. 虚引用在任何时候都可能被垃圾回收器回收

    final finally finalize 的区别

  5. final 是java中的关键字表示所修饰的变量/方法/类是最终的不可更改/重写/继承

  6. finally是在异常处理时提供finally块来执行通常与try/cath联合使用, 无论try中代码块是否发生异常都会执行finally中的语句
  7. finalize 是Object类的方法 在垃圾收集器删除对象之前对这个对象调用finalize方法

若有收获,就点个赞吧