Java开发基础技术(一)

1、JVM内存模型

在这里插入图片描述

1.1 程序计数器(Program Counter Register)

  • 记录每个线程执行位置,线程私有的。

1.2 java虚拟机栈(VM Stack)

  • 生命周期同线程同步,线程创建的同时创建一个虚拟机栈,其中存放栈帧,方法调用对应压栈出栈的过程。
  • 栈帧分为几个区域:局部变量表、操作数栈、动态链接、方法出口等。
  • 线程私有的。

1.3 本地方法栈(Native Method Stack)

  • 功能类似java虚拟机栈,调用本地方法服务(native)是使用,线程私有。

1.4 堆(Heap)

  • 在虚拟机启动时创建,用于对象创建实例(new Object)时分配内存空间。
  • 分为新生代(Young)、老生代(Old),GC垃圾回收主要目标区。
  • 非线程私有,线程共享。

1.5 方法区

  • 存放虚拟机加载的类元数据信息(元空间)、常量、静态变量等。
  • 也可被划分为堆的一个逻辑部分,习惯叫永久代(Perm),很少发生垃圾回收,但常量池回收、类型卸载等可能导致垃圾回收。
  • 非线程私有,线程共享。
  • Java8中已经没有方法区,取而代之的是元空间(Metaspace)。

1.6 直接内存

  • 非JVM规范的定义。
  • NIO中的ByteBuffer对象的方法allocateDirect(int capacity),可以使用Native函数库直接分配堆外内存,然后通过Java堆里面的DirectByteBuffer对象引用进行操作。

2、GC垃圾回收

2.1 Java中对象引用类型

  • 强引用:Java中普遍的引用关系,如Object o = new Object()。
  • 软引用:SoftReference类实现软引用,只在内存不足时,被GC处理。
  • 弱引用:WeekReference类实现弱引用,无论内存是否充足,下一次GC时处理。
  • 虚引用:PhantomReference类实现,无法通过虚引用获取实例对象,只用于GC时收到系统通知。

2.2 标记阶段算法

2.2.1、引用计数算法(Reference Counting Collector)

  • 每个对象都有一个计数器,对象每被引用一次,计数器+1,引用失效时,计数器-1,计数器为0时标记回收。
  • 优点:执行简单,判断效率高。
  • 缺点:无法检测出循环引用关系。

2.2.2、根搜索法(Tracing Collector)

  • 从所有GC Roots对象向下递归查找引用链,不可达对象标记回收。
  • 宣告一个对象死亡,至少要经过2次标记。
  • 第一次标记后,通过一个低优先级的线程执行对象的finalize()方法,这是最后一次逃脱机会(使对象重新关联到引用链)。

2.3 垃圾回收算法

2.3.1 标记-清除算法

  • 分为标记和清除2阶段,通过根搜索法先标记,后统一回收空间。
  • 优点:无需进行内存移动,仅对死亡对象进行处理。
  • 缺点:(1)效率低,需要额外的空闲列表来记录所有的空闲地址和大小。(2)产生大量内存碎片。

2.3.2 标记-整理算法

  • 标记阶段和“标记-清除”算法相同,回收阶段把所有的对象移动到同一端,然后把范围外的空间回收。
  • 优点:整理后没有内存碎片,分配新对象空间简单高效。
  • 缺点:整理过程中移动对象导致GC占用太多时间。

2.3.3 复制算法

  • 把内存分为大小相等的两部分,当A块内存用完,把所有存活对象复制到B块,然后直接回收A块内存。
  • 优点:1)标记和复制可同时进行;2)无内存碎片;
  • 缺点:浪费一半内存。
  • 适合短生存期对象(如:新生代)

2.3.4 分代收集

  • 现代虚拟机大多采用这种方式,根据对象生产周期,将堆分成新生代和老生代,新生代对象生存周期端,采用复制算法,老生代使用标记整理 或 标记清除。

2.3.5 Adaptive算法(Adaptive Collector)

  • 监测当前堆的使用情况,自动选择合适的算法。

2.4 Java垃圾回收器(GC)

2.4.1 串行垃圾回收器(Serial Garbage Collector)

  • 使用单线程执行垃圾回收,同时冻结所有的应用线程(Stop the World,STW)。
  • 不适合服务器环境,适合单CPU、暂停时间不敏感、简单的命令行程序。
  • 通过-XX:+UseSerialGC可以使用串行垃圾回收器。

2.4.2 并行垃圾回收器(Parallel Garbage Collector)

  • 使用多线程执行垃圾回收,同时冻结所有的应用线程。JVM默认使用。
  • 优点:使用多线程扫描和压缩堆。
  • 缺点:在minor和full GC时都会暂停应用。
  • 适合服务器环境,适合多CPU、暂停时间敏感。
  • 可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

2.4.3 并发标记扫描垃圾回收器(CMS Garbage Collector)

  • 使用多线程,标记并回收内存,特定情况冻结应用线程:
    1) 标记的对象在Tenured区。 2)垃圾回收的同时,堆内存数据被并发的改变。
  • 相比并行GC,并发使用更多的CPU来确保吞吐量。
  • CMS最大的问题是会产生严重的碎片化,只有在出发FullGC时才会进行碎片整理。
  • 对比并行GC,会占用更多的CPU。
  • 通过-XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

2.4.4 G1收集器

  • 内存大于4G时,G1优先考虑使用,它是JDK7u4引入的,G1把堆分成1M到32M的多个区域,使用多个后台线程来扫描,优先扫描最多垃圾的区域(垃圾优先Garbage First),在处理的同时会进行碎片整理。
  • 如果在后台线程完成扫描前堆内存耗光,才会进行STW收集。

2.5 堆内存分配

Java Heap

  • 对象在堆上分配,主要在Eden上分配,如启用线程本地分配缓存机制,在TLAB上先分配,少数情况也直接分配在老年代中。
  • 当Eden满时,发起一次MinorGC(新生代GC),把Eden和S1复制到S2,然后清理Eden和S1(S1和S2是相对的),如果S2不足以存放存活对象,则提前复制到老年代。
  • 大对象直接进入老年代,可通过-XX:PretenureSizeThreshold配置,大对象需要连续空间分配,过多的大对象导致频繁GC,应避免。
  • 长期存活对象进入老年代,经过多次MinorGC增加年龄,可通过-XX:MaxTenuringThreshold配置,默认为15岁。
  • JDK8中,方法区(永久代)被删除,取而代之的是类元数据区(Class Metadata)
    1)类元数据直接在本地内存分配,不在JVM中,理论上只受物理内存大小限制,也可通过-XX:MaxMetaspaceSize参数设置,默认无限制。
    2)部分数据转移到heap中,如字面量、静态变量

3、类的加载过程

定义:JVM把class文件读取到内存中,经过校验、解析转换、初始化,在方法区和堆上面分配内存,形成可以被JVM使用的类型的过程。
Java class

3.1 加载

  • 执行动作:
    1)类加载器通过类的全路径限定名读取类的二进制字节流。
    2)将字节流代表的类结构转化到 JVM方法区 中。
    3)在 JVM堆 上生成代表该类的java.lang.Class实例。
  • 类加载器
    可以使用jvm自带加载器,也可以自定义加载器,不同的加载器从不同地方读取字节流:jar包、class文件、网络流等。
    同一个类加载器加载后的同源类,才是同类,instanceof 返回 true.
  • 类加载的双亲委派模型
    类加载器优先使用父加载器来加载,如果没有才自己加载。这样可以保持java类型的一致性,优先使用JDK的类加载器
  • 4种类加载器
    1)启动类加载器(JVM bootstrap Loader),JVM的一部分,负责JAVA_HOME/lib下的类的加载
    2)扩展类加载器(Extension Loader),负责JAVA_HOME/lib/ext和java.ext.dir目录下类加载。
    3)应用系统类加载器(Application System Loader),负责加载用户类路径下的类库,如果没有使用自定义的加载器,这就是默认的类加载器了。
    4)自定义加载器

3.2 验证

  • 加载和验证是交叉执行的,验证内容主要包括:
    1)文件格式:符合class文件格式,才能在 方法区 分配内存。
    2)元数据:符合java语言规范。
    3)字节码:数据流和控制流分析验证。
    4)符号引用:符号引用转换为直接引用,对自身以外引用的可访问性验证。

3.3 准备

  • 在方法区给static类变量分配内存,并 零值初始化。

3.4 解析

  • 常量池内的 符号引用 替换为 直接引用。

3.5 初始化

  • 真正开始执行java代码,static静态变量赋值为代码设定的值。
  • 只有以下5中主动引用,才会触发初始化的发生,其他类的引用称为被动引用,不会触发初始化:
    1)new 对象。
    2)访问static字段和static方法。
    3)使用反射调用未初始化过的类。
    4)初始化子类,需优先初始化父类。
    5)启动main方法所在的类时,jvm优先初始化该类。
  • 继承情况下的初始化顺序
    1)父类的static变量和块,按出现顺序
    2)子类的static变量和块,按出现顺序
    3)父类构造函数
    4)子类构造函数

4、Java对象生命周期

4.1 创建

  • 分配空间,构造对象
  • 先父类后子类,对static成员初始化。
  • 父类成员变量按顺序初始化,递归调用父类构造方法。
  • 子类成员变量按顺序初始化,调用子类构造方法。

4.2 应用

  • 至少被一个强引用,并且在作用域内。

4.3 不可见

  • 程序不再持有对象的强引用,但这些引用还存在着,一般具体是程序的执行超出作用域了。

4.4 不可达

  • 不在被任何强引用
  • 可能还被某些JVM线程,或jni所有,这些特殊的强引用成为GC Root,容易导致内存泄漏,无法被回收。

4.5 回收

  • 对象不可达,GC回收阶段。
  • 如果重写了finazlie()方法,可能导致对象复活,尽量不要重写此方法。

4.6 终结

  • 对象执行完finazlie()方法后,仍处于不可达状态,进入终结阶段,等待GC回收。

4.7 空间重新分配

  • 空间重新被分配,对象彻底消失。

猜你喜欢

转载自blog.csdn.net/weixin_43102143/article/details/82748792