JVM--八股

目录

一  JVM 中的内存区域划分

二  JVM 的类加载机制

类加载的五个步骤:

1. 加载

双亲委派模型:

2. 验证

3. 准备

4. 解析

5. 初始化

类加载这个动作,啥时候会触发?

三  JVM 中的垃圾回收策略

GC 中主要分成两个阶段:

1. 找, 确认谁是垃圾

2. 释放,把垃圾对象的内存给释放掉

三种典型策略:

1.标记清除:

2. 复制算法

​编辑 3. 标记整理

4. 分代回收


一  JVM 中的内存区域划分

JVM 其实是一个Java 进程,Java 进程会从操作系统这里申请一大块区域,给 java 代码使用.申请的这一大块区域会进一步划分,给出不同的用途,其中有最核心的三个区域:

1. 堆 :  new 出来的对象.(成员变量)

2. 栈 :  维护方法之间的调用关系.(局部变量) 

3. 方法区/元数据区:  放的是类加载之后的类对象~~ (静态变量)

(Java文件被编译后,生成了.class文件,JVM此时就要去解读.class文件 ,被编译后的Java文件.class也被JVM解析为一个对象,这个对象就是 java.lang.Class .这样当程序在运行时,每个java文件就最终变成了Class类对象的一个实例。)

 考题: 内置类型的变量是在栈上的, 引用类型的变量时在堆上

变量所在的位置和变量的类型是无关的. 例: Test t = new Test();

t 本身是一个引用类型,t 是一个局部变量,此时 t 是在栈上的;

new Test() 这个对象,对象的本体是在堆上的

内存区域划分图:

给个区域的作用: 

1. 虚拟机栈,是给 java 代码使用的.

2. 本地方法栈, 是给 jvm 内部的本地方法使用的,(JVM 内部通过 C++ 代码实现的方法)

3. 程序计数器, 用途是纪录当前程序指定到哪个指令了, 简单的 long 类型的变量存了一个内存地址. 内存地址就是下一个要执行的 字节码 所在的地址

注: 堆和元数据区,在一个 jvm 进程中,只有一份. 栈(本地方法栈和虚拟机栈)和 程序计数器则是存在多份, 每个线程都有一份.

二  JVM 的类加载机制

类加载: 把 .class 文件,加载到内存, 得到类对象这样的过程.

类加载的五个步骤:

1. 加载

找到 .class 文件,并读取文件内容

双亲委派模型:

JVM 中, 加载类,需要用到一组特殊的模块,类加载器:

• BootStrap ClassLoader: 负责加载 Java 标准库中的类

• Extension ClassLoader:  负责加载一些非标准但是 Sun/Oracle 扩展的库的类

• Application ClassLoader:  负责加载项目中自己写的类以及第三方库中的类

 当具体加载一个类的时候,需要先给定一个类的全限定名. 例: "java.lang.String"(字符串)

当加载时,首先是从Application ClassLoader 开始加载,但这个类并不能立刻开始搜索目录,首先需要它的父类去找(这三个类加载器是父子关系):

2. 验证

 .class 文件有明确的数据格式(二进制文件)

3. 准备

给类对象分配内存空间

4. 解析

针对字符串常量进行初始化

字符串常量在 .class 文件中就存在了,但是由于它们还在文件中,只知道彼此之间的相对位置(偏移量),不知道在内存中的实际地址,只能使用特殊符号去占位.这时候的字符串常量就是符号引用

真正加载到内存中,就会把字符串填充到内存中的特定地址上,字符串常量之间的相对应位置还是一样的,但是这些字符串有了自己真正的内存地址,此时的字符串就是直接引用(java 中的普通的引用)

5. 初始化

针类对象进行初始化(初始化静态成员, 执行静态代码,类如果有父类,还要加载父类)

类加载这个动作,啥时候会触发?

并不是 jvm 一启动,就把所有的 .class 都加载了!! 整体是一个 "懒加载" 的策略(懒汉模式) 非必要,不加载

什么叫做"必要":

1. 创建了这个类的实例

2. 使用了这个类的静态方法/静态属性

3. 使用子类,会触发父类的加载

三  JVM 中的垃圾回收策略

JVM 中的内存有好几个区域,是释放那部分空间?

堆!!!(new 出来的对象)

程序计数器,就是一个单纯存地址的整数,不需要随着线程一起销毁,栈也是随着线程一起销毁,方法调用完毕,方法的局部变量自然随着出栈操作就销毁了,元数据区/方法区,存的类对象,很少会"卸载"

GC 中主要分成两个阶段:

1. 找, 确认谁是垃圾

java 中使用一个对象,只能通过引用来访问,如果一个对象,没有引用指向它,此时这个对象一定是无法被使用的(此时就是垃圾),如果一个对象不想用了,但是这个引用可能还指向着,此时就不是垃圾.

java中只是单纯通过引用没有指向这个操作,来判定垃圾的

具体来说,java 怎样知道一个对象是否有引用指向呢?

1. 引用计数: 给对象安排一个额外空间,保存一个整数,表示该对象有几个引用指向

(java 实际上没有使用这个方案, Python, PHP 采用了)

2. 可达性分析 : 

可达性分析关键要点,就是需要有"起点"(gcroots):
1) 栈上局部变量(每个栈的每个局部变量,都是起点)

2) 常量池中引用的对象

3) 方法区中,静态成员引用的对象

可达性分析,就是从所有的 gcroots 的起点出发,看看该对象里又通过引用能访问哪些对象(类似于二叉树).顺藤摸瓜,把所有可以访问的对象都遍历一遍(遍历的同时把对象标记成"可达").剩下的自然是"不可达"

优点: 可达性分析,克服了引用计数的两个缺点,但是也有自己的缺点.

缺点:

1. 消耗更多的时间,因此某个对象成了垃圾,也不一定能第一时间发现,因为扫描的过程中,需要消耗时间         

2. 在进行可达性分析的时候,要顺藤摸瓜,一旦这个过程中,当前代码中的对象的引用关系发生变化了(当对象变成垃圾并没有被扫描出来),就麻烦了.因此,为了更准确的完成"顺藤摸瓜"这个过程,需要让其他的业务线程停止工作.就引出了 STW(stop the world)问题.

2. 释放,把垃圾对象的内存给释放掉

三种典型策略:

1.标记清除:

2. 复制算法

把整个内存够空间,分成成两段,一次只用一半

 3. 标记整理

当2 和4 是垃圾,将3复制到2 中,将 5复制到 三的位置,然后释放内存.

优缺点

1. 能够解决内存碎片化问题

2. 搬运的开销太大

4. 分代回收

由于上述三种方法都不能很好的解决内存释放,因此实际上 JVM 的实现思路,是结合了上述几种的方法,针对不同的情况,使用不同的策略.

当我们new 一个对象时,年龄为0,每经过一轮扫描(可达性分析),没被标记成垃圾,年龄加一,针对不同的年龄对象采取了不同的回收策略

 1. 新创建的对象,放到伊甸区. 当垃圾回收扫描到伊甸区之后, 绝大部分对象都会在第一轮 gc 中被干掉, 大部分对象时活不过一岁的(经验规律)

2.  如果伊甸区的对象,熬过第一轮 GC ,就会通过复制算法,拷贝到生存区,生存区分成两半(大小均等),一次只使用一半. 垃圾回收扫描伊甸区对象,也是发现垃圾就淘汰,不是垃圾的,通过复制算法,复制到生存区的另一半

3.  当这个对象在生存区,熬过若干轮 gc 之后,年龄增长到一定程度了,就会通过复制算法拷贝到老年代

4.  进入老年代的对象,年龄都很大了,再消亡的概率比前面新生代的对象小不少,针对老年代的 gc 的扫描频次就会降低很多. 如果老年代中发现某个对象是垃圾了,使用标记整理的方式清除

5. 特殊情况: 如果对象非常大,直接进入老年代,(大对象进行复制算法,成本比较高,而且大对象也不会很多)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值