JVM——运行时数据区、双亲委派模型、垃圾回收算法、垃圾收集器

1、堆(heap)

程序中所有创建的对象(数组也是对象)都存放在堆中

  • 对象指的是等号右边的;
  • 变量指的是等号左边的。

注:jdk1.8,常量池(字符串常量池)也在堆中。

JVM参数设置:

  • -Xms最小堆内存;
  • -Xmx最大堆内存

堆里面分为两个区域(逻辑上):新生代老生代。新生代放新建的对象,经过一定GC次数之后仍然存活的对象以及创建的大对象会放入老生代。

垃圾回收的时候会将Eden中存活的对象放在一个未使用的Survivor中,并把当前正在使用的Eden和Survivor清理掉。

2、方法区

用来存放被虚拟机加载的类信息(class字节码,进行类加载,把类加载到方法区,保存类的代码数据,同时堆中会生成一个类对象)、常量、静态变量、即时编译器编译后的代码等

  • 永久代(JDK1.7):属于Java内存进程;
  • 元空间(JDK1.8):内存属于本地内存,不再受JVM最大内存的影响,与本地内存的大小有关。
  • javac:编译器(编译Java代码为字节码文件);
  • java:包含解释器(Java程序运行的时候,把class字节码文件翻译为机器码);
  • 即时编译器:运行时,将热点代码翻译为机器码,之后就不用再进行翻译。
3、Java虚拟机栈
  • Java虚拟机栈的生命周期和线程相同,
  • 每个方法执行的时候都会创建一个栈帧,进行入栈操作,方法返回时,出栈;包含局部变量表、操作栈、动态链表、方法返回地址。

Java虚拟机栈:

  • 局部变量表:**存放八大基本数据类型、变量(等号左边)。**局部变量表所需的空间在编译时分配。进入一个方法时,方法在帧内分配的局部变量空间是确定的,执行期间局部变量表的大小不会发生改变。
  • 操作栈:每个方法会生成一个先进后出的操作栈;
  • 动态链表:指向运行时常量池的方法引用;
  • 方法返回地址:PC寄存器的地址。
4、本地方法栈

**调用Java方法,就创建栈帧,**放在线程的Java虚拟机栈中;如果调用其他的函数,就是用本地方法栈。

5、程序计数器

记录当前线程执行的行号。

6、内存布局中的异常问题
  1. 内存溢出(OOM):指存放数据的大小超出该区域的内存大小。运行时数据区域中,除了程序计数器,都可能发生内存溢出。内存溢出会导致整个进程都挂掉;
  2. 内存泄漏:线程生命周期太长,始终引用一些不使用的数据(没法进行gc垃圾回收),随着使用时间越来越长,不适用的垃圾就越来越多,可用空间越来越少——可能导致OOM。(解决方法:直接重启)
  3. StackOverflow:如果栈中的栈帧数量超过jvm规定,就会出现该异常。(递归使用不恰当)

三、类加载

1、类加载过程

(1)加载

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

(2)验证

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

(3)准备

正式为类中的变量(静态变量)分配内存并对类变量进行初始化:

  • static常量:赋值为初始值;
  • static变量:赋值为缺省值;(Integer缺省值为null,int为0)
(4)解析

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

  • 符号引用:class文件(字节码)private static int x=123;此时进程还没有启动,无法表示变量x指向123(本质是指向内存地址),此时使用符号引用来表示这个指向关系;
  • 直接引用:进程运行起来,x直接指向123(内存地址);
  • 替换:把class文件常量池中的符号引用,替换为进程运行起来的运行时常量池中的直接引用。
(5)初始化

是类的初始化,不是对象初始化。初始化阶段,Java虚拟机真正开始执行类中编写的Java程序代码。初始化阶段就是执行类构造器方法的过程。

类对象的初始化:执行静态变量和静态代码块。

2、双亲委派模型

(1)什么是双亲委派模型

类加载的机制:双亲委派模型(jdk默认的类加载机制),其它机制(破坏双亲委派模型的其它机制)。

**类加载器:**包含四种,从上而下(上下关系,不是以extends继承来实现的,是逻辑上的上下关系):

  • BootStrap ClassLoader   启动类加载器(主要负责加载Java核心类库,即%JRE_HOME%\lib目录)
  • ExtClassLoader   扩展类加载器(主要负责加载目录%JRE_HOME%\lib\ext目录下的类)
  • AppClassLoader  系统/应用类加载器(加载当前应用的classpath目录下的类)
  • 自定义加载器

启动/扩展/应用这三类类加载器,只是加载jar目录下不同的jar包。

什么是双亲委派模型?

基于四种类加载器,按照从上到下的顺序,来加载类。类加载器收到类加载请求时,不会立即去自己加载,而是将这个请求向上传递,每一层都是如此,因此所有的加载请求最终应该在最顶层的启动类加载器中。因为类加载只执行一次,所以,上边找到,下边就不执行加载;上边没有找到,就交给下一级的加载。

(2)双亲委派模型的优点
  • 避免重复加载类:如A类和B类都有一个父类C,当A启动起来的时候就会加载C,则B类进行加载时就不需要重复加载C类。
  • 安全性:确保优先采用启动/扩展/应用类加载器来加载类。
    如果自定义一个类加载,加载自定义的java.long.Object类:
    【1】遵循双亲委派模型,就不会加载到自定义的Object,还是jre中的(安全);
    【2】不遵循双亲委派模型,就可以加载到自定义的Object类(不安全)。

【注】在jdk中,自定义类加载器时,进行了安全校验:加载类,如果类的全限定名以java./javax.开头的包,报错。

(3)破坏双亲委派模型

遵循双亲委派机制的类加载,某些场景下,可能无法实现知道需要加载的类名(jdbc中,jdk是无法知道数据库驱动类的类名)。

解决方案:破坏双亲委派机制。

常用的方案:SPI机制(jdk提供的一个ServiceLoader.load,约定了从jar包下/META-INF/services/文件名  中进行加载)

四、垃圾回收(GC)

  • Java语言,不用自己分配内存,也不用自己回收内存(jvm中,实现了垃圾回收机制,自动回收)。
  • 垃圾回收区域:堆(回收的主要区域)、方法区(保存类信息,静态变量,很少回收);

1、死亡对象判断算法

(1)引用计数算法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器-1;任何时刻计数器为零的对象不能再被使用,即对象已死。**但jvm中没有选用引用计数器来管理内存:引用计数器无法解决对象的循环引用问题。**如下:

//此时instance的引用计数器会陷入死循环
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
(2)可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点往下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有引用链相连时,证明是“不可达的”

引用的分类(强度依次递减):

  • 强引用:只要引用还在,就不会被回收;
  • 软引用(SoftReference):系统要发生内存溢出之前,会对其及逆行回收;
  • 弱引用:每次GC发生时都会进行回收;
  • 虚引用:无法使用,只是在对象被回收时发起一个系统通知。

2、垃圾回收算法

(1)标记清除算法(老年代回收算法)

算法分为两个阶段:

  • 标记:标记出所有需要回收的对象;
  • 清除:标记完成后统一回收所有被标记的对象。

缺陷:

  • 效率低:标记和清除过程效率都不高;
  • 清除后产生大量不连续的内存碎片,导致在之后的运行中需要分配较大对象时,无法找到足够的连续内存不得不提前进行下一次垃圾回收。
(2)复制算法(新生代算法)

复制算法解决了”标记-清理“的效率问题。将可用的内存按容量分为大小相等的两块,每次只使用一块。当内存需要进行垃圾回收时,会将该区域存活的对象复制到另一块上面,然后将已经使用过的内存区域清理掉。

**使用场景:**大多数对象具有朝生夕死的特性。新生代的对象符合该特性,使用复制算法。

**缺陷:**空间利用率不高,只有50%。

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

  1. 新生代中98%以上的对象都是“朝生夕死”的,所以不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的Survivor区(8:1:1);
  2. 每次使用Eden和一块Survivor。回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间,最后清理掉Eden和使用的Survivor空间。
  3. 部分对象会在两个Survivor区域来回复制,如此交换15次(默认15)之后如果还存活,会vu你放到老年代。
(3)标记整理算法(老年代算法)

复制收集算法在对象存活率较高时会及逆行大量的赋值操作,效率比较低。因此来年代一般不使用。标记整理算法过程和标记清除算法一致,不同的是,标记完后,不直接进行清除,而是将存活的对象移动到一端,最后清除掉端边界以外的内存。

(4)分代算法

分代算法(没有具体的算法实现)是通过区域划分,实现不同的区域不同的垃圾回收策略,从而更好地实现垃圾回收。JVM垃圾收集都采用的是“分代收集”算法,只是根据对象的存活周期的不同将内存分为几块。一般是分为新生代和老年代。新生代中,每次垃圾回收只有少量的对象存活,使用复制算法;老年代中,对象存活率高,使用标记清除算法或标记整理算法。

哪些对象进入新生代?哪些对象进入老年代?

  • 默认创建的对象(非大对象)都进入新生代;
  • 老年代:
    【1】大对象:对象占据的空间超出jvm规定的阈值;
    【2】新生代中年龄超过15的对象。
    【3】新生代GC时,分配担保失败的对象:新生代GC时把Eden区域和s0中的存活对象复制到s1区域中。(新生代对象的特点是朝生夕死,大多数情况下,存活的对象不足10%,但是也有可能s1区域中放不下,此时就放入老年代中。

什么时候发生垃圾回收?

对象进入哪个区域,如果该区域空间不足,就会触发该区域的GC。

Minor GC和Major GC有什么区别?

  • 新生代GC/Minor GC:采用复制算法,效率比较高;
  • 老年代GC/Major GC:采用标记清除算法/标记整理算法,效率比较差,一般比新生代GC慢十倍以上。

3、垃圾收集器

理解以下几个概念:

  • **并行:**指多条垃圾收集线程并行工作,用户线程仍处于等待状态(暂停SWT【Stop the World】);
  • **并发:**用户线程和垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,二垃圾收集程序在另外一个CPU上;
  • **吞吐量:**CPU用于运行用户代码的时间和CPU总消耗时间的比值;(是一个程序的性能指标)
  • **用户体验:**stw单次停顿越长,用户体验就越差,此时用户体验就不好;单词stw停顿时间短一点,整个stw的时间可以长一点(用户体验更好,但吞吐量小(性能差一点))
    适用场景:和用户打交道的程序,比如我们的一个Java网站后端。

jvm中经典的垃圾收集器:

  • Serial收集器:新生代收集器(复制算法);单线程(单个垃圾回收线程)的方案=>目前大多数电脑都是支持多线程,所以这个收集器效率不高;

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

jvm中经典的垃圾收集器:

  • Serial收集器:新生代收集器(复制算法);单线程(单个垃圾回收线程)的方案=>目前大多数电脑都是支持多线程,所以这个收集器效率不高;

[外链图片转存中…(img-E1VT0BVj-1714413454047)]
[外链图片转存中…(img-ofl4HPec-1714413454047)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值