JVM学习

记录学习Java JVM

一、JVM的学习方式

  • 请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
  • 什么是OOM?什么是栈溢出?怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • 谈谈JVM中,类加载器的认识?
  1. JVM的位置
  2. JVM的体系结构
  3. 类加载器
  4. 双亲委派机制
  5. 沙箱安全机制
  6. Native
  7. PC寄存器
  8. 方法区
  9. 三种JVM
  10. 新生区、老年区
  11. 永久区
  12. 堆内存调优
  13. GC
    1. 常用算法
  14. JMM
  15. 总结

二、JVM的体系结构

image-20241001184024276

image-20241001190719023

image-20241001190723436

image-20241001190940381

三、类加载器及双亲委派机制

作用:加载Class文件

image-20241001191035701

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器(BootstrapClassLoader
  3. 扩展类加载器(ExtClassLoader
  4. 应用程序(系统类)加载器(AppClassLoader

四、Java历史-沙箱安全机制

java安全模型的核心就是java沙箱(sandbox)

什么是沙箱?

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

沙箱机制就是将java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离。防止对本地系统造成破坏。沙箱主要限制系统资源访问。

那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也不一样。

所有的java程序运行都可以指定沙箱,可以指定安全策略。

在java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信受信的。对于受信的本地代码,可以访问一切本地资源。而对于非受信的远程代码在早期的java实现中,安全依赖于沙箱机制。如下图所示 JDK1.0安全模型

image-20241002123848724

但是如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限,如下图所示JDK1.1安全模型

image-20241002123904705

在java1.2版本中,再次改进了安全机制,增加了代码签名,不论本地代码或者远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型

image-20241002123934033

当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件,就具有当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)

image-20241002123956626

组成沙箱的基本组件

  • 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
  • 类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
    • 它防止恶意代码去干涉善意的代码;(双亲委派机制)
    • 它守护了被信任的类库边界;
    • 它将代码归入保护域,确定了代码可以进行哪些操作。

虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。

类装载器采用的机制是双亲委派模式。

  1. 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
  2. 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
  • 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
  • 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    • 安全提供者
    • 消息摘要
    • 数字签名(keytools)
    • 加密
    • 鉴别

五、Native方法区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo {
public static void main(String[] args) {
new Thread(() -> {

}).start();
}

/**
* native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层C语言的库!
* 会进入本地方法栈
* 调用本地方法接口! JNI
* JNI作用:扩展java的使用,融合不同的编程语言为java所用!最初:C、C++
* java诞生的时候 C、C++横行,想要立足,必须要有调用C、C++的程序
* 他在内存区域中专门开辟了一块标记区域:Native Method Stace,登记native方法
* 在最终执行的时候,加载本地方法库中的方法,通过JNI
*/
//java程序驱动打印机,管理系统。掌握即可,在企业级应用中较为少见!
private native void start0();
}

PC寄存器

程序计数器:program counter register

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条执行的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

方法区

Method Area 方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间

静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

六、深入理解一下栈

栈:数据结构

栈:先进后出 、后进先出 (桶)

队列:先进先出(FIFO: first input first output)

为什么main()方法先执行,最后结束!

栈:栈内存,主管程序的运行,生命周期和线程同步

线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题

一旦线程结束,栈就over

栈中可以存什么:

  • 8大基本类型
    • boolean
    • byte
    • char
    • short
    • int
    • long
    • float
    • double
  • 对象引用
    • reference类型
  • 实例的方法

栈运行原理:栈帧

栈满了会抛出:StackOverflowError

栈 + 堆 + 方法区 交互关系

image-20241002124557784

JVM虚拟机栈执行原理深入详解 - 知乎 (zhihu.com)

七、走进HotSpot和堆

三种JVM

  • sun公司的 HotSpot
  • BEA JRockit
  • IBM J9 VM

我们学习的都是:HotSpot

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

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

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

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

image-20241002135759659

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

加入内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:java heap space

在JDK1.8以后,永久区改了一个名字~元空间

八、新生区、永久区、堆内存调优

新生区

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

现象:经过研究,99%的对象都是临时对象!

老年区

永久区

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

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

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

image-20241002140740183

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JVMDemo01 {
public static void main(String[] args) {
//虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
//虚拟机的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();

System.out.println("maxMemory:" + maxMemory + "字节\t" + (maxMemory / (double) 1024 / 1024) + "MB");
System.out.println("totalMemory:" + maxMemory + "字节\t" + (totalMemory / (double) 1024 / 1024) + "MB");
//默认情况下,分配的最大内存是电脑内存的1/4 而初始化的内存是 1/64
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails
/**
* OOM:
* 1. 尝试扩大堆内存看结果
* 2. 分析内存,看一下哪个地方出现了问题(专业工具)
*/
}
}

输出

1
2
maxMemory:4211081216字节	4016.0MB
totalMemory:4211081216字节 252.0MB

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

idea设置JVM堆内存大小

这里要注意新版idea是默认隐藏vm option的

image-20241002143615421

输出:

1
2
3
4
5
6
7
maxMemory:1048576000字节	1000.0MB
totalMemory:1048576000字节 1000.0MB
[0.183s][info ][gc,heap,exit] Heap
[0.183s][info ][gc,heap,exit] garbage-first heap total reserved 1024000K, committed 1024000K, used 6144K [0x00000000c1800000, 0x0000000100000000)
[0.183s][info ][gc,heap,exit] region size 1024K, 6 young (6144K), 0 survivors (0K)
[0.183s][info ][gc,heap,exit] Metaspace used 858K, committed 1024K, reserved 1114112K
[0.183s][info ][gc,heap,exit] class space used 68K, committed 128K, reserved 1048576K

如下代码设置

-Xms1m -Xmx1m -XX:+PrintGCDetails

-Xms 设置初始化内存分配大小,默认1/64

-Xmx 设置最大分配内存大小,默认1/4

-XX:+PrintGCDetails — 打印GC垃圾回收信息

-XX:+HeapDumpOnOutOfMemoryError — OOM DUMP

拓展

这里也可以使用一些工具去检测对应的堆内存问题的,推荐就是可以使用JPofiler

在一个项目中,突然出现了OOM故障,那么该如何排除~

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

MAT,Jprofiler作用:

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

九、GC介绍之引用计数器

image-20241002150143735

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

  • 新生区
  • 幸存区
  • 老年区

GC种类:

  • 轻GC(普通的GC)
  • 重GC(全局的GC)

GC题目:

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

image-20241002150210872s

十、GC之复制算法

image-20241002151906956

image-20241002152233036

好处:没有内存的碎片

坏处:浪费了内存空间,多了一半空间永远是空(To),假设对象100%存活(极端情况)

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

十一、GC之标记压缩清除算法

标记清除

image-20241002152649930

优点:不需要额外的空间!

缺点:两次扫描,严重浪费时间,会产生内存碎片

标记压缩

image-20241002152705256

标记清除压缩

先标记清除几次

image-20241002152724230

再压缩

image-20241002152734368

十二、GC算法总结

内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率: 标记压缩算法 = 标记清除算法 > 复制算法

思考:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的算法 —–》 GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法

老年代:

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

一天时间学JVM,不现实,要深究,必须要花时间,多看面试题,以及《深入理解JVM》

但是,我们可以掌握一个学习JVM的方法

十三、如何快速学习方法讲解

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

  1. 什么是JMM

    【JMM】(Java Memory Model的缩写)

  2. 它干嘛的?– 官方,其他人的博客,对应的视频

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

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

image-20241002152846508

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

  2. 它该如何学习?

    JMM:抽象的概念,理论

    JMM对这八种指令的使用,制定了如下规则:

    • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

    • 不允许一个线程将没有assign的数据从工作内存同步回主内存

    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

    JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。


学习新东西是常态

针对面试学习

针对技术学习

面试:

3/10 == pass ,面经 =10 ,分析这10个?触类旁通:百度面试题

通过大量的面试总结,得出一套解题思路

更多阅读:https://mp.weixin.qq.com/s/nSwNZpObWLGteG-v7n5PSw