初探JVM

JVM

类加载机制

JVM处于机器的哪个位置?

JVM运行在操作系统之上,实际运行在OS之上的就是我们常说的JRE,而JRE中就包含了JVM。

OS运行在硬件体系之上,


JVM体系结构

Java Stack 和 Native Method Stack 和 PC 不会存在垃圾回收

类加载器:加载、链接、初始化

JVM调优几乎是在堆及方法区实现的

在这里插入图片描述


类加载器

用于加载 Class 文件

加载器包括:

  • JVM自带的加载器
    • 引导(启动)类(根)加载器(在JRE中lib的rt.jar包中
    • 扩展类加载器(在JRE中lib的ext中的jar包中
    • 系统类加载器(应用程序加载器)
  • 用户自定义的类

双亲委派机制
在这里插入图片描述

保证安全性

当加载器要将.class文件加载类到JVM时,采用双亲委派机制,即

  • 类加载器收到类加载请求后,不会先自己进行加载,而是把该请求委托给父类加载器进行加载,如果父类加载器还存在加载器,再委托给其父类加载器,当达到最顶层的类加载器时,如果该加载器可以完成类加载,则成功返回,否则该类加载器的子类加载器尝试进行类加载。

  • 如果父类已经加载过该类,则直接返回,无需重复加载


沙箱安全机制

保证JVM的安全

沙箱是一个限制程序运行的环境。

沙箱机制就是将Java代码程序限制在JVM特定的运行环境中。

Java将执行程序分为本地代码和远程代码,默认本地代码是安全的,而远程代码不一定是安全的。

Java沙箱的基本组件

  • 字节码校验器

  • 类加载器

  • 存取控制器

  • 安全管理器

  • 安全软件包

沙箱的元素

  • 权限
  • 代码源
  • 保护域
  • 策略文件
  • 密钥库

内存结构

本地方法栈

native

native

  • 使用该关键字的函数都是Java无法触及的范围,需要调用底层C/C++的库
  • 使用该关键字会进入本地方法栈,调用JNI
    • JNI:Java本地方法接口
      • 扩展Java,使得不同语言可以在Java中使用
      • JNI需要去调用本地方法库

本地方法栈

  • 用于管理本地方法的调用

  • 登记 native 方法,在执行引擎执行时加载本地方法库

方法区

方法区

  • 方法区是线程共享的,当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
  • 方法区的大小是不固定的,可以动态调整
  • 方法区存在GC
  • 保存的信息:
    • 类信息
      • 类型信息
      • 静态变量
      • 常量
      • 类型的常量池
        • 每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;

程序计数器

程序计数器

  • 程序计数器是记录着当前线程所执行的字节码的行号指示器。

  • 每个线程都有一个程序计数器,是线程私有的

  • PC的值对应该线程执行字节码指令的地址(可以认为PC是一个指针)

  • 程序计数器占用内存空间很小,几乎可以忽略不计

Java栈

Java栈

运行原理:栈帧

Java栈的相关概念

  • Java栈是线程私有的,生命周期与线程相同
  • Java栈描述的是Java方法执行的内存模型
    • 每个方法执行时会创建一个栈帧存入栈中,其中栈内存,即局部变量表是需要关注的重点

栈帧

  • 栈帧是用于JVM进行方法调用与执行的数据结构,是虚拟机运行时数据区中的Java栈的栈元素

  • 栈帧存储了方法的局部变量表操作数栈动态链接方法返回地址

  • 局部变量表存储了八大基本类型对象引用returnAddress类型(值为指向了一条字节码指令的地址

在这里插入图片描述

堆 Heap

一个Java虚拟机只存在一个堆,是线程共享的,且堆内存是可动态调节的

默认情况下,JVM分配的最大内存是宿主机的1/4,初始内存是宿主机的1/64

JDK1.8以前,堆包括:

  • 新生带(年轻代)
  • 老年代(养老区)
  • 永久代

JDK1.8以后,堆包括:

  • 新生带(年轻代)
  • 老年代(养老区)

JDK 1.8 以后,永久代被元空间取消了,而元空间采用的是本地内存,而不是虚拟内存


新生带

该区域存放的内存对象是新创建的对象或者刚创建不久的对象,新生带分为:

  • 伊甸区

    大多数新建对象处于 Eden 区,该区域被填充时,执行 Minor GC,并将没有被GC的对象移动到幸存区中

  • 幸存0区

  • 幸存1区

默认情况下,三者的比例为: 8 : 1 : 1 8:1:1 8:1:1,可以通过 -XX:SurvivorRatio 配置

新生带在填充的时候,会执行垃圾回收,称为Minor GC,在 Eden 区执行 Minor GC 时,会将幸存的对象移动到另一个幸存者空间,故总有一个幸存区是空的

多次 Minor GC 仍然存活下来的对象会被移动到老年带


老年代

该区域存放着一些“顽固分子”,通常在此区域的垃圾回收都是在此区域内存满的时候进行,该区域的GC称为 Major GC(主GC),耗费时间相比Minor GC更长

需要大量连续内存空间的对象(大对象)一般不进入新生带,因为Eden区和两个幸存区之间存在内存拷贝


永久代(元空间)

只有在 HotSpot 中才有永久代的概念说法

该区域也称为非堆*(Non-Heap),当JVM关闭时,该区域的内存才会被释放

元空间:使用的是计算机物理内存(本地内存),非使用JVM的内存


堆内存调优

通过 IDE 设置 JVM 的起始堆内存和最大堆内存,参数分别为:-Xms-Xmx

  • -Xms:设置初始堆内存大小;
  • -Xmx:设置最大堆内存大小

这两个参数的值需要带单位

通过 -XX+PrintGCDetails 可以查看GC详情


TLAB

TLAB:Thread Local Allocation Buffer

  • 对 Eden 区域进行划分,JVM为每个线程分配一个私有缓存区域在 Eden 区重
  • 使用 TLAB 可以保证多线程问题时的线程安全,且提高内存分配的吞吐量(通常情况下对象是分配在堆上的,因为堆是线程共享的,所以同一时间可能会有很多线程申请空间分配,在这种情况下一般要加锁处理,加锁便会造成分配效率下降。而TLAB是每个线程独有的,它可以避免这种开销,直接分配空间,效率等同于C语言)
  • OpenJDK 衍生的 JVM 提供了 TLAB 设计

通过 -XX:UseTLAB 可以设置 TLAB 的开启,通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占伊甸园区的大小比例


逃逸分析

逃逸分析:JVM中的一种优化技术

  • 当一个对象在方法中定义后,不会被外部方法引用,则认为不会发生逃逸
  • 当一个对象在方法中定义后,被外部方法引用,则认为发生逃逸

HotSpot 在 JDK7 后就默认开启了逃逸分析,也可以通过 -XX:DoEscapeAnalysis 开启

逃逸分析开启后,编译器会做如下代码优化:

  • 栈上分配

    不会发生逃逸则使用栈上分配

  • 同步省略

    对象值能从一个线程被访问到则使用同步省略

  • 分离对象/标量替换

垃圾回收机制

对象回收的判断

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,也就是需要回收的对象。

存在问题:循环引用问题会导致对象无法被回收,故JVM 不采用引用计数算法


可达性分析算法

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

在Java语言中,可以作为GC Roots的对象包括以下几种

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即本地方法接口)引用的对象

如果对象在进行可达性分析之后被发现没有与 GC Roots 相连的引用链,那么它将会被第一次标记,并且进行一次筛选,筛选的条件就是此对象是否有必要执行 finalize() 方法

两次标记过程:

  1. 第一次标记: 如果对象在进行可达性分析后发现没有 GC Roots 相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行此时会直接回收该对象;如果对象被判定为有必要执行,则会被放到一个 F-Queue 队列。
  2. 第二次标记:finalize() 方法是对象跳脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中对象进行第二次小规模标记,如果对象要在finalize()中重新拯救自己:只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时她将被移出即将回收的集合。

垃圾回收算法

复制

作用场景:存放存活率低,占用内存空间小的对象的区域

按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

JVM 中如何使用?

作用区域:年轻代中的 Eden 区和幸存区

目前的商用 JVM 普遍采用复制回收算法来回收年轻代,但并不是将年轻代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

缺点:内存使用率较低


标记-清除

该算法分为两个阶段,标记和清除,标记无用对象,然后进行清除回收

缺点:效率不高,内存碎片严重化


标记-整理

记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存

缺点:效率不高


分代收集

分代收集算法,根据对象的存活周期将内存划分为几块,按照块来采用不同的 GC 算法

一般包括年轻代、老年代。

根据对象存活周期的不同将内存划分为几块,一般是年轻代和老年代,新生代基本采用复制算法,老年代采用标记-清除/标记-整理算法

当前商业虚拟机都采用分代收集的垃圾收集算法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

programming_rooike

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值