JVM Java虚拟机学习笔记:jvm内存模型,垃圾回收,引用类型,类加载

Java 虚拟机

概念: JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

运行过程:
Java 源文件 --> 编译器 --> 字节码文件 --> JVM解释器 --> 机器码

Java 的跨平台性: 每一种平台的解释器不同, 但实现的虚拟机是相同的, 当一个程序开始运行, 虚拟机就开始实例化了, 多个程序启动就会存在多个虚拟机实例.

线程

JVM允许一个应用并发执行多个线程, hotspot Jvm 中的java线程和原生操作系统线程有直接映射关系. 但本地存储,缓冲区分配,同步对象,栈,程序计数器等准备号了, 就会创建一个操作系统原生线程.

JVM后台线程:

  • 虚拟机线程: 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM位于安全点。这些操作的类型有:stop-theworld 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
  • 周期性任务线程: 负责定时器时间, 用来调度周期性操作执行
  • GC线程: 在运行时将字节码动态编译成本地平台相关的机器码
  • 信号分发线程: 接受发送到jvm的信号,并调用适当的jvm方法处理

线程和进程的区别: (1) 进程是操作系统资源分配和调度的基本单位, 线程是CPU调度和分派的基本单位; (2) 进程是线程的容器, 线程是进程的一个实体, 线程不能独立执行, 一个进程至少有一个线程; (3) 进程在执行过程中拥有独立的内存单元, 而一个进程的多个线程共享内存;

JVM 内存模型

线程私有区域: 虚拟机栈和本地方法栈、程序计数器
线程共享区域: 方法区 和 堆

  • 程序计数器: 是当前线程所执行的虚拟机字节码的地址的指示器, 如果是本地方法, 则为空; 内存区域唯一一个没有规定OOM的区域.
  • 虚拟机栈: 描述java方法执行的内存模型, 每个方法在执行时都会创建一个栈帧, 用于存储局部变量表,操作数栈,动态链接,返回地址等信息.
  • 本地方法栈: 为 Native 方法服务, 在 HotSpot JVM 中, 与虚拟机栈合二为一.
  • 方法区: 用于存储被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等.
    • 运行时常量池: 方法区的一部分, Class 文件中处理类的版本,字段,方法,接口描述信息外, 还有一项常量池. 用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  • :
    • 新生代: 用来存放新生的对象, 1/3的堆空间, 由于频繁创建新对象, 所以会频繁触发 Minor GC
      • Eden: 新对象出生地, 大对象则直接分配到老年代. Eden区内存不够时就触发 GC.
      • ServivorFrom: 上一次的幸存者, 这次被扫描
      • ServivorTo: 保留这次 Minor GC过程中的幸存者
    • 老年代: 主要存放程序中声明周期较长的内存对象, 或大对象.
    • 永久代: 内存的永久保存区, 主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理. 容易抛出OOM异常.

java8与元数据: 在java8中, 永久代被移除, 使用元数据区(元空间)代替. 元空间使用本地内存. 类的元数据放入native memeory中, 字符串和类的静态变量放入java堆中.

元数据: 描述数据的数据, 例如在java代码中吗添加到方法/字段/类/包上的额外信息, 如注解

注: minor GC: 新生代的垃圾回收, major Gc 老年代的垃圾回收, full GC 同时回收新生代/老年代/永久代

新生代的垃圾回收

  1. eden/servivorFrom 复制到 servivorTo, 年龄+1, 到达老年标准则复制到老年代(年龄15)
  2. 清空 eden/servivorFrom
  3. servivorFrom和servivorTo 互换, (只是身份互换)

老年代的垃圾回收
老年代对象比较稳定, 在major GC之前, 一般都先进行了一次minor GC, 新进的对象发现空间不够才触发.
使用标记清除算法, 先扫描所有老年代, 标出存活的对象, 然后回收无标记的对象.

垃圾回收

确定垃圾的方式

  • 引用计数法: 一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。存在循环引用的问题.
  • 可达性分析:通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

垃圾回收算法:

  • 标记清除算法: 标记阶段标记出所有需要回收的对象, 清除阶段回收被标记的对象所占用的空间. 缺点: 导致大量内存碎片
  • 复制算法: 按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉. 缺点:内存利用率低, 却存活对象较多时, 复制效率也低.
  • 标记整理算法: 标记阶段和标记清除算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
  • 分代收集算法: 根据对象存活的不同生命周期将内催划分为不同的域, 老年代每次只有少量对象需回收, 新生代每次有大量垃圾回收. 不同区域采用不同的算法.

java四种引用类型

  • 强引用: 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到, JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
  • 软引用: 需要用SoftReference类实现, 对于只有软饮用的对象, 只有系统内存不足时才会被回收, 通常用在内存敏感的程序中.
  • 弱引用: WeakReference类实现, 只要垃圾回收机制一运行, 不管jvm空间是否足够, 总会被回收.
  • 虚引用: PhantomReference类实现, 必须和引用队列联合使用, 主要用于跟踪对象被垃圾回收的状态.

GC 垃圾收集器

新生代收集器: serial, ParNew, Parallel Scavenge, G1
老年代收集器: CMS, Serial Old, Parallel Old, G1

  • Serial 垃圾收集器, (单线程, 复制算法)
    需要暂停所有其他的工作线程, 简单高效
    运行在client模式下默认的新生代垃圾收集器

  • ParNew 垃圾收集器, (Serial + 多线程)
    Serial 的多线程版本, 默认和cpu数目相同的线程, 使用-XX:ParallelGCThreads限定

  • Parallel Scavenge 收集器, (多线程复制算法, 高效)
    重点关注程序达到一个可控制的吞吐量(=运行代码时间/(运行代码时间+垃圾收集时间)) , 自适应调节策略

  • Serial Old 收集器, (单线程标记整理算法)
    运行在client默认的java虚拟机老年代垃圾收集器

  • CMS 收集器 (多线程标记清除算法)
    concurrent mark sweep 收集器是老年代垃圾收集器, 主要目标是获取最短垃圾回收停顿时间. 并发标记/清除好使最长,所以总体上看CMS是和用户线程一起并发执行.

    • 初始标记: 只标记GC Roots能直接关联的对象, 速度很快, 仍需暂停所有工作线程
    • 并发标记: 进行GC Roots 跟踪的过程, 和用户线程一起工作
    • 重新标记: 为修正并发标记期间引用户程序继续运行导致标记产生的变动, 仍需停止所有工作线程
    • 并发清除: 清除GC Roots 不可达对象, 和用户线程一起工作.
  • G1 收集器

    • 基于标记-整理算法, 不产生内存碎片
    • 可以非常精确控制停顿时间, 在不牺牲吞吐量的前提下, 实现低停顿垃圾回收.

    G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

JVM 类加载机制

jvm 类的加载机制分为五个部分: 加载, 验证, 准备, 解析, 初始化

  • 加载: 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的入口.
  • 验证: 确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求.
  • 准备: 准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间.
    赋初值, 只是0, false等, 真正赋值是在编译后, 类构造器方法中
  • 解析: 指虚拟机将常量池中的符号引用替换为直接引用的过程。
    • 符号引用: 引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
    • 直接引用: 是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。引用的目标必定已经在内存中存在。
  • 初始化: 初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码.

不会执行类的初始化的情况:

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组,不会触发该类的初始化。
  3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触
    发定义常量所在的类。
  4. 通过类名获取 Class 对象,不会触发类的初始化。
  5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

类加载器

  • 启动类加载器, Bootstrap ClassLoader
    负责加载JAVA_HOME\lib目录中或 -Xbootclasspath 参数指定路径中被虚拟机认可(rt.jar)的类
  • 扩展类加载器, Extension ClassLoader
    JAVA_HOME\lib 目录中, 或java.ext.dirs系统变量指定路径中的类库
  • 应用程序类加载器, Application ClassLoader
    负责加载用户路径上的类库

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

比如对于rt.jar中的java.lang.Object, 不管那个加载器加载这个类, 最终都委托给启动类加载器, 保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值