一、前景提示 ⛹️♀️ @CreateByShadow
- 请谈谈你对JVM的理解?java8的虚拟机有什么更新?
- 什么是OOM?什么是StackOverFlowError?有哪些方法分析?
- JVM的常用参数调优你知道哪些?
- 谈谈JVM中,对类加载器你的认识?
二、JVM核心
1、JVM体系结构概述
JVM是运行在操作系统之上的,它与硬件没有直接的交互
1.1 结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BCFSVXAN-1624682352966)(./images/1.jpg)]
1.2 类装载器(ClassLoader)
1.2.1 介绍
负责加载class文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ehvifC3Q-1624682352976)(./images/2.jpg)]
echo %JAVA_HOME%
echo %PATH%
echo %CLASSPATH%
1.2.2 类加载器的种类
-
虚拟机自带的加载器
-
启动类加载器(Bootstrap)C++
$JAVAHOME/jre/lib/rt.jar 包里面的类
Object o = new Object(); System.out.println(o.getClass().getClassLoader()); // null
-
扩展类加载器(Extension)Java
$JAVAHOME/jre/lib/ext/*.jar 里面的jar包
-
应用程序类加载器(AppClassLoader)
$CLASSPATH
MyObject m = new MyObject(); // null System.out.println(m.getClass().getClassLoader().getParent().getParent()); // sun.misc.Launcher$ExtClassLoader@2cdf8d8a System.out.println(m.getClass().getClassLoader().getParent()); // sun.misc.Launcher$AppClassLoader@58644d46 System.out.println(m.getClass().getClassLoader());
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upoQ43AM-1624682352980)(./images/3.jpg)]
-
-
用户自定义加载器(了解)
- java.lang.ClassLoader的子类,用户可以定制类的加载器
1.2.3 类加载器的双亲委派机制、沙箱安全机制
双亲委派:
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
沙箱安全:
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
1.3 本地方法栈(Native Method Stack)【了解】
native 方法:
目前该方法使用越来越少,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备。
它的具体做法是Native Method Stack 中登记native方法,在Execution Engine 执行时加载本地方法库。
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("Hello Native...");
} catch (InterruptedException e) {
e.printStackTrace();
};
},"A").start();
// start()方法源码
// ...
start0();
// ...
private native void start0();
1.4 PC寄存器(Program Counter Register)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果执行的是一个Native方法,那这个计数器是空的。
用以完成分支,循环,跳转,异常处理,线程恢复等基础功能。不会发生内存溢出(OutOfMemoryError)OOM错误。
1.5 知识小总结:
JVM系统架构图
类加载器
// null System.out.println(m.getClass().getClassLoader().getParent().getParent()); // sun.misc.Launcher$ExtClassLoader@2cdf8d8a System.out.println(m.getClass().getClassLoader().getParent()); // sun.misc.Launcher$AppClassLoader@58644d46 System.out.println(m.getClass().getClassLoader());
- 有哪几种类加载器
- 双亲委派
- 沙箱安全机制
Native
- native 是一个关键字
- 有声明,无实现,why ?Native Method Stack
PC寄存器
记录了方法之间的调用和执行情况,类似排班值日表
用来存储指向下一条指令的地址,也即将要执行的指令代码
他是当前线程所执行的字节码的行号指示器
- 方法区
它存储了每一个类的结构信息
方法区是规范
- stack
栈管运行,堆管存储
方法区、堆、栈保存哪些对象?
- 方法区:方法区是线程共享的运行时内存区域,存储了每一个类的结构信息,例如:运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容,还包括类、实例、接口初始化时用到的特殊方法【例如:< init>和< clinit>】
- 堆:堆是线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域
- 栈:java栈是线程私有的内存区域,它描述的是java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧(stack frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
1.6 方法区(Method Area)
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。
方法区是规范,在不同虚拟机里头的实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。
实例变量存在堆内存中,和方法区无关
1.7 java栈(Java stack)
1.7.1 介绍
程序 = 算法 + 数据结构
程序 = 框架 + 业务逻辑
两种数据结构:
- 队列(FIFO):先进先出(排队打饭)
- 栈 (FILO) :先进后出(枪支弹夹)
方法 = 栈帧
Stack栈
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收机制,只要线程一结束该栈就over,生命周期和线程是一致的,是线程私有的。
8种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配
栈存储什么?
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量
- 栈操作(Operand Stack):记录出站、入栈的操作
- 栈帧数据(Frame Data):包括类文件、方法等等
图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L78OG50H-1624682352984)(./images/4.jpg)]
1.7.2 StackOverflowError
public class JVM {
public void t() {
t();
}
// Exception in thread "main" java.lang.StackOverflowError
public static void main(String[] args) {
JVM jvm = new JVM();
jvm.t();
}
}
java.lang.StackOverflowError是异常还是错误?
是错误
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-diPg6FeC-1624682352987)(./images/5.jpg)]
1.8 栈+堆+方法区的交互关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCZNXTSc-1624682352987)(./images/6.jpg)]
2、堆体系结构概述
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑上分为三部分:新生+养老+永久
物理上:新生+养老
2.1 新生区
新生区是类的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),所有的类都是在伊甸区被new出来的。幸存区有两个:0区和1区。
当伊甸区的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC)
将伊甸区中的不再被其他对象引用的对象进行销毁。然后将伊甸区中的剩余对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区,若1区也满了,再移动到养老区,若养老区也满了,那么这个时候讲产生Major GC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,将产生OOM(OutOfMemoryError)异常。
如果出现java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因有二:
1、Java虚拟机的堆内存设置不够,可以通过参数 -Xms、-Xmx来调整。
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
2.1.1 伊甸区(Eden Space)
2.1.2 幸存0区/from(Survivor 0 Space)
2.1.3 幸存1区/to(Survivor 1 Space)
2.1.4 对象生命周期及GC
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vShXDNpo-1624682352989)(./images/7.jpg)]
2.2 养老区(Tenure Generation Space)
2.3 元空间/永久代(Permanent Space)
方法区的实现
3.堆参数调优入门
JDK7的堆模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uxhrltI1-1624682352990)(./images/9.jpg)]
JDK8的堆模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ign5AQIF-1624682352991)(./images/10.jpg)]
解读:
- GC发生在新生区
- Full GC发生在养老区
- 新生区/养老区 = 1/2
- Eden/S0/S1 = 8/1/1
- 调优参数 -Xms 及 -Xmx
JDK7与JDK8的区别:
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代,元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于:
永久代使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ED8S97sG-1624682352992)(./images/8.jpg)]
3.1 内存参数配置(-Xms初始内存大小,-Xmx最大内存)
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- 初始内存和最大内存怎么配?
Xms和Xmx的值必须设置一样大
理由:避免GC和应用程序争抢内存,理论值的峰值和峰谷忽高忽低。
怎么配?(IDEA)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-myjsCjPe-1624682352993)(./images/11.jpg)]
3.2 查看heap结构(-XX:+PrintGCDetails)
// CPU核数
System.out.println(Runtime.getRuntime().availableProcessors());
// 最大内存
System.out.println("Xmx:"+Runtime.getRuntime().maxMemory()/(double)1024/1024);
// 初始内存
System.out.println("Xms:"+Runtime.getRuntime().totalMemory()/(double)1024/1024);
4 Xmx:981.5 Xms:981.5 Heap // 新生区 PSYoungGen total 305664K, used 20972K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000) // 伊甸区 eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7b048,0x00000000fab00000) // from区 from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000) // to区 to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000) // 养老区 ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000) object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000) // 元空间 Metaspace used 3264K, capacity 4496K, committed 4864K, reserved 1056768K class space used 350K, capacity 388K, committed 512K, reserved 1048576K
物理上:新生区 + 养老区
699392K + 305664K = 981.5M
3.3 OOM(java.lang.OutOfMemoryError: Java heap space)
-Xms10m -Xmx10m -XX:+PrintGCDetails
byte[] bytes = new byte[40 * 1024 *1024];
[GC (Allocation Failure) [PSYoungGen: 2048K->484K(2560K)] 2048K->1011K(9728K), 0.0021184 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2532K->500K(2560K)] 3059K->1555K(9728K), 0.0015301 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 850K->500K(2560K)] 1905K->1603K(9728K), 0.0012114 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 500K->500K(2560K)] 1603K->1619K(9728K), 0.0064274 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [Full GC (Allocation Failure) [PSYoungGen: 500K->0K(2560K)] [ParOldGen: 1119K->1482K(7168K)] 1619K->1482K(9728K), [Metaspace: 3272K->3272K(1056768K)], 0.0605470 secs] [Times: user=0.05 sys=0.00, real=0.06 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 1482K->1482K(9728K), 0.0092194 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 1482K->1454K(7168K)] 1482K->1454K(9728K), [Metaspace: 3272K->3272K(1056768K)], 0.0114834 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.yidao.promote.http.wangqin.jvm.JVM2.main(JVM2.java:21)
3.4 解读GC日志
公式:[名称:GC前内存占用 -> GC后内存占用 (该区内存总大小)]
- GC
[GC (Allocation Failure) [PSYoungGen: 2048K->484K(2560K)] 2048K->1011K(9728K), 0.0021184 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qeja3H8v-1624682352995)(./images/12.jpg)]
- Full GC
[Full GC (Allocation Failure) // GC 类型 Full GC // 新生区:GC前0k -> GC后0k (新生区总大小2560k) [PSYoungGen: 0K->0K(2560K)] // 养老区:GC前1482k -> GC后1454k(养老区总大小7168k)|FullGC前堆内存占用1482k -> FullGC后内存占用1454k(堆内存总大小9728k = 2560k[新生区总大小]+7168k[养老区总大小]) [ParOldGen: 1482K->1454K(7168K)] 1482K->1454K(9728K), // 元空间:GC前内存占用 -> GC后内存占用(元空间总大小) 耗时 [Metaspace: 3272K->3272K(1056768K)], 0.0114834 secs] // 用户耗时 需要耗时 实际耗时 [Times: user=0.03 sys=0.00, real=0.01 secs]
3.5 GC
3.5.1 GC是什么(分代收集算法)
- 次数上频繁收集Young区
- 次数上较少收集Old区
- 基本不动元空间
3.5.2 GC4大算法
-
GC算法概述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xaQ6yj6I-1624682352996)(./images/13.jpg)]
- JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
- 因此GC按照回收的区域又分为两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)
Minor GC 和 Major GC的区别:
普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因此大多数Java对象存活率都不高,所以Minor GC 非常频繁,一般回收速度也比较快。
全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次Minor GC(并不是绝对的)。Major GC的速度一般比Minor GC慢上10倍以上。
Full GC为什么比GC慢?
普通GC 发生在新生区,Full GC发生在养老区,根据内存占用比例:1:2,Full GC发生至少会伴随至少一次的普通GC,区域更大,因此会慢。
-
4大算法
-
引用计数法(了解)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPeApOOY-1624682352997)(./images/14.jpg)]
循环引用问题:
public class RefCountGC { private byte[] bytes = new byte[2 * 1024 *1024]; Object instance = null; public static void main(String[] args) { RefCountGC o1 = new RefCountGC(); RefCountGC o2 = new RefCountGC(); o1.instance = o2; o2.instance = o1; o1 = null; o2 = null; System.gc(); } }
-
复制算法(Copying)
-
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
-
原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WL0IDs26-1624682352998)(./images/15.jpg)]
-
解释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qchCKGSk-1624682352999)(./images/16.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-naCSx4NO-1624682353000)(./images/17.jpg)]
-
优缺点
- 无内存碎片
- 耗内存空间
- 若存活对象占有率多,需要复制的就多,这种情况会比较耗时
-
-
标记清除(Mark-Sweep)
-
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
-
原理
算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4kwAFSQL-1624682353001)(./images/18.jpg)]
-
优缺点
- 节约内存空间
- 两次扫描,耗时严重
- 产生内存碎片
-
-
标记整理(Mark-Compact)
-
老年代一般是由标记清除或者标记清除与标记整理的混合实现
-
原理
-
标记(Mark)
与标记清除一样
-
压缩(Compact)
再次扫描,并往一端滑动存活对象
-
清除标记对象
-
-
优缺点
- 没有内存碎片
- 需要移动对象的成本,即耗时
标记-清除-压缩
原理:
- Mark-Sweep 和 Mark-Compact的结合
- 和Mark-Sweep一致,当进行多次GC后才Compact
优点:
减少移动对象的成本
-
-
-
小总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Grlc9fto-1624682353002)(./images/19.jpg)]
问题:哪张算法最好?
无,没有最好的算法,只有最合适的算法 ======>>> 分代手机算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ji0y2siy-1624682353004)(./images/20.jpg)]
-
面试题
-
执行一个空的main方法,后台有几个线程?
2个线程,一个main线程,一个GC线程
-
谈谈对 public static void main(String[] args) 的理解?
public,static,void的解释
程序的入口。
-
对 System.gc() 的理解?
显示开启GC,但不是立即执行;实际开发中禁用!
-
JVM内存模型及分区,需要详细到每个分区放什么
-
堆里面的分区,Eden,survivor from to,老年代,各自的特点
-
GC的三只手机方法,标记清除,标记整理,复制算法的原理与特点,分别用在什么地方
-
Minor GC 和Full GC 分别在什么时候发生
-
4.JMM(java内存模型)
4.1 volatile是java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排序
4.2 JMM 你谈谈
JMM(Java内存模型 Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECVoiS4f-1624682353006)(./images/21.jpg)]
4.2.1 可见性
4.2.2 原子性
4.2.3 有序性
4.2.4 VolatileDemo代码演示可见性+原子性代码
public class Jmm {
public static void main(String[] args) {
Test t = new Test();
new Thread(() -> {
System.out.println("come in A thread...");
try {
TimeUnit.SECONDS.sleep(3);
t.change();
System.out.println(Thread.currentThread().getName() + "修改了flag" + t.flag);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
while (t.flag == 10) {
// 需要有一种机制来告诉main线程,flag已经被修改了,跳出while
}
System.out.println(Thread.currentThread().getName() + " is over...");
}
}
class Test{
// Integer flag = 10;
// volatile关键字的使用
volatile Integer flag = 10;
public void change() {
this.flag = 20;
}
}