JVM初探总结
-2022.3.25 -BDY
文章目录
前言
本次总结是对看完黑马程序员的JVM视频的一次小总结,看完视频也不清楚学了多少,也没有一个清楚的认识,所以想到写一个博客总结一下,主要会摘抄别人的笔记内容。PS:tmd连个大纲都不清楚,还写个屁。
黑马程序员JVM
https://www.bilibili.com/video/BV1yE411Z7AP?spm_id_from=333.999.0.0
大佬的JVM笔记
https://nyimac.gitee.io/2020/07/03/JVM%E5%AD%A6%E4%B9%A0/
第二个大佬的JVM笔记
https://blog.csdn.net/weixin_50280576/article/details/113742011
一、什么是JVM?
定义
java virtual machine ,java程序的运行环境(java二进制季节码的运行环境)
什么是java二进制字节码的运行环境?-> JVM
好处
1.一次编写,到处运行
2.自动内存管理,垃圾回收机制
3.数组下标越界检查
1.这也是java的好处,编写成jar包后,只要有jvm就可以在电脑上运行
2.涉及JVM内存管理,JVM垃圾回收
3.数组下标越界检查
->
二、内存结构
整体架构和定义
JVM内存结构主要包括:方法区(method area), 堆(heap),虚拟机栈(JVM stacks),程序计数器(PC register),本地方法栈(Native Method Stacks)
方法区(method area)
在jdk1.8之后,方法区会移动到本地内存空间,称为元空间(1.6之前为永久代),JVM中只保存方法区的引用,方法区中的串池也会移动到堆中存储
- 内存溢出OutOfMemoryError
1.8以前会导致永久代内存溢出
1.8以后会导致元空间内存溢出 - 常量池
二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)
下面是方法区常量池的实例:
运行时常量池:
常量池是.class文件中的,当该*类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址** - 串池StringTable
intern方法1.8
如果串池中没有该字符串对象,则放入成功
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
StringTable调优
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,(减少hash碰撞),来减少字符串放入串池所需要的时间
-XX:StringTableSize=xxxx
堆(heap)
- 定义
通过new关键字创建的对象都会被放在堆内存
PS:在堆中放入的是创建的对象,在方法区中是对象的引用。 - 特点
1.线程共享
2.有垃圾回收机制
3.堆和方法区同样都是共享区 - 堆内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出
虚拟机栈(JVM stacks)
- 定义
1.每个线程运行需要的内存空间,称为虚拟机栈
2.每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存,每个方法对应一个栈帧。
3.每个线程只能有一个活动栈帧,对应着当前正在执行的方法。
- 问题
- 内存溢出
Java.lang.stackOverflowError 栈内存溢出
原因:栈帧过多(无限递归),栈帧过大。
程序计数器(PC register)
- 作用
用于保存JVM中下一条所要执行的指令的地址 - 特点
1.线程私有
当线程时间片用完再从新获取时间片后,通过程序计数器可以知道要执行哪条代码。
2.不会存在内存溢出
本地方法栈(Native Method Stacks)
一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法
直接内存
三、垃圾回收
1.判断垃圾是否可回收(垃圾回收原理)
- 引用计数法
顾名思义,当对象被引用后,计数加一,消除引用,计数减一
弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放 - 可达性分析算法
这是JVM使用的算法。
方法:扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
补充:
- 五种引用
强引用
只有GC Root都不引用该对象时,才会回收强引用对象
如上图B、C对象都不引用A1对象时,A1对象才会被回收
软引用
当GC Root指向软引用对象时,在内存不足时,会回收软引用所引用的对象,只有当内存不足时才会回收
如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收
软引用的使用
public class Demo1 {
public static void main(String[] args) {
final int _4M = 4*1024*1024;
//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
}
}
如果想要清理软引用,需要使用引用队列
大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)
弱引用
只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象
如上图如果B对象不再引用A3对象,则A3对象会被回收
弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference,在是否回收时有区别
虚引用
当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,调用虚引用的方法
虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存
如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存
终结器引用
所有的类都继承自Object类,Object类有一个finalize方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以被垃圾回收了
如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了
引用队列
1.软引用和弱引用可以配合引用队列
在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象
2.虚引用和终结器引用必须配合引用队列
虚引用和终结器引用在使用时会关联一个引用队列
2.垃圾回收算法(垃圾回收过程)
- 标记—清除
定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间,这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。
缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢
- 标记—整理
**定义:**标记-整理 会将不被GC Root引用的对象回收,清楚其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题
**缺点:**但是因为整体需要消耗一定的时间,所以效率较低
- 复制
将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间
3.分代回收(垃圾回收过程)
流程:
1.新创建的对象都被放在了新生代的伊甸园中
2.当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC
Minor GC 会将伊甸园和幸存区FROM存活的对象先复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区
3.再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1
4.如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中
如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收
补:GC分析
4.垃圾回收器(垃圾回收的几种垃圾回收器使用)
1.相关概念
2.回收器
5.GC调优(垃圾回收器调优)
1.调优领域
2.目标
3.最好的GC是不发生GC
4.新生代调优
特点:
所有的new操作分配内存都是非常廉价的
TLAB
死亡对象回收零代价
大部分对象用过即死(朝生夕死)
MInor GC 所用时间远小于Full GC
关于新生代内存是否越大越好?
不是
新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
5.幸存区调优
改变晋升阈值,改变幸存区大小
幸存区需要能够保存 当前活跃对象+需要晋升的对象
晋升阈值配置得当,让长时间存活的对象尽快晋升
6.老年代调优
更改老年代内存区大小
四、类加载和字节码技术
类加载流程图:
1.类文件结构
类的字节码文件:
类文件结构
PS:面试不考,再见!
2.字节码指令(各种类型分析)
了解类加载流程
代码:
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
1.常量池载入运行时常量池
2.方法字节码载入方法区
3.开始执行字节码
具体流程请看转载:
大佬的JVM笔记
https://nyimac.gitee.io/2020/07/03/JVM%E5%AD%A6%E4%B9%A0/
4.构造方法
cinit()V
public class Demo3 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(i); //结果为30
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V
init()V
public class Demo4 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo4(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo4 d = new Demo4("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后
cinit和init方法区别:
一个在类加载准备阶段,一个在new对象之后
加载方法类型不同,一个是静态代码块,一个是{}代码块和成员变量
5.方法调用
public class Demo5 {
public Demo5() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.test1();
demo5.test2();
demo5.test3();
Demo5.test4();
}
}
6.多态原理
多态原理
有亿点点难
7.异常处理(try—catch)
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
3.编译器处理
经典:语法糖
所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换**的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
1.构造函数
public class Candy1 {
}
public class Candy1 {
//这个无参构造器是java编译器帮我们加上的
public Candy1() {
//即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
super();
}
}
2.自动拆装箱
public class Demo2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
public class Demo2 {
public static void main(String[] args) {
//基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
//包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
3.泛型集合取值
4.可变参数
public class Demo4 {
public static void foo(String... args) {
//将args赋值给arr,可以看出String...实际就是String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args
public class Demo4 {
public Demo4 {}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
5.foreach
foreach转为for
public class Demo5 {
public static void main(String[] args) {
//数组赋初值的简化写法也是一种语法糖。
int[] arr = {1, 2, 3, 4, 5};
for(int x : arr) {
System.out.println(x);
}
}
}
public class Demo5 {
public Demo5 {}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i=0; i<arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
如果是集合使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator.
6.switch字符串
public class Demo6 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
public class Demo6 {
public Demo6() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
//通过字符串的hashCode+value来判断是否匹配
switch (str.hashCode()) {
//hello的hashCode
case 99162322 :
//再次比较,因为字符串的hashCode有可能相等
if(str.equals("hello")) {
x = 0;
}
break;
//world的hashCode
case 11331880 :
if(str.equals("world")) {
x = 1;
}
break;
default:
break;
}
//用第二个switch在进行输出判断
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
default:
break;
}
}
}
7.其余类型
4.类加载阶段
加载-链接-验证-准备-解析-初始化
一、加载
1.将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法
2.如果这个类还有父类没有加载,先加载父类
3.加载和链接可能是交替运行的
4.nstanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
5.InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址
6.类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
二、链接
三、验证
验证类是否符合 JVM规范,安全性检查
四、准备
为 static 变量分配空间,设置默认值
五、解析
未解析时,常量池中的看到的对象仅是符号,未真正的存在于内存中
解析以后,会将常量池中的符号引用解析为直接引用
六、初始化
发生时机
类的初始化的懒惰的,以下情况会初始化:
验证类是否被初始化,可以看改类的静态代码块是否被执行
5.类加载器
1.定义:
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
2.启动类加载器Bootstrap ClassLoader
3.拓展类加载器Extension ClassLoader
4.应用程序类加载器Application ClassLoader
5.自定义加载器
6.双亲委派机制和沙箱安全机制
双亲委派机制:
解释:
当一个类收到了类的加载请求,他首先不会自己去加载这个类,而是把这个请求委派给父亲去完成,每一层的类加载器都是如此,只有当父类加载器反馈自己无法完成这个请求的时候(在他的加载的路径下没有找到所需要加载的class),子类加载器才会尝试自己去加载。
优点
比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都会委托给顶层的启动类(BootStrap Class Loader)进行加载这样就保证了不同的加载的类加载器最终得到的都是同样的一个Object 对象
沙箱安全机制:
解释:
因为有双亲委派机制,也就是会在Java的BootStrap Class Loader 加载的jar包中寻找,在该jar包下就会找到一个java.lang.String 的类,此时就会停止在子级中寻找(先到先得原则),但是在该类中并未找到main方法,所以运行时会报错。
这样就保证了Java的出厂源码不会受到开发人员编写的污染(沙箱安全机制)
6.运行期优化
1.分层编译
JVM 将执行状态分成了 5 个层次:
即时编译器(JIT)与解释器的区别
逃逸分析
逃逸分析优化
1.锁消除
2.标量替换
3.栈上分配
2.方法内联
不建议现在学习!!!
后面再说
3.反射优化
不建议现在学习!!!
后面再说
五、内存模型
主要是关于JUC中的知识,详情等学完JUC再来补充