超详解 JVM 中重点内容,对整个底层体系更进认知

Posted 满眼*星辰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超详解 JVM 中重点内容,对整个底层体系更进认知相关的知识,希望对你有一定的参考价值。

JVM

JVM概念

JVM(Java Virtual Machine的简称。意为Java虚拟机

虚拟机:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统


java能够一次编写,到处运行的关键(JVM兼容处理)【Linux,macos,windows】

常见虚拟机:
第一: HotSpot JVM
第二: IBM J9 JVM

JVM 布局(HotSpot)

JDK 1.8

1. 堆

new Object() 所有对象都是存在此区域,此区域也是 JVM 中最大的一块区域

JVM 的垃圾回收就是针对此区域

堆划分:新生代、老年代

  • 新生代:第一次创建的对象都会分配到此区域

  • 老年代:经历了一定的垃圾回收之后,依然存活下来的对象会移动到老年代;大对象在创建的时候也会直接进入老年代。

为什么大对象会直接进入老年代?
核心原因是大对象的初始化比较耗时,如果频繁的创建和销毁,会带来一定的性能开销,因此最好的实现方式是将他存放到 GC 垃圾回收频率更低的老年代

新生代区域划分

  1. Eden:80% 内存
  2. S0:10% 内存
  3. S1:10% 内存
    新生代内存的利用率就可以达到90%
    Eden + S0 / Eden + S1

新生代 -> 老年代

HotSpot 默认的执行次数是 15 次,经历 15 次 GC 就会从新生代转到老年代。

2. JVM 栈(Java虚拟机栈)

a)局部变量表:8 大基础数据类型,对象的引用
b)操作栈:每个方法都会对应一个操作栈
c)动态连接:指向常量池的方法引用
d)方法返回地址:PC 寄存器的地址

进栈出栈代码演示

public class ThreadDemo102 

    public static void main(String[] args) 
        System.out.println("执行了main方法");
        methodA();
    

    private static void methodA() 
        System.out.println("执行了方法A");
        methodB();
    

    private static void methodB() 
        System.out.println("执行了方法B");
        methodC();
    

    private static void methodC() 
        System.out.println("执行了方法C");
    


3. 本地方法栈

它与 JVM 栈比较类似,只不过 JVM 栈是给 java 和 JVM 使用,而本地方法栈它是本地方法(c / c++)服务。

4. 程序计数器

用来记录线程执行的行号

5. 元空间(JDK 1.8)/ 方法区(JDK 1.7)

运行时常量信息、字符串常量池、类的元信息 等(JDK 1.7 的时候的方法区)

JDK 1.8 元空间:本地内存,并且将字符串常量池移动到堆

内存线程共享与私有

线程共享:堆、元空间

线程私有:JVM 栈、本地方法栈、程序计数器是

JVM参数调优

在 idea.exe.vmoptions 文件进行配置
-Xmx10m :堆最大容量
-Xms10m :堆最小容量设置
通常情况下可以将 Xmx 和 Xms 的大小设置相同,这样可以防止堆扩容所带来的抖动

JVM 调优的时候可以设置的参数类型:

  1. -X:非标准的参数设置,它只能针对特殊 HotSpot 生效(-Xms10m)
  2. -XX:标准参数设置,它针对所有的 HotSpot 都生效
  3. -D:设置应用程序的参数(-Dmykey=value)

JVM类加载机制(class loading)

1. 加载

(Loading)【去机场】

  1. 根据类路径全名加载二进制流
  2. 将静态的存储结构转换成运行时的数据结构
  3. 在内存中生成一个此类的方法入口

(将静态文件转成运行内存)

2. 效验

【安检】

  1. 文件格式进行效验
  2. 字节码效验
  3. 元数据
  4. 符合引用
    等针对正确性和安全性进行效验

3. 准备

【等待飞机起飞】
将类的静态变量在内存中进行分配,这里只赋予默认值

比如这样一行代码:
public static int count = 123;
注意此时只会在内存中生成一个 count=0 变量,对变量类型的初始化

将类中的静态变量在内存中进行分配

4. 解析

【根据机票找座位】

  • 字面量:String str = “abc”,abc就是字面量
  • 符号引用:类、方法的完全限定名(全路径名称)
  • 直接引用:将符号引用加载到内存中(根据引用指向内存中的对象)

初始化final修饰的常量

将常量池中的符号引用替换为直接引用(内存地址)的过程

5. 初始化

【准备起飞】

为类的静态变量赋初值

此步骤开始将执行权从 JVM 转移到自己写的程序,开始执行构造函数

6. 使用

【起飞】

7. 卸载

【落地】

JVM 双亲委派模型

当加载一个类的时候,那么这个类不会直接加载,而是将这个加载任务直接交给父类。当找不到父类的时候,才自己尝试去加载


优点:

  1. 唯一性(父类执行加载一次)
  2. 安全性(会往上找生层的类是系统提供的类,避免加载自定义的类,从而一定程度上保证了安全性)

破坏双亲委派模型

(3次)

  1. JDK 1.2 提出的双亲委派模型,为了兼容老版本,因此在 JDK 1.2 的时候已经出现了破坏双亲委派模型的场景。
  2. 是因为双亲委派模型自身的缺点而导致的,比如在父类当中要调用子类的方法是没办法实现
  3. 人们对于热更新的追求,导致了双亲委派模型的又一次破坏

垃圾回收

1. 判别死亡对象(垃圾)

a)引用计数器算法

给每个对象创建一个计数器,当有程序引用此类的时候,计数器 +1,不使用的时候计数器 -1,当计数器为 0 时,则表示此对象没人用,那么就可以将它归为死亡对象,等待垃圾回收器回收

引用计数器算法缺点:
它会有循环引用的问题

public class Work 

    private byte[] bytes = new byte[5*1024*1024];
    public Work obj = null;

    public static void main(String[] args) 
        Work w1 = new Work();
        Work w2 = new Work();
        //循环引用
        w1.obj = w2;
        w2.obj = w1;
        //释放对象
        w1 = null;
        w2 = null;
        //强制垃圾回收
        System.gc();
    



打印垃圾回收详情信息

执行结果:

所以HotSpot 默认的垃圾回收器使用的不是引用计数器算法

b) 可达性分析算法

HotSpot 默认使用的算法

如果对象的父类一直往上最后是 GC Roots,则不是垃圾对象,否则是垃圾对象,如上图

可以作为GC Roots的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中静态属性引用的对象
  3. 方法去中常量引用的对象
  4. 本地方法栈中 JNI(Native 方法) 引用的而对象

2. 垃圾回收算法

标记清除算法

标记:可达性分析(存活的对象和死亡的对象)

黑色:标记对象
浅色:不是垃圾
白色:空内存

缺点:内存碎片
比如要创建一个数组,数组的内存空间必须是连续的,这种算法使得有很多内存碎片没有办法连续的使用内存。

复制

优点:内存性能比较高
缺点:内存利用率低

因为98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,为了解决内存利用率低,就分为将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,分别占80%,10%,10%,以此来提高利用率

标记整理算法

针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步
骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的
内存。

3. 垃圾回收器

Serial

单线程串行的垃圾回收器(复制算法)

Serial Old

老年代垃圾回收器(标记-整理算法)【串行GC】

PerNew

是 Serial 的多线程版本(并行垃圾回收器)

Parallel Scavenge

并行垃圾回收器(新生代)

以吞吐量作为主要依据进行垃圾回收

场景:后端系统

Parallel Old

并行垃圾回收器(老生代)

CMS

并发

(标记-整理算法)

  1. 初始标记,简单的标记一下,STW小
  2. 并发标记,和用户线程一起执行,
  3. 重新标记,重新标记并发中用户线程产生的垃圾,STW小
  4. 并发清理,
  5. 循环,跳转到1

适用场景:BS,用户交互系统

G1

JDK11是默认的垃圾回收器

G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标
记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的

一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区
域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域
主要用于存储大对象-即大小超过一个region大小的50%的对象

JMM

Java内存模型(让 JVM 高速运行的一种技术)

解决不同操作系统在操作内存时的性能差异。

存储构成


速度:寄存器 》 L1缓存 》 L2缓存 》主内存

主内存与工作内存

Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中
保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内
存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线
程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示 :

内存间交互操作

  1. lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  2. unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可
    以被其他线程锁定。
  3. read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随
    后的load动作使用。
  4. load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量
    副本中。
  5. use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
  6. assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
  7. store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的
    write操作使用。
  8. write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量
    中。

JMM 三大特征

  1. 原子性:要么全部成功,要么全部失败
  2. 可见性:内存可见性
  3. 有序性:操作有执行顺序

volatile

  1. 保证此变量对所有线程的可见性
  2. 使用volatile变量的语义是禁止指令重排序

以上是关于超详解 JVM 中重点内容,对整个底层体系更进认知的主要内容,如果未能解决你的问题,请参考以下文章

超详解 JVM 中重点内容,对整个底层体系更进认知

yolo3各部分代码详解(超详细)

JVMJVM系列之JVM体系

JVM宏观认知&&内存结构

找了很久的网上唯一的一套JVM底层Hotspot源码视频,免费送!

理论+实战 构建完整JVM知识体系