深入理解Java虚拟机(内存模型、类加载、垃圾回收、触发初始化、垃圾断定) JVM
深入理解Java虚拟机
本文针对JDK 1.8,仅作抛转引玉,因水平有限,文章或许有不足之处,望您不吝指出!
什么是JVM?
Java虚拟机(Java Virtual Machine)是运行Java字节码的虚拟机!对于不同的操作系统有特定的实现,相同的字节码(*.class 文件),它都会给出相同的结果。
*.class 文件 <—— javac 编译源文件
一次编译,随处运行
字节码 + (不同系统的)虚拟机是实现“一次编译,随处运行”的关键;
Java基本类型
byte、short、boolean、char、int、float、double、long
在Java体系中、基本类型占用的存储空间不受硬件架构的变化的影响。
构造器——constructor
可以重载、不可重写
重载: 方法名相同,参数列表不同(参数类型、参数个数、参数顺序至少一个不同),返回值类型、访问修饰符可以不同
重写: 方法名相同,对父类允许访问的方法的实现过程重新编写。发生在子类中,参数列表必须一致,返回值&抛出异常范围不大于父类;访问修饰符不小于父类。
区别 | -重载 | -重写 |
---|---|---|
英文 | Overload | Ooverride |
定义 | 方法名称相同,参数列表不同 | 方法名称、参数列表、返回类型一致 |
范围 | 同一类中 | 子类中 |
权限 | 不受权限控制 | 访问权限不小于父类 |
继承是子类使用父类的方法,而多态是父类使用子类重写的方法。
JVM体系结构(JVM运行在操作系统之上,与硬件无直接交互)
*.java ——> (Javac 编译) ——> *.class 文件 ——> JVM编译、解释 ——> 机器可执行的二进制机器码
JVM内存模型
栈管运行、堆管存储
其中,
(紫色)区域线程共有、发生GC;
(绿色)线程私有,内存很小,几乎不需要GC
程序计数器不会OOM,记录了方法之间的条用和执行情况。存储了指向下一条指令的地址,它是当前线程所执行的字节码的行号指示器。
方法区
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。<——规范
不同虚拟机有不同的实现,如永久代、元空间。
注意: 实例变量存在堆内存中和方法区无关
栈:
主管Java程序的运行,随线程的创建而创建,结束时释放,对栈而言不存在垃圾回收。
基本数据类型、对象的实例变量、实例方法都是在栈中分配。
主要存储:
-
本地变量
-
栈操作
-
栈帧数据
-
基本类型变量
-
引用
堆
一个JVM只有一个堆内存,堆内存的大小可调节,类加载器读取了class文件之后,需要把类方法、常量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑分三部分: 新生代:老年代:元空间
新生区由 Eden、from、to组成 三者占比8:1:1
伊甸园区和幸存区(from、to)
新生区记录类的诞生、成长、消亡
元空间
本质与永久代相似,不再在虚拟机内存中分配,直接在物理内存中分配
垃圾回收过程
当伊甸园空间用完时,程序继续创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC), Eden区中的不再被其他对象引用的对象进行销毁,被引用的移动到幸存区的From区(0区);
再次触发Minor GC (复制 -> 清空 -> 互换) 时,0区和Eden区同时垃圾回收,将幸存的移到To区,如此往复,当移动15次后依然不能被回收,将幸存了15次的对象移到养老区;
当养老区满了,触发Major GC(Full GC ), 若Full GC后仍然无法保存对象,就会产生OOM。
survivor from 复制到survivor to 年龄+1;
-
Eden满 ——> Minor GC ——> 幸存的拷贝到 survivor from 区
-
Eden再满 ——> Eden & survivor from ——> Minor GC ——> 幸存的拷贝到 survivor to 区
复制之后有交换,谁空谁是To
垃圾回收时,确定对象无引用时,调用finalize()方法。
产生OOM的原因
-
堆内存设置不足
-
代码中创建了大量的对象,且长时间不能被垃圾收集器收集
打印详细的垃圾回收日志
-XX: +PrintGCDetails
如何确定垃圾?
-
引用计数 数字为0时,无任何引用(不能解决循环依赖)
-
根可达
常见的垃圾回收算法
-
标记清除
-
拷贝
-
标记压缩
调整Java堆内存大小 -Xms,-Xmx
-Xms: 设置初始内存分配大小,默认为物理内存的1/64
-Xmx: 设置最大内存分配,默认为物理内存的1/4
JMM关于同步规定
-
线程解锁前,必须把共享变量刷回到主内存
-
线程加锁前,读取主内存中的最新值到线程的工作内存
-
加解锁是同一把锁
类加载器
负责加载class文件(cafe babe开头)到内存,并将class文件的内容转换成方法区中的运行时数据结构,且类加载器只负责加载,是否可以运行有执行引擎决定。
类加载器协同工作
实现机制: 双亲委派
双亲委派: 当类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是委派给自己的父类,找不到继续向上委派,到Bootstrap都加载不到,Bootstrap 就会向往下丢,向下找不到,知道最初发起加载的类加载去完成,若发起的这个加载类仍无法加载,丢ClassNotFounException!
沙箱安全: 用户定义的类不能污染JDK自带的类
用户自定义类加载器
-
集成ClassLoader
-
重写加载方法ClassLoader();
-
实例化class对象
虚线之上是虚拟机自带的加载器
-
Bootstrap 启动类加载器, C++实现
-
Extension 扩展类加载器, Java实现
-
Application 应用程序类加载器 Java 也叫 系统类加载器,加载当前应用的classpath下的所有类。
类一般成员——属性的初始化
在加载时进行默认初始化,在初始化时进行赋值
局部变量不会在加载时进行默认初始化,必须显示赋值
类加载(懒加载)
加载 ——> 连接 ——> 初始化 ——> 使用 ——> 卸载
连接阶段的主备阶段会给变量赋默认值
连接: 验证 ——> 准备 ——> 解析
验证: 文件格式、元数据、字节码 符号引用
准备: 正式为类变量分配内存并设置初始值;如果被final修饰,常量值同步指定为声明的值(一步到位)。
解析:常量池中的符号引用替换为直接引用
初始化:类加载的最后一步 执行clinit()方法
触发初始化
-
遇到new、getStatic、putStatic或invokeStatic四条字节码指令时,访问final变量除外,如果类没有进行初始化,触发;
-
对类反射调用时,未初始化,触发
-
初始化子类时,父类未初始化,触发
-
JVM启动时,用户需指明一个执行的主类,虚拟机会初始化这个主类
不被初始化
-
通过子类引用父类的静态字段
-
数组定义引用类 如:类[] c = new 类[10];
-
类的常量