JVM简单初理论

本文介绍了JVM的基础知识,包括双亲委派机制、沙箱安全机制和Native关键字。详细讨论了PC寄存器在JVM中的作用,解释了为何它是线程私有的。此外,还涉及堆内存结构、垃圾回收以及Java内存模型(JMM)的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM初探

1. 双亲委派机制

在介绍双亲委派机制的时候,不得不提ClassLoader(类加载器)。说ClassLoader之前,我们得先了解下Java的基本知识。
Java是运行在Java的虚拟机(JVM)中的,但是它是如何运行在JVM中了呢?我们在IDE中编写的Java源代码被编译器编译成**.class**的字节码文件。然后由我们得ClassLoader负责将这些class文件给加载到JVM中去执行。
JVM中提供了三层的ClassLoader:

  • Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类

当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException

image-20210213154440152

2. 沙箱安全机制

​ 引导类加载器会先加载jdk自带的文件,如果有一个外来类需要加载,并且包名和类名相同,则由于优先由引导类加载了自带的类,所以不会加载这个不符合规范的类,这样保护了java核心源代码,也称沙箱安全机制

3. Native关键字

native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。

用法

1.编写带有native声明的方法的Java类(java文件)
2.使用javac命令编译编写的Java类(class文件)
3.使用javah -jni ****来生成后缀名为.h的头文件(.h的文件)
4.使用其他语言(C、C++)实现本地方法
5.将本地方法编写的文件生成动态链接库(dll文件)

4. PC寄存器的作用

  1. PC寄存器用来存储当前正在执行的Java虚拟机指令的地址,当 方法是 native标注的本地方法时指令值 `未定义(注意 : 有的文章描述 下一条待执行指令地址,但JVM规范中说明正在执行的指令地址)
  2. 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
  3. 在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
  4. 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
  5. 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  6. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
  7. 它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且`没有垃圾回收
  8. 我们都知道多线程并行实则是CPU的假象,CPU不停的来回切换线程执行,这时候切换回来以后,就得知道接着从哪开始继续执行,这时候PC寄存器就派上用场了

image-20210213202520363

1. 使用PC寄存器存储字节码指令地址有什么好好处

因为CPU需要不停的且切换各个线程,这个时候切换回来就需要知道接着从哪里开始继续执行

2. 为什么使用PC寄存器记录当前线程执行地址呢

JVM的字节码解释器,就需要通过改变PC寄存器的值明确一条应该执行什么样的字节码指令

3.PC寄存器为什么设定为线程私有的

  1. 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法, CPU会不停的做任务切换,这样必然会导致经常中断或者恢复, 为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是为每一个线程分配自己独立的PC寄存器,这样各个线程就是独立计算,从来不会出现干扰的情况
  2. 由于CPU时间片轮限制,众多线程在并发执行的过程中,任何一个切丁的时间刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一个指令

深入理解栈

: 先进后出, 后进先出

**队列: ** 先进先出(FIFO: First Input First Output)

**形象的比喻: **喝多了吐就是栈, 吃多了拉就是队列

  1. 为什么main()先执行,最后结束

image-20210214091551263

main()方法压在栈底当main()方法弹出后该方法结束

  1. 栈: 栈内存,主管程序的运行,生命周期和线程同步; 当线程结束的时候,栈内存也就是释放,对于栈来说,不存在垃圾回收问题, 一旦线程结束,栈也就结束了

  2. **栈中存放哪些东西: ** 8大基本类型 + 对象的引用 + 实例的方法

  3. **栈运行的原理: **

    image-20210214092722805

栈如果满了: StackOverflowError

  1. **栈 + 堆 + 方法区: ** 交互关系

Heap, 一个JVM只有一个堆内存,堆内存的大小是可以调节的.

类加载器读取了类文件后, 一般会把什么东西放到堆中? 类,方法,常量,变量~ 保存我们所有引用类型的真实对象;

堆内存中还要细分为三个区域:

  • 新生区 (伊甸园区) Young/New
  • 养老区 old
  • 永久区 Perm

image-20210216095447254

GC垃圾回收,主要是在伊甸园和养老区

假设内存满了,OOM,堆内存不够 (java.lang.OutOfMemoryError:Java heap space)

**OOM报错: ** 是因为堆内存满了

在JDK8以后,永久存储区改了个名字(元空间)

新生区

  • 类: 诞生和成长的地方, 甚至死亡;
  • 伊甸园, 所有的对象都是在伊甸园区 new 出来的
  • 幸存者区 (0,1)

老年区

image-20210216101103952

真理: 经过研究, 99%的对象都是临时对象

永久区

这个区域常驻内存的.用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息~ 这个区域不存在垃圾回收, 关闭VM虚拟机就会释放这个区域的内存

一个启动类,加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成的反射类. 不断的被加载,直到内存满,就会出现OOM

  • jdk1.6之前: 永久代, 常量池是在方法区
  • jdk1.7 : 永久代,但是慢慢的退化了.去永久代,常量池在堆中
  • jdk1.8之后: 无永久代,常量池在元空间

image-20210216102824805

元空间: 逻辑上存在, 物理上不存在

在一个项目中,突然出现了OOM故障,那么该如何排除研究为什么出错

  • 能够看到代码第几行出错: 内存快照分析工具, MAT, Jprofiler
  • DuBug,一行一行分析代码

MAT, Jprofiler 作用

  • 分析Dump内存文件,快速定位内存泄露
  • 获得堆中数据

测试Jprofiler

设置VM options

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

image-20210216111044728

运行

image-20210216111241523

测试代码

image-20210216111654924

使用JPofiler工具分析OOM的原因

  1. 下载JPofiler客户端,并安装
  1. Idea中下载jpofiler插件重启
  1. 编写程序测试

设置VM options

image-20210216134336285

编写代码

由于该异常是OutOfMemoryError 使用Exception无法捕获到错误,

应该使用Error

image-20210216134055352

会生成.hprof文件, 寻找到该文件后打开

image-20210216134449781

因为该线程占用大

image-20210216134630487

GC: 垃圾回收

image-20210216143136222

JVM在进行GC时,并不是对这三个区域统一回收. 大部分时候,回收都是新生代~

  • 新生代
  • 幸存区 (from, to)
  • 老年区

GC两种类: 轻GC(普通的GC), 重GC(全局GC)

GC题目:

  • JVM的内存模型和分区~ 详细到每个区放什么?
  • 堆里面的分区有哪些? Eden, from, to, 老年区, 说说他们的特点
  • GC的算法有哪些? 标记清除法, 标记压缩, 复制算法, 引用计数器, 怎么用的?
  • 轻GC和重GC分别在什么时候发生?

引用计数法:

image-20210216164221851

复制算法:

image-20210216174133502

GC简易过程

image-20210216174836472

  • 好处: 没有内存的碎片~
  • 坏处: 浪费了内存空间~ 多了一半空间永远是空的 to区 假设对象百分百存活(极端情况)

复制算法最佳使用场景: 对象存活度较低的时候, 新生区

标记清除算法

image-20210216194325169

  • 优点: 不需要额外的空间
  • 缺点: 两次扫描,严重浪费时间, 会产生内存碎片.

标记压缩

在优化:

image-20210216195122103

标记清除压缩

先标记清除几次

image-20210216195431420

在压缩

image-20210216195508746

总结

内存效率:

​ 复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)

内存整齐度:

​ 复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:

​ 标记压缩算法 = 标记清除算法 > 复制算法

没有最好的算法, 只有最合适的算法 ----> GC: 分代收集算法

年轻代:

  • 存活率低
  • 复制算法

老年代:

  • 区域大: 存活率
  • 标记清除(内存碎片不是太多)+标记压缩混合 实现

JMM(Java Memory Model)

  1. 什么是JMM?

    JMM: (Java Memory Model):Java内存模型

  2. 它是干嘛的?

    作用: 缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)

    JMM定义了线程工作内存和主内存之间的抽象关系, 线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

    image-20210216203333379

    解决共享对象可见性这个问题: volatile

  3. 它该如何学习?

    JMM: 抽象的概念, 理论

    volatile关键字

.

Memory Model):Java内存模型

  1. 它是干嘛的?

    作用: 缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)

    JMM定义了线程工作内存和主内存之间的抽象关系, 线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

    [外链图片转存中…(img-AR6OBcer-1615122049648)]

    解决共享对象可见性这个问题: volatile

  2. 它该如何学习?

    JMM: 抽象的概念, 理论

    volatile关键字

.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值