《offer来了》第一章学习笔记

第一章 JVM

1.JVM运行机制

JVM定义:

​ 运行Java字节码的虚拟机。

Java运行过程:

  • Java源文件(.java)被编译器编译成字节码(.class)文件
  • JVM字节码编译成对应操作系统的机器码
  • 机器码调用对应系统的本地方法库执行相应方法

JVM包括:

  • 类加载子系统 Class Loader SubSystem
  • 运行时数据区 Runtime Data Area
  • 执行引擎
  • 本地接口库 Native Interface Library (本地接口库通过调用本地方法库与OS交互

上图中:

  • 类加载器子系统将.Class文件加载到JVM中
  • 运行时数据区存储JVM运行时产生的数据
  • 执行引擎包括
    • 即时编译器:将.Class编译成机器码
    • 垃圾回收器:回收不再使用的对象
  • 本地方法库调用OS本地方法库完成指令操作

2.JVM线程

JVM后台运行线程:

  • 虚拟机线程 JVM Thread
    • 在JVM到达安全点(SafePoint)时出现
  • 周期性任务线程
    • 通过 定时器 来调度 线程,实现周期性任务执行
  • GC线程
    • 用于垃圾回收
  • 编译器线程
    • 将.Class动态编译成机器码
  • 信号分发线程
    • 接收发送给JVM的信号并调用JVM

3.JVM内存区域

JVM内存分为:

  • 线程私有区域
    • 程序计数器
    • 虚拟机栈
    • 本地方法区
  • 线程共享区域
    • 方法区
  • 直接内存
v2-c9013168fa861b8a6a9e6dea5cdb9fb0_r

上图中:

  • 线程私有区域:
    • 生命周期与线程相同,同生共死。
  • 线程共享区域:
    • 与JVM同生共死。
  • 直接内存:
    • 又称堆外内存。
    • JDK的NIO就是基于堆外内存实现。
    • Java进程可以通过堆外内存技术避免在Java堆与Native来回复制数据带来资源浪费与性能损耗。
    • 因此在高并发场景广泛使用(Netty、Flink、HBase、Hadoop

3.1 程序计数器

程序计数器:

  • 用于存储当前线程执行的字节码行号指示器
  • 属于线程私有内存区域,唯一没有内存溢出(OOM)问题的区域

3.2.JVM栈

JVM栈:

  • 描述方法执行过程的内存模型
  • 在当前栈帧中存储局部变量表、操作数栈、动态链接、返回地址等信息

栈帧:

  • 存储部分运行数据及数据结构
  • 处理动态链接方法返回值异常分派
  • 记录方法执行过程。
  • 方法执行时JVM会创建对应的栈帧。
  • 方法执行和返回对应栈帧在JVM栈中的入栈与出栈。
v2-9e56c0fad8b3cdd32d55062beabcccfc_r

上图:

  • 线程1在CPU1执行
  • 线程2在CPU2执行
  • 线程N在等待获取CPU时间片
  • 每个方法的执行对应一个栈帧
  • 每个线程只能存在一个活动的栈帧

3.3.本地方法区

本地方法区与JVM栈类似

不同的是

  • JVM栈为Java方法服务
  • 本地方法为Native方法服务

3.4.堆

  • JVM运行产生的对象和数据存储在堆中
  • 堆是线程共享的内存区域
  • 是垃圾回收器进行垃圾回收的最主要区域
  • 由于现代JVM采用分代收集算法,因此堆从GC角度还可以分为:
    • 新生代
    • 老年代
    • 永久代

3.5.方法区

被称为永久代,存储存储常量、静态变量、类信息、机器码、运行时常量池等数据。

v2-e86686b6587c2e9240756a1ad0e3b634_r

4.JVM运行时内存

  • 也叫JVM堆
  • GC角度分为新生代(占1/3)、老年代(占2/3)和永久代(占很少)。新生代又分为3个区。
v2-e72f865b7df3c65f661d013b2434713a_r

4.1.新生代

新生代

  • 新建对象存放区域(大对象除外)
  • 由于JVM频繁创建对象,新生代会频繁GC
  • 新生代GC算法叫做复制算法

Eden区

新建对象首先存放区,如果是大对象,会放在老年代。

SurvivorFrom区

将上次GC幸存者作为这次GC扫描对象

SurvivorTo区

保留上次GC幸存者

新生代GC过程:

  • 将Eden区和SurvivorFrom区存活对象复制到SurvivorTo区中。如果对象年龄太大或SurvivorTo区内存不够或是大对象,则直接复制到老年代中。
  • 清空Eden区和SurvivorFrom区对象
  • 将SurvivorTo区与SurvivorFrom区互换,原先的SurvivorTo区成为下次GC的SurvivorFrom区

4.2.老年代

  • 存储长生命周期对象与大对象
  • 对象比较稳定,不会频繁GC
  • 老年代GC算法叫做标记清除算法
    • 首先扫描所有对象并标记存活对象,然后回收未标记对象。
  • 老年代GC时间长
  • 老年代没有内存可分配时,会抛出OOM

4.3.永久代

永久代

  • 意味内存永久保留区域
  • 主要存放Class和元数据
  • 运行时不会被GC,因此永久代内存随着Class文件增加而增加,加载过多Class文件会抛出OOM。比如Tomcat引用Jar文件过多导致无法启动
  • Java8中,永久代被元数据区取代。

永久代与元数据区区别

元数据区没有使用JVM内存,而是使用OS内存。因此元数据内存不再被JVM内存限制,只和OS内存有关

5.垃圾回收算法

1.5.1如何确定垃圾

通过引用计数法可达性分析来判断对象是否可回收。

v2-f20fad55b43c8e7830a6ea3e52a8209f_r

引用计数法

  • Java如果操作一个对象,就要获得对象的引用。
    • 为对象添加引用时,引用计数+1。
    • 减少一个引用时,引用计数-1。
    • 当引用计数为0,说明对象没有被引用,可被回收。
  • 引用计数法会导致循环引用问题,当两个对象相互引用时,就不能被回收。
v2-62c702cb87d1e6d0a715f3dbd10a337c_r

可达性分析

  • 为解决引用计数的循环引用问题而设计
  • 定义一些GC ROOTS对象,以这些GC ROOTS对象作为起点,向下搜索。
  • 如果一个对象和GC ROOTS没有可达路径,则称这个对象不可达。
  • 不可达对象被两次标记后回收。

1.5.2.Java中常用GC算法

标记清除法、复制法、标记整理法、分代收集法

v2-7c480c8eb5d34ead989cbd0ea33169c6_r

标记清除法

  • 基础GC算法
  • 分为标记和清除两阶段
  • 标记阶段标记,清除阶段回收
v2-c798e4e9cde77d5fa9061868d881ea8a_r

标记清除法的问题:

​ 如果回收小对象过多,会引起内存碎片化,导致大对象无法获取连续可用内存问题。

复制算法

为解决标记算法内存碎片问题而设计

将内存划分为相等内存区,即区域1和区域2。

新对象放在区域1中,区域1满了就将区域1对象标记,将标记后存活对象复制到区域2中。清空区域1。

v2-ddb01351fcfe88ecf1d4ccdbc938773d_r

复制算法问题:

​ 每次只用一般内存,浪费空间。

​ 区域1与区域2来回复制影响性能。

标记整理算法

  • 结合上面两个算法优点。

  • 标记阶段和标记清除算法一样。

  • 清除阶段和复制算法一样。将存活对象移到内存另一个区域。

v2-a0c6c5770f37fc7ffb335bc20679d0b4_r

分代收集算法

  • 上面3个算法都无法对所有类型对象回收(长生命对象、短生命对象、大对象、小对象)。

  • 因此针对不同对象,设计了分代收集算法。

  • 新生代存放新对象,数量多,生命短,每次GC都大量被回收。采用复制算法。

  • 老年代存放大对象,生命长,被GC数量少。采用标记清除算法。

6.Java中的4种引用类型

  • 强引用
    • 将一个对象赋值给另一个对象时。强引用一定会为可达性,因此不会被GC。强引用也是造成内存泄漏的主要原因。
  • 软引用
    • 通过SoftReference类实现。系统内存不足时会被回收。
  • 弱引用
    • 通过WeakReference类实现。GC过程中一定会被回收。
  • 虚引用
    • 通过PhantomReference类实现,虚引用和引用队列一起使用,用于追踪对象的垃圾回收状态。
v2-7194ba04a13b9ed14a8d85f9dbfa20e1_r

7.分代收集算法和分区收集算法

  • 分代收集算法
    • 新生代用复制算法
    • 老年代用标记整理算法
  • 分区收集算法
    • 将堆内存划分为连续大小不等的小区域,每个小区域单独运行与GC。
    • 好处是根据每个小区域灵活使用使用和释放内存。

8.垃圾收集器

Java堆内存分为新生代与老年代。新生代用复制算法进行GC。老年代用标记整理进行GC。

因此JVM为新生代和老年代提供了不同的GC器。

v2-e2db0bf3125dd4b70a1509ecccbd5f39_r

Serial:

​ 新生代,单线程,复制算法

ParNew:

​ 新生代,多线程,复制算法

Parallel Scavenge:

​ 新生代,多线程,复制算法

Serial Old:

​ 老年代,单线程,标记整理算法。配合Serial使用

v2-235b23e76d24a515fd9db1bf7f746fad_r

Parallel Old:

​ 老年代,多线程,标记整理算法。配合Parallel Scavenge使用

v2-c0331e45cba03aeca1ed98c2b3228504_r

CMS:

老年代,多线程,标记清除算法。

机制复杂:

​ 初始标记:只标记和GC Roots直接关联对象。需要暂停工作线程。

​ 并发标记:用用户线程一起工作,执行GC Roots跟踪标记过程

​ 重新标记:在并发标记过程中,用户线程运行导致对象状态变化,需要暂停工作线程并且重新标记。

​ 并发清除:和用户线程一起工作,执行清除GC Roots不可达对象任务。

v2-6ad166eefbb9474f72a63d4c78321a00_r

G1:

将堆内存划分为大小固定的独立区域,后台维护优先级表。优先回收垃圾最多区域。

相对于CMS改进:

​ 基于标记整理算法,不产生内存碎片

​ 在不牺牲吞吐量前提下实现最短停顿垃圾回收

9.网络编程

1.阻塞IO

读写数据时会发生阻塞。

工作流程:

​ 用户线程发出IO请求,内核检查数据是否就绪,用户线程阻塞等待数据就绪;

​ 数据就绪后,内核将数据复制到用户线程中,返回IO执行结果到用户线程,用户线程解除阻塞开始处理数据。

2.非阻塞IO

用户线程发出IO请求,无须阻塞就可以得到内核返回结果。内核返回false,没准备好,需要稍后发起IO操作。

一旦数据准备好,并且再次收到用户线程请求,内核就将数据复制到用户线程中并将结果通知用户线程。

在非阻塞IO中,用户线程需要不断询问内核数据是否就绪,在未就绪时可以处理其他任务。

3.多路复用IO

Java NIO模型基于此。会有一个Selector线程不断轮询多个Socket状态,在Socket有读写事件时,才通知用户线程进行IO操作。

多路复用IO可以一个线程管理多个Socket,并且只有真正Socket读写事件时才会使用操作系统的IO资源,节约资源。

4.信号驱动IO

用户线程发起IO操作,系统为该请求注册一个信号函数,然后用户线程处理其他任务;

当内核数据就绪时,系统发送信号给用户线程,用户线程会在信号函数中调用对应IO操作。

5.异步IO

用户线程不关心IO操作,只需要发起请求,接收到内核返回成功或失败信号时,说明IO操作完成,直接使用数据。

6.Java IO

5个类

​ File、OutputStream、InputStream、Writer、Reader

一个接口

​ Serializable

7.NIO

三个核心:Selector(选择器)、Channel(通道)、Buffer(缓存区)

Selector用于监听多个Channel事件

NIO与传统IO区别:

  1. IO面向流。NIO面向缓冲区,方便在缓存中对数据进行前后移动操作。
  2. 传统IO阻塞。NIO非阻塞。
v2-0077ff23c56cfebf55385ba7ae292157_r

Channel:

​ 和流Stream类似,不同的是Stream单向,Channel双向。

Buffer:

​ 一个容器,读写必须经过Buffer。

v2-307ff3e0baff5863b5f70782e15466f1_r

Selector:

​ 选择器,检测多个注册的Channel是否有IO事件。一个Selector管理多个Channel。

10.JVM类加载机制

10.1.类加载阶段

分为:加载、验证、准备、解析、初始化

v2-e25df7bcc452a5693802f58738fa7674_r

加载:

​ JVM读取Class文件,创建java.lang.Class对象

验证:

​ 只有通过验证的Class文件才能被JVM加载

准备:

​ 为方法区中的类变量分配内存空间并设置初始值

解析:

​ JVM将常量池中的符号引用替换为直接引用

初始化:

​ 通过执行类构造器的<client>方法为类初始化。

10.2.类加载器

JVM提供3个:启动类加载器、扩展类加载器、应用程序类加载器

v2-7c05db9506cd4f4ece4b5da9b001e526_r

启动类加载器:

​ 加载Java_HOME/lib目录中的类库

扩展类加载器:

​ 加载Java_HOME/lib/ext目录中的类库

应用程序类加载器:

​ 加载用户路径(classpath)中的类库

自定义的类加载器:

​ 用户通过继承java.lang.ClassLoader自定义类加载器

10.3.双亲委派机制

JVM通过双亲委派对类加载。

双亲委派机制:类收到加载请求后,自己不会加载,而是委托给父类加载。其父类接收到委派请求后,又会委托给他的父类,依此类推。这样所有类的加载请求都委托给了启动类加载器

若父类收到委派,发现自己也无法加载(通常由于该类的Class文件在父类的类加载路径不存在),会将该信息反馈给子类,向下委托。直到加载成功,否则报ClassNotFound。

双亲委派核心:保证类的唯一性与安全性。

双亲委派流程:

v2-67414f68fa129c9ce45534ca37ac1052_r

11.总结

image-20200421015946622

猜你喜欢

转载自blog.csdn.net/u012138605/article/details/105649200
今日推荐