深入理解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虚拟机(内存模型、类加载、垃圾回收、触发初始化、垃圾断定) JVM

 

 

*.java ——> (Javac 编译) ——> *.class 文件 ——> JVM编译、解释 ——> 机器可执行的二进制机器码

JVM内存模型

栈管运行、堆管存储

 

深入理解Java虚拟机(内存模型、类加载、垃圾回收、触发初始化、垃圾断定) JVM

其中,

(紫色)区域线程共有、发生GC;

(绿色)线程私有,内存很小,几乎不需要GC

程序计数器不会OOM,记录了方法之间的条用和执行情况。存储了指向下一条指令的地址,它是当前线程所执行的字节码的行号指示器。

方法区

供各线程共享的运行时内存区域。它存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。<——规范

不同虚拟机有不同的实现,如永久代、元空间。

注意: 实例变量存在堆内存中和方法区无关

栈:

主管Java程序的运行,随线程的创建而创建,结束时释放,对栈而言不存在垃圾回收

基本数据类型、对象的实例变量、实例方法都是在栈中分配。

主要存储:

  • 本地变量

  • 栈操作

  • 栈帧数据

  • 基本类型变量

  • 引用

深入理解Java虚拟机(内存模型、类加载、垃圾回收、触发初始化、垃圾断定) JVM

一个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文件的内容转换成方法区中的运行时数据结构,且类加载器只负责加载,是否可以运行有执行引擎决定。

深入理解Java虚拟机(内存模型、类加载、垃圾回收、触发初始化、垃圾断定) JVM

 

 

类加载器协同工作

实现机制: 双亲委派

双亲委派: 当类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是委派给自己的父类,找不到继续向上委派,到Bootstrap都加载不到,Bootstrap 就会向往下丢,向下找不到,知道最初发起加载的类加载去完成,若发起的这个加载类仍无法加载,丢ClassNotFounException!

沙箱安全: 用户定义的类不能污染JDK自带的类

深入理解Java虚拟机(内存模型、类加载、垃圾回收、触发初始化、垃圾断定) JVM

 

 

用户自定义类加载器

  • 集成ClassLoader

  • 重写加载方法ClassLoader();

  • 实例化class对象

虚线之上是虚拟机自带的加载器

  • Bootstrap 启动类加载器, C++实现

  • Extension 扩展类加载器, Java实现

  • Application 应用程序类加载器 Java 也叫 系统类加载器,加载当前应用的classpath下的所有类。

类一般成员——属性的初始化

在加载时进行默认初始化,在初始化时进行赋值

局部变量不会在加载时进行默认初始化,必须显示赋值

 

类加载(懒加载)

加载 ——> 连接 ——> 初始化 ——> 使用 ——> 卸载

连接阶段的主备阶段会给变量赋默认值

连接: 验证 ——> 准备 ——> 解析

验证: 文件格式、元数据、字节码 符号引用

准备: 正式为类变量分配内存并设置初始值;如果被final修饰,常量值同步指定为声明的值(一步到位)。

解析:常量池中的符号引用替换为直接引用

初始化:类加载的最后一步 执行clinit()方法

触发初始化

  • 遇到new、getStatic、putStatic或invokeStatic四条字节码指令时,访问final变量除外,如果类没有进行初始化,触发;

  • 对类反射调用时,未初始化,触发

  • 初始化子类时,父类未初始化,触发

  • JVM启动时,用户需指明一个执行的主类,虚拟机会初始化这个主类

不被初始化

  • 通过子类引用父类的静态字段

  • 数组定义引用类 如:类[] c = new 类[10];

  • 类的常量