JVM就该这么学 DAY01 —— 不一样的JVM学习以及面试绝对让你眼前一亮~

JVM(Java Virtual Machine)就该这么学 DAY01

写在前面

本文参考《深入理解Java虚拟机》
							——周志明著

博主最近在夯实Java基础的相关知识,也是第二次翻阅这本周志明大佬缩写的书,读了第二遍时的感受,确实是第一次读书入门时无法感受到的,二刷希望能更加的牢记部分知识点的同时也能根据目前比较火的有关于JVM的面试题做出自己的相应见解,因为篇幅太过长了,只好分学习日子来发了,请多多见谅,话不多说,进入JVM基础夯实的大门吧~

Java内存区域与内存溢出异常

1、运行时数据区域

1、运行时数据区域

根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

在这里插入图片描述

1.1 程序计数器

程序计数器

内存空间小,当前线程执行字节码的行号指示器

字节码解释器工作时,通过改变程序计数器,获取下一条需要执行的字节码指令

它是程序控制流的指示器

线程私有,每个线程拥有独立的程序计数器,为了保证在多线程中JVM能在线程切换后恢复到正确的执行位置。

当线程在执行一个Java方法时,此时程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域

1.2 Java虚拟机栈

Java虚拟机栈

线程私有且生命周期与线程相同

用来描述Java方法执行的线程内存模型:

1、当每个方法执行后,JVM会同步创建一个栈帧用于存储局部变量操作数栈动态链接方法出口等信息。

2、每个方法被调用到执行完毕的过程,就对应着栈帧在JVM栈中从入栈到出栈的过程。

局部变量表:存放了编译期可知的八大基本数据类型、对象引用(reference类型,可能是一个指向对象起始位置的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)以及returnAddress类型(指向一条字节码指令的地址

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。

OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

1.3 本地方法栈

本地方法栈

区别于Java虚拟机栈的是,Java虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则是虚拟机使用到Native方法服务(通过Native方法调用c++来操作底层)。也会有StackOverflowError和OutOfMemoryError异常

1.4 Java堆

Java堆

对于绝大多数应用来说,Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,虚拟机启动时创建

主要用来存放对象实例,并且为分配内存空间

所有线程共享的JAVA堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer) 用于提高效率

可以位于物理上不连续的空间,但是逻辑上要连续

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

1.5 方法区

方法区

是各个线程共享的内存区域,用于存储已经被虚拟机加载的类型信息,常量,静态变量、及时编译器编译后的代码缓存等信息。
在这里插入图片描述

1.6 运行时常量池

运行时常量池

是方法区的一部分。

Class文件中除了有类的版本、字段、方法、接口等描述的信息之外,还有一项常量池表用于存放编译期生成的各种字面量和符号引用。会在类被类加载器加载完成之后存放到方法区的运行时常量池当中

编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError

1.7 直接内存

直接内存

非运行时数据区的一部分。

在JDK1.4时引入的NIO(New Input/Output)类,引入了一种基于通道与缓存的I/O方式,他可以通过Native函数库直接分配堆外内存,然后通过一个Java堆中一个DirectByteBuffer对象作为这块内存的引用 来进行内存操作。可以避免Java堆和Native堆中来回的数据耗时操作

OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

2、HotSpot虚拟机对象探秘

主要描述的是HotSpot虚拟机正在Java堆中对象分配,布局和访问的全过程

2.1 对象的创建

注意

(这里不考虑clone,反射等技术时的对象创建)
在这里插入图片描述

对象创建时简图如上:

对象创建的过程简述

1、当我们使用new关键字来实例化对象的时候,首先会去检查这个对应的参数能否在常量池中定位到一个类的符号引用,并且会检查这个符号引用代表的类是否已经被加载过、解析、初始化。如果没有JVM会自动的帮咱们进行类加载.(Class对象)

2、当类加载检查通过时,就会为新的对象分配内存空间(类加载完成过后便可确认),在Java堆的空闲内存中划分一块区域(指针碰撞-内存工整时使用或者空闲列表-内存交错时使用)

3、本地线程缓冲(TLAB)可有效避免在多线程并发情况下频繁的创建对象实例而造成的线程不安全(当指针还未来得及修改,另一条线程又使用了原来的指针位置),也可以利用同步处理CAS(compareAndSet)来确保操作的原子性。

4、内存空间分配完成之后会将所有属性值初始化为0、随后填充对象头、用来确定该对象是哪个类的实例,找到类的元数据信息、对象的hash值、GC分代年龄等信息均在对象头当中。

5、当new指令执行过后,需要执行()方法按照程序猿意愿对对象进行初始化。此时一个真正可用的对象才算完全被创建出来。

2.2 对象的内存布局

对象的内存布局

在HotSpot虚拟机中,分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

2.3 对象的访问定位

对象的访问定位

创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象

通过句柄访问对象

在这里插入图片描述

通过直接指针访问对象

在这里插入图片描述

比较:

1、使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。

2、直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。

总结:如果是对象频繁移动 GC ,那么句柄方法好,如果是对象频繁访问则直接指针访问好。

2.4 实战——OutOfMemoryError异常

实战——OutOfMemoryError异常

后续所有的OOM都指代的是(OutOfMemoryError)

2.4.1 Java堆内存溢出
/**
 * 功能描述
 *  Java堆内存溢出异常测试
 *  需要设置最小以及最大的JVM可分配内存
 *  不然不会报错哦~
 * @author Alascanfu
 * @date 2021/12/23
 */
public class HeapOOM {
    static class OOMObject{
    
    }
    
    public static void main(String[] args) {
        ArrayList<OOMObject> list = new ArrayList<>();
        while (true){
            list.add(new OOMObject());
        }
    }
}

2.4.2 虚拟机栈和本地方法栈溢出
/**
 * 功能描述
 * 虚拟机栈和本地方法栈溢出
 * 修改参数减少对应的栈内存容量才会出现StackOverflowError
 * @author Alascanfu
 * @date 2021/12/23
 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }
    
    public static void main(String[] args) throws Throwable{
        JavaVMStackSOF oom = new JavaVMStackSOF();
    
        try {
            oom.stackLeak();
        } catch (Exception e) {
            System.out.println("stack length:"+ oom.stackLength);
            throw e;
        }
    }
}

虚拟机栈和本地方法栈出现异常的原因:

1、如果线程请求的深度大于虚拟机允许的最大深度将会抛出SOF异常。

2、如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,就会抛出OOM异常。

3、内容场景面试

“诶?这不是昨天那个一面的小伙子么?今天可让我逮着你了!这次先看看你JVM的基础功底怎么样~” HR小哥哥对着我来了一个礼貌而不失优雅的微笑。

小付内心:mdbk这哥们竟然对我笑,不会对我有意思吧!先不管了,面试要紧!

小伙子,今天咱们来聊一聊JVM的基础知识,你先来谈一谈JVM的主要组成部分及其作用?

(啊?这不就是让我搞个图给他画一下JVM的组成以及简述下组成部分的作用吗?)

“帅气逼人的面试官您好,java虚拟机包括了两个子系统:类加载器和执行引擎,两个组件:运行时数据区域和本地库接口”

知识点

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。

  • Execution engine(执行引擎):执行classes中的指令。

  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

作用 :首先通过编译器把 Java 代码转换成字节码xxx.class文件,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。(通过C++来调用底层)

小付说道:“当我们创建好一个java程序过后,xxx.java程序会通过javac进行编译成为xxx.class文件,随后这个class文件会通过JVM中的类装载器把文件中的二进制数据读取到内存区域当中,将其放在运行时数据区域的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,随后通过JVM的执行引擎调用底层的Native方法来实现java程序的功能。”

嗯!挺全面的,(小伙子不错呀,讲的挺细的,和深入理解JVM中讲的差不多,这崽子应该是看过大概,在刁难刁难他)那么我再来问你说一下Java运行时数据区由哪些部分组成简述相应功能?

小付捣鼓了一下下给HR弄个了大致的想象图出来

在这里插入图片描述

(回答写在图里了,也要自己总结一些话语说出来,毕竟这种要多锻炼,才能不怯场~)

emmmm…你说的还不错,但是我觉得堆和栈之间的区别你说的不是很详细,能具体说说堆和栈的区别么?

小付:存放的内容他们有着明显的不同

堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

注意

  • 静态变量在方法区中

  • 静态的对象还是放在堆中

且堆对于所有线程都是共享的,而栈是线程私有的。

而且栈分配的内存大小地址一般是连续的,而堆分配不连续,同时堆的内存大小也远远大于栈。

深拷贝和浅拷贝你有了解么?

阿巴阿巴…小付触碰到盲区了…可能没复习到这个知识点

回家后去csdn了一下博文~

浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,

使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。

浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

没答上来呀,那咱们换个问题吧,那么Java会存在内存泄漏吗?请说说你的看法

这个小付还是有所了解的:内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。

但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景.

emmm这几个基础回答的不错 ,我今天要先去弄个文件,你先回去吧,明个时间再来~

凉了,凉了芭比Q了~今天得回去再恶补一下,明日再战!我一定会回来的!!!

写在最后

今天学习了Java内存区域以及内存溢出异常的相关知识,

希望大家可以在学习之后,能手绘出Java内存区域模型的同时也能指出对应区域的相关作用,

这样不仅为你加深了印象,更加培养了相应的学习方法。

又是夯实基础的一天~后面会继续更新JVM的相关内容,冲冲冲!不要放弃。

最后

每天进步点 每天收获点

愿诸君 事业有成 学有所获

如果觉得不错 别忘啦一键三连哦~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alascanfu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值