超详解 JVM 中重点内容,对整个底层体系更进认知
Posted 满眼*星辰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超详解 JVM 中重点内容,对整个底层体系更进认知相关的知识,希望对你有一定的参考价值。
JVM
JVM概念
JVM(Java Virtual Machine的简称。意为Java虚拟机
虚拟机:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统
java能够一次编写,到处运行的关键(JVM兼容处理)【Linux,macos,windows】
常见虚拟机:
第一: HotSpot JVM
第二: IBM J9 JVM
JVM 布局(HotSpot)
JDK 1.8
1. 堆
new Object() 所有对象都是存在此区域,此区域也是 JVM 中最大的一块区域
JVM 的垃圾回收就是针对此区域
堆划分:新生代、老年代
-
新生代:第一次创建的对象都会分配到此区域
-
老年代:经历了一定的垃圾回收之后,依然存活下来的对象会移动到老年代;大对象在创建的时候也会直接进入老年代。
为什么大对象会直接进入老年代?
核心原因是大对象的初始化比较耗时,如果频繁的创建和销毁,会带来一定的性能开销,因此最好的实现方式是将他存放到 GC 垃圾回收频率更低的老年代
新生代区域划分
- Eden:80% 内存
- S0:10% 内存
- S1:10% 内存
新生代内存的利用率就可以达到90%
Eden + S0 / Eden + S1
新生代 -> 老年代
HotSpot 默认的执行次数是 15 次,经历 15 次 GC 就会从新生代转到老年代。
2. JVM 栈(Java虚拟机栈)
a)局部变量表:8 大基础数据类型,对象的引用
b)操作栈:每个方法都会对应一个操作栈
c)动态连接:指向常量池的方法引用
d)方法返回地址:PC 寄存器的地址
进栈出栈代码演示
public class ThreadDemo102
public static void main(String[] args)
System.out.println("执行了main方法");
methodA();
private static void methodA()
System.out.println("执行了方法A");
methodB();
private static void methodB()
System.out.println("执行了方法B");
methodC();
private static void methodC()
System.out.println("执行了方法C");
3. 本地方法栈
它与 JVM 栈比较类似,只不过 JVM 栈是给 java 和 JVM 使用,而本地方法栈它是本地方法(c / c++)服务。
4. 程序计数器
用来记录线程执行的行号
5. 元空间(JDK 1.8)/ 方法区(JDK 1.7)
运行时常量信息、字符串常量池、类的元信息 等(JDK 1.7 的时候的方法区)
JDK 1.8 元空间:本地内存,并且将字符串常量池移动到堆
内存线程共享与私有
线程共享:堆、元空间
线程私有:JVM 栈、本地方法栈、程序计数器是
JVM参数调优
在 idea.exe.vmoptions 文件进行配置
-Xmx10m :堆最大容量
-Xms10m :堆最小容量设置
通常情况下可以将 Xmx 和 Xms 的大小设置相同,这样可以防止堆扩容所带来的抖动
JVM 调优的时候可以设置的参数类型:
- -X:非标准的参数设置,它只能针对特殊 HotSpot 生效(-Xms10m)
- -XX:标准参数设置,它针对所有的 HotSpot 都生效
- -D:设置应用程序的参数(-Dmykey=value)
JVM类加载机制(class loading)
1. 加载
(Loading)【去机场】
- 根据类路径全名加载二进制流
- 将静态的存储结构转换成运行时的数据结构
- 在内存中生成一个此类的方法入口
(将静态文件转成运行内存)
2. 效验
【安检】
- 文件格式进行效验
- 字节码效验
- 元数据
- 符合引用
等针对正确性和安全性进行效验
3. 准备
【等待飞机起飞】
将类的静态变量在内存中进行分配,这里只赋予默认值
比如这样一行代码:
public static int count = 123;
注意此时只会在内存中生成一个 count=0 变量,对变量类型的初始化
将类中的静态变量在内存中进行分配
4. 解析
【根据机票找座位】
- 字面量:String str = “abc”,abc就是字面量
- 符号引用:类、方法的完全限定名(全路径名称)
- 直接引用:将符号引用加载到内存中(根据引用指向内存中的对象)
初始化final修饰的常量
将常量池中的符号引用替换为直接引用(内存地址)的过程
5. 初始化
【准备起飞】
为类的静态变量赋初值
此步骤开始将执行权从 JVM 转移到自己写的程序,开始执行构造函数
6. 使用
【起飞】
7. 卸载
【落地】
JVM 双亲委派模型
当加载一个类的时候,那么这个类不会直接加载,而是将这个加载任务直接交给父类。当找不到父类的时候,才自己尝试去加载
优点:
- 唯一性(父类执行加载一次)
- 安全性(会往上找生层的类是系统提供的类,避免加载自定义的类,从而一定程度上保证了安全性)
破坏双亲委派模型
(3次)
- JDK 1.2 提出的双亲委派模型,为了兼容老版本,因此在 JDK 1.2 的时候已经出现了破坏双亲委派模型的场景。
- 是因为双亲委派模型自身的缺点而导致的,比如在父类当中要调用子类的方法是没办法实现
- 人们对于热更新的追求,导致了双亲委派模型的又一次破坏
垃圾回收
1. 判别死亡对象(垃圾)
a)引用计数器算法
给每个对象创建一个计数器,当有程序引用此类的时候,计数器 +1,不使用的时候计数器 -1,当计数器为 0 时,则表示此对象没人用,那么就可以将它归为死亡对象,等待垃圾回收器回收
引用计数器算法缺点:
它会有循环引用的问题
public class Work
private byte[] bytes = new byte[5*1024*1024];
public Work obj = null;
public static void main(String[] args)
Work w1 = new Work();
Work w2 = new Work();
//循环引用
w1.obj = w2;
w2.obj = w1;
//释放对象
w1 = null;
w2 = null;
//强制垃圾回收
System.gc();
打印垃圾回收详情信息
执行结果:
所以HotSpot 默认的垃圾回收器使用的不是引用计数器算法
b) 可达性分析算法
HotSpot 默认使用的算法
如果对象的父类一直往上最后是 GC Roots,则不是垃圾对象,否则是垃圾对象,如上图
可以作为GC Roots的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法去中常量引用的对象
- 本地方法栈中 JNI(Native 方法) 引用的而对象
2. 垃圾回收算法
标记清除算法
标记:可达性分析(存活的对象和死亡的对象)
黑色:标记对象
浅色:不是垃圾
白色:空内存
缺点:内存碎片
比如要创建一个数组,数组的内存空间必须是连续的,这种算法使得有很多内存碎片没有办法连续的使用内存。
复制
优点:内存性能比较高
缺点:内存利用率低
因为98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,为了解决内存利用率低,就分为将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,分别占80%,10%,10%,以此来提高利用率
标记整理算法
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步
骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的
内存。
3. 垃圾回收器
Serial
单线程串行的垃圾回收器(复制算法)
Serial Old
老年代垃圾回收器(标记-整理算法)【串行GC】
PerNew
是 Serial 的多线程版本(并行垃圾回收器)
Parallel Scavenge
并行垃圾回收器(新生代)
以吞吐量作为主要依据进行垃圾回收
场景:后端系统
Parallel Old
并行垃圾回收器(老生代)
CMS
并发
(标记-整理算法)
- 初始标记,简单的标记一下,STW小
- 并发标记,和用户线程一起执行,
- 重新标记,重新标记并发中用户线程产生的垃圾,STW小
- 并发清理,
- 循环,跳转到1
适用场景:BS,用户交互系统
G1
JDK11是默认的垃圾回收器
G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标
记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的
一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区
域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域
主要用于存储大对象-即大小超过一个region大小的50%的对象
JMM
Java内存模型(让 JVM 高速运行的一种技术)
解决不同操作系统在操作内存时的性能差异。
存储构成
速度:寄存器 》 L1缓存 》 L2缓存 》主内存
主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中
保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内
存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线
程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示 :
内存间交互操作
- lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
- unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可
以被其他线程锁定。 - read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随
后的load动作使用。 - load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量
副本中。 - use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
- assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
- store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的
write操作使用。 - write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量
中。
JMM 三大特征
- 原子性:要么全部成功,要么全部失败
- 可见性:内存可见性
- 有序性:操作有执行顺序
volatile
- 保证此变量对所有线程的可见性
- 使用volatile变量的语义是禁止指令重排序
以上是关于超详解 JVM 中重点内容,对整个底层体系更进认知的主要内容,如果未能解决你的问题,请参考以下文章