JVM--八股

简介: JVM--八股

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

相关文章
|
7月前
|
算法 数据可视化 Java
深入理解JVM系列教程(完) - 终章总结
深入理解JVM系列教程(完) - 终章总结
41 0
|
6天前
|
存储 Java 编译器
【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
|
1月前
|
安全 算法 Java
深入浅出JVM(十三)之垃圾回收算法细节
深入浅出JVM(十三)之垃圾回收算法细节
|
1月前
|
算法 Java 程序员
[JVM] 字节二面~简述垃圾回收以及类加载过程,别说八股文,我想看到你自己的理解
[JVM] 字节二面~简述垃圾回收以及类加载过程,别说八股文,我想看到你自己的理解
|
7月前
|
算法 Oracle Java
温故知新-JVM篇
温故知新-JVM篇
29 0
|
11月前
|
存储 算法 Java
JVM知识扫盲篇
JVM知识扫盲篇
261 0
JVM垃圾回收器详解:不同的复制算法比较及对程序员的启迪
前面提到整个JVM中只有串行回收按照Cheney的设计实现新生代回收,其他的垃圾回收器在新生代回收时都对Cheney的复制算法进行了增强。 其中最大的改变就是不使用宽度优先,而是使用深度优先的处理方式。其中Moon在1984年提出了一种近似深度优先遍历的处理方式,称为层次遍历,使用层次遍历大概可以将GC效果提升6%。
|
存储 算法 安全
JVM理解其实并不难!
JVM理解其实并不难!
JVM理解其实并不难!
|
存储 算法 Java
JVM垃圾清理机制详解 ✨ 每日积累
JVM垃圾清理机制详解 ✨ 每日积累
JVM垃圾清理机制详解 ✨ 每日积累
|
存储 算法 安全
JVM探究
请谈谈你对JVM的理解? java8虚拟机和之前的变化更新? - 什么是OOM,什么是栈溢出StackOverFlowError? 怎么分析? - JVM的常用调优参数有哪些? - 内存快照如何抓取,怎么分析Dump文件? - 谈谈JVM中,类加载器你的认识
109 0