• JVM


    JVM 就是 Java 虚拟机, 是⽤来运⾏我们平时所写的 Java 代码的。 优点是它会⾃动进⾏内存管理和垃圾回收, 缺点是⼀旦发⽣问题, 要是不了解 JVM 的运⾏机制, 就很难排查出问题所在。

    JVM运行流程

    程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能

    JVM 运行时数据区(内存布局)

    堆和方法区是共享的。

    1.堆

    堆的作用:程序中创建的所有对象都在保存在堆中。

    堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。

    2.方法区

    方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。

    GC的时候,方法区叫永久代(jdk1.7),元空间(JDK1.8)

    1.7-》属于Java进程内存

    1.8-》-----系统内存

    3.Java虚拟机栈

    1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。

    2. 操作栈:每个方法会生成一个先进后出的操作栈。操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去

    3. 动态链接:指向运行时常量池的方法引用。假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。

    4. 方法返回地址:PC 寄存器的地址

    (1)栈的生命周期和线程相同:创建线程,就创建栈,销毁线程,就销毁这个线程对应的栈

    (2)线程执行某个方法,就创建该方法栈帧(入栈) ,方法返回,就出栈

    4.本地方法栈

    本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的

    5.程序计数器

    程序计数器的作用:用来记录当前线程执行的行号的。

    OOM (内存溢出) :

    指存放数据的大小,超出该区域的内存大小

    运行时数据区域中,除了程序计数器,其他都可能发生OOM

    内存泄露:

    线程生命周期太长,导致始终引用一些不使用的数据 (这些数据就没法gc垃圾回收),随着使用时间越来越长,不使用的垃圾就越来越多,可用空间越来越少一最后可能导致OOM

    GC (垃圾回收) :堆,方法区;其他没有

    线程共享的区域存在,私有的不存在:栈是线程销毁才回收,栈帧是某次方法调用后,返回才回收

    类加载的过程

    1.加载

    加载class字节码到java进程的内存中,在堆中创建类对象

    2.验证

    验证class字节码的规范(是否符合jvm规范)

    3.准备

    为static修饰的类变量分配内存,并设置初始值(0或null).

    4.解析

    把常量池中的符号引用,替换为直接引用(初始化常量的过程)

    了解:

    符号引用: class文件(字节码) private static intx = 123;此时进程还没有启动,就无法表示变量x指向1 23值(本质是指向内存地址)

    5.初始化

    类的初始化---执行静态变量的赋值,静态代码块。

    加载机制-双亲委派模型(JDK默认机制)

    如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

    (遵循双亲委派机制的类加载,类加载器不直接加载,而是委派给父类加载器,以此类推,达到从上到下进行类加载的方式)

    优点:1. 避免类的重复加载2. 避免Java的核心API被篡改

    缺点:不够灵活,无法加载不知道的类

    类加载的过程, 就是把 class 文件装载到 JVM 内存中, 装载完成以后就会得到一个 Class 对象, 我们就可以使用 new 关键字来实例化这个对象。

    加载器:包含4种,从上到下:

    BootStrap ClassLoader 启动类载器

    ExtClassLoader 扩展类加载器

    AppClassLoader 系统/应用类加载器

    自定义类加载器

    GC垃圾回收

    发生在堆(主要)、方法区(很少)

    垃圾回收,是回收堆中的对象=>对应=右边的对象

    判断是否是垃圾的算法

    1.引用计数算法

    一个对象被引用一次,计数器+1,如果计数器=0,就表示是垃圾,可以回收

    缺陷:无法解决循环引用问题=> 因此,JVM没有采取这个算法

    2.可达性分析算法

    通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",

    当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。

    了解: 4种引用类型

    1.强引用:普通的对象及=赋值,如Object o=new Object

    gc永远不回收强弓|用对象

    2.软引用: SoftReference xxx=new SoftReference(obj)

    内存溢出前,会回收这部分对象

    3.弱引用:

    gc发生时,都会回收

    4.虚引用

    无法使用,只是gc时,发起一个通知

    垃圾回收算法

    1.标记清除算法=>老年代回收算法

    分为两个步骤:

    (1)标记:标记对象

    (2)清除:回收垃圾对象

    缺陷:

    (1)效率:两个阶段效率都不高

    (2)内存碎片:清除后,产生大量的内存碎片= >剩余可用空间足够存放某个大对象,但连续空间不足存放,就无法存放

    2.标记整理算法=>老年代gc算法

    过程: (1) 标记 (2)整理:把存活对象往一端移动, 然后清理掉端外的空间

    3.复制算法=>新生代的回收算法

    将某个内存区域,划分为两块大小相同的空间,每次只使用其中一个,回收就是把存活对象复制到另一个不用的空间, 清除之前使用的空间

    JVM中,新生代的回收算法,是复制算法的优化版本

    内存划分为1个E区(Eden)和两个S区(Suvivor, SO, S1),每次使用E区和其中一块S区

    E区和S区在jvm中默认的比例是8:1,,,空间利用率就是90%

    说明:创建一个对象, 放在新生代,如果放不下,就触发新生代的gc

    一个对象在s区中,存活超过15 (默认值),就会进入老年代

    当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间

    4.分代回收算法=> jvm中,采取的算法

    没有具体的算法实现,只是把不同区域,采取不同的垃圾回收策略

    具体一-点: 内存(堆)划分为新生代(E区1+ S区2)和老年代,新生代采取复制算法,老年代采取标记清除/标记整理算法

    说明:

    创建对象,是放在哪块区域?--》新生代:默认创建的对象(非大对象)都进入新生代

    老年代:

    (1) 大对象:对象占据的空间超出jvm规定的阈值(可以设置,不设置就是默认的)

    (2)新生代中年龄超过15的对象: 对象在新生代,每经历- -次新生代的gc,还存活,年龄就+1

    (3)新生代gc时,分配担保失败的对象:新生代gc是把8(E区)+ 1(1个S区)中的存活对象复制到1(另-个S区)

    新生代对象的特性是朝生夕死,也就是说,大多数情况下,90%空间中存活对象,不足10% (另-块S区能放下)

    不是绝对的情况,如果放不下,就进入老年代(这个情况,就是分配担保失

    败=>老年代来担保,如果失败,就进入老年代)

    什么时候发生gc?

    对象进入哪个区域(新生代还是老年代),如果该区域空间不足,就会触发该区域的gc

    两个gc的特性:

    新生代gc:又叫minor gc,采取复制算法,效率比较高

    老年代gc:又叫major gc,采取标记清除/标记整理算法,效率比较差, - -般比新生代gc慢10倍以上

    垃圾回收器

    1.Serial收集器(了解)

    新生代收集器(复制算法)

    单线程(单个垃圾回收线程)的方案=>目前的大多数电脑都是支持多线程,所以这个收集器效率不高,也不怎么使用

    2.ParNew收集器(了解)

    新生代收集器(复制算法)

    多线程

    搭配CMS (老年代收集器)的方案

    3. Parallel Scanvenge收集器(了解)

    新生代收集器(复制算法)

    吞吐量优先= >适用性能优先的程序

    搭配Parallel Old (老年代收集器,也是吞吐量优先的)

    了解:这个收集器,具有某些特性: (1) 可控制的吞吐量(2) 自适应的调节策略

    4. Serial Old收集器(了解)

    老年代收集器(标记整理算法)

    单线程

    5. Parallel Old收集器(了解)

    老年代收集器(标记整理算法)

    吞吐量优先

    因此,在吞吐量优化的程序,目前只有一种选择:

    Parallel Scanvenge + Parallel Old

    6.CMS收集器(Concurrent Mark Sweep)

    1.老年代收集器

    2.标记清除算法

    3.用户体验优先= >整体看是并发(垃圾回收线程和用户线程同时执行)的过程,有局部的stw(少许时间是暂停用户线程的)

    =>此时,CMS-般是搭配新生代的ParNew收集器

    =>表现特性:并发收集,低停顿

    4.步骤:分为4个步骤

    (1)初识标记:标记GC Roots能直接关联的对象,需要STW

    (2)并发标记:进行GC Roots引用链追踪的过程(搜索引用路径)、

    (3)重新标记:修复第2个阶段用户线程同时执行时,产生标记变动的记录,需要STW

    (4)并发清除:并发清除垃圾

    第1, 3个阶段,都需要STW,但耗时比较少

    第2, 4个阶段,是并发,耗时也比较长

    5.缺陷

    (1) CPU比较敏感

    用户体验优先,就意味吞吐量稍微低-点(单次停顿时间短,整个停顿时间长- -点) =>CPU利用率下降

    (2)浮动垃圾问题(浮动垃圾:第4个阶段用户线程并发执行时产生的垃圾,在此次GC无法回收,称为浮动垃圾)

    会出现两个问题:

    1)需要预留一部分空间(并发清除阶段用户线程创建的对象)

    2)并发模式失败(Concurrent Model Failure)

    并发清除阶段用户线程创建的对象超出预留空间大小=>再次触发另- -次的老年代GC

    说明: CMS本身就是老年代GC,所以这里就是老年代gc时,又触发- -次老

    年代gc,而老年代gc是比较耗时(效率比较低)

    方式:采取老年代gc的后备方案: Serial Old收集器进行回收

    (3)内存碎片问题

    标记清除算法就会带来这个问题;内存碎片只是现象,对gc的影响是:可能导致提前触发gc

    说明:所有可用空间足够,但连续的可用空间不足存放大对象

    7.G1收集器= >全堆的收集器

    说明:使用G1,堆的内存划分,就不是一个新生代(E区1+S区2) 及-个老年代

    内存划分方案:把堆划分为多个相同大小的region区,动态分配为E区, S区,或T区(Tenured区, 老年代)

    1.老年代收集器

    2.全堆收集器= >整体看基于“标记整理算法”,局部看基于“复制算法"

    3.用户体验优先

    4.步骤

    新生代回收:回收多个E区+多个S区,复制存活对象到空的region区(动态指定它为S区)

    老年代回收:分为4个阶段

    (1)初识标记:和cms类似(标记GC Roots关联的对象,STW), 不同的是,可以和新生代gc同时执行

    (2)并发标记:和cms类似,多:优先回收(Garbage First, G1名词的由来)= >直接回收存活率低或几乎没有存活的region

    (3)最终标记:和cms类似

    (4)筛选回收:筛选存活率低的region回收

    用户体验优先:

    (1) ParNew+ CMS

    (2) G1

    JMM(Java内存模型)

    不全

    为了屏蔽硬件和操作系统内存访问差异

    三大特性:

    (1)原子性:是指8大字节码指令是原子

    (2)可见性:一个线程修改某个共享变量,是否对另- -个线程可见

    volatile, synchronized可以保证可见性

    (3)有序性:一个线程看自己的代码,都是有序的,看别的线程代码执行,都是无序的

  • 相关阅读:
    推荐一个拥有386万订阅者,10000多免费学习视频的频道
    动态规划解决01背包问题
    制作U盘启动盘
    Echarts画散点图
    免杀对抗-内存加载-shellcode转换-UUID+MAC+IPV4
    防御课第一次作业第一天笔记整理
    Qt发布exe软件及修改exe应用程序图标
    出现“线程无法访问非本线程创建的资源”的错误
    大数据在智慧城市的建设中起到了哪些作用?_光点科技
    C++ 强制类型转换(int double)、查看数据类型、自动决定类型、三元表达式、取反、
  • 原文地址:https://blog.csdn.net/qq_60991267/article/details/127443989