文章目录
JVM探究
-
请你谈谈JVM的理解?java8虚拟机和之前的更新变化?
-
什么时OOM,什么是栈溢出StackOverflowError?怎么分析?
-
JVM的常用调优参数有哪些?
-
内存快照如何抓取,怎么分析Dump文件?
-
谈谈JVM中,类加载器的认识?
1、JVM位置
在操作系统之上。与其他软件平行,Java程序运行在JVM之上
2、体系结构
Java通过类加载器将字节码文件加载到虚拟机中。
new的对象放在堆,引用的放在Java栈中
常量池在方法区中
比如Student s1 = new Student() , 具体实例的引用"s1"这个名字在java栈里面,具体的对象放在堆中!
栈、本地方法栈、程序计数器中一定没有垃圾,用完就会弹出去,不会有垃圾回收!
重点
jdk1.8之前
jdk1.8之后
3、类加载器
作用:加载class文件
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
双亲委派机制
当一个Hello.class这样的文件要被加载时。
- 首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。
- 如果没有,那么会拿到父加载器。父类中同理也会先检查自己是否已经加载过,如果没有再往上
注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。
- 直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
好处:安全,防止有人替换系统级别的类。
4、Native关键字
凡是带了native关键字的,说明java的作用范围达不到了,会去调用C语言的库!,会进入本地方法栈,调用本地方法本地接口 JNI(java native interface)。
JNI作用:扩展Java的使用,融合不同的编程语言为Java使用!
所以在内存区域中专门开辟了一块标记区域:Native Method Stack(本地方法栈),登记native方法,在最终执行的时候,加载本地方法库中的方法。
线程:线程级别的东西java处理不了,jvm虚拟机在操作系统之上,操作系统的操作java调不了,必须要用接口调用本地方法,所以new Thread.start()中有一个native的方法。
5、本地方法栈
见第四条
6、程序计数器(Program Counter Register)
每个线程都有一个程序计数器,是线程私有的,
7、方法区(Method Area)
方法区也被称为永久代!
方法区是被所有线程共享的,所有的字段和方法字节码,以及一些特殊的方法,如构造函数,接口代码也在此定义,简单来说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是,实例对象存在堆内存中,和方法区无关。
static final Class 常量池定义在方法区
注意:字符串常量池在1.8之后移到了堆中
JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
7.1 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
7.2 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
- JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
- JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代 。
- JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
8、深入理解 栈
为什么main()先执行,最后结束
程序运行的时候先把main方法压入栈,再是main方法中的其他方法,最后运行完最后弹出!
栈:栈内存,主管程序的运行,生命周期和线程同步。对于栈来说,不存在垃圾回收问题。
主要存的是八大基本类型+对象引用+实例的方法
栈满了:StackOverFlowError
9、栈、堆、方法区交互关系
- 绿色:栈
- 粉色:方法区
- 深蓝色:方法区中的常量池
- 淡蓝色:堆
new一个类的过程,就是往栈里放了一个引用,在堆中实例化类,引用指向堆中的实例。
10、堆(重点)
元空间:逻辑上存在,物理上不存在。
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
堆内存分为三个区域
- 新生区
– 伊甸园区
– 幸存者区1
– 幸存者区2 - 老年区
- 永久区(JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存)
GC垃圾回收,主要是在新生区和老年区。
假设内存满了,OOM,堆内存不够!
新生区
类诞生和生长的地方,甚至死亡。
永久区
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存。
11、出现OOM,如何解决(重点)
- 尝试扩大堆内存看结果
- 分析内存,看一下哪个地方出现问题。(专业工具)
- 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
MAT,Jprofiler作用
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象!
12、对象已死亡?
![img](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/11034259.png)
1、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
2、可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
13、GC
GC的作用区在堆、方法区中。
JVM在进行GC时,大部分时候都是对堆中的新生区进行回收。
GC分为:轻GC,重GC。
13.1 标记清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
13.2 复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
![复制算法](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/90984624.png)
13.3 标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
13.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
14、对象的创建过程(重要)
十分重要!
1、类加载器检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2、分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,需要掌握)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
3、初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5、执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
15、四种引用
强引用 > 软引用 > 弱引用 > 虚引用