JVM基础及JVM内存结构(1)

栈帧的组成是:局部变量表,操作数栈,方法返回地址,动态链接。

虚拟机栈:JVM会每个线程创建一个虚拟机栈内存。即虚拟机栈是线程运行过程中需要的内存。虚拟机栈是线程私有的。

栈帧:线程中每次方法调用对应虚拟机栈有一个栈帧入栈,每次方法调用结束都对应虚拟机栈一个栈帧出栈。即栈帧是方法运行过程中需要的内存。

注意:线程只能有一个活动栈帧,就是当前正在执行的方法,即虚拟机栈顶的栈帧。

局部变量表:方法运行中方法参数,局部变量所需要的内存,存储单位称为槽位。

操作数栈:方法运行中方法参数,局部变量运算所需要的内存。

返回地址:方法出口。

动态链接:是找到正确的方法入口,然后才有后来的进栈出栈的执行。

问题0:虚拟机栈是否会出现内存溢出?

会,虚拟机栈是一种栈结构,它有着入栈出栈两种行为。当入栈的栈帧数目大于虚拟机栈的深度时,就会导致java.lang.StackOverFlowError。现实例子比如方法的递归调用,但是递归方向是未知的。还有一种情况是单个栈帧内存过大,直接超出了虚拟机栈剩余内存大小,比如一个方法中定义了过多的变量,此时会报错java.lang.StackOverFlowError。

还有一种情况,就是创建过多的线程,导致没有足够的内存给新线程创建虚拟机栈,此时会报错java.lang.OutOfMemoryError。

问题1:虚拟机栈内存是否越大越好?

JVM可以通过-Xss参数来设置单个线程的虚拟机栈的内存大小,如果单个线程的栈内存设置的越大,则总线程数越少。另外当创建的线程过多时,可能出现OOM。

问题2:虚拟机栈内存是否需要被垃圾回收?

不需要,因为虚拟机栈的生命周期就是线程的生命周期。当线程结束时,虚拟机栈内存被JVM自动回收。相较于堆中对象,虚拟机栈中的数据生命周期更短且更容易判断是否存活。

问题3:方法内的局部变量是否线程安全?

线程安全场景出现的前提是:1.多线程环境 2.多线程共享操作同一个数据

一个方法被多线程执行,

如果方法内的局部变量来自于外部传入(即方法参数),则可能该局部变量线程不安全,

如果方法内的局部变量会作为方法返回值返回,则可能局部变量线程不安全,

如果方法内的局部变量,既不从外部传入,也不传出给外部,则局部变量线程安全。

当然前面三个判断是以局部变量是引用类型为前提的,如果方法内局部变量是基本类型,则一定线程安全。

本地方法栈


和虚拟机栈没有本质区别。

只是虚拟机栈是给Java方法用的,本地方法栈是给Native方法用的。

本地方法栈也会出现StackOverFlowError和OOM。

定义:所有的对象实例和数组实例都存在堆中。

特点

  1. 线程共享,堆中对象都需要考虑线程安全的问题
  1. 堆内存是GC管理的内存区域,可以实现垃圾对象内存回收。
  1. 堆内存是实现分代的,如经典分代将堆分为两块:新生代,老年代
  1. 堆内存可以划分出线程私有的TLAB,即本地线程分配缓冲区,提高内存分配效率,减少内存分配冲突。
  1. 堆内存在物理上可以是不连续的内存组成,但是逻辑上是连续的。
  1. 堆内存既可以是固定大小的,也可以可扩展大小的。可以通过虚拟机参数控制:
  • -Xms 堆初始内存大小
  • -Xmx 堆最大内存大小

注意:当堆内存不足以分配时,就会发生堆内存溢出,即报错java.lang.OutOfMemoryError:Java heap space

方法区


定义:是线程共享的内存区域。方法区是堆的逻辑部分,但是也叫非堆。

作用:存储加载的类型信息,常量,静态变量,即时编译器编译代码的缓存

实现:方法区只是一种概念,对于Hotspot虚拟机,它的具体实现分为两阶段

  1. JDK8:元空间,元空间处于本地内存中
  1. JDK8以前:永久代,永久代处于JVM内存中

设置方法区内存大小的虚拟机参数变化

JDK8:-XX:MaxMetaspaceSize=

JDK8以前:-XX:MaxPermSize=

方法区内存溢出报错变化

JDK8:java.lang.OutOfMemoryError:Metaspace

JDK8以前:java.lang.OutOfMemoryError:PermGen space

问题:是否会存在方法区溢出的场景?

会,大量加载类到方法区,当前流行的Spring,Mybatis框架都使用了cjLib,即底层使用ClassWriter原理自动生成二进制类。

运行时常量池


常量池和运行时常量池的关系:

常量池是class文件,即二进制字节码文件中的Constant Pool信息,当字节码被类加载器加载到方法区后,它的Constant Pool信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

串池和运行时常量池的关系:

运行时常量池中的字符串仅仅是符号,而不是对象。只有对应字符串符号第一次被使用时,才会变成字符串对象,并存入串池。

串池特点:

  1. 字符串对象的延迟加载特性:运行时常量池中的字符串仅仅是符号,只有运行时常量池中的字符串符号第一次被使用时才会变成字符串对象。
  1. 字符串对象的去重特性:当需要使用字符串时,会先使用字符串符号去串池中查找是否由对应的字符串对象,如果有,则直接引用串池中的字符串对象。
  1. 可以使用String::intern方法,主动将串池中还没有的字符串对象放入串池
  1. 字节串+号拼接优化:

对于字符串变量+拼接:底层new StringBuilder().append(“x”).append(“y”).toString();

对于字符串常量+拼接:编译期优化,则直接替换为拼接结果字符串对象。

串池位置:

JDK6:永久代

JDK7:堆

问题:如何验证串池位置

我们可以利用String::intern()方法往串池中加入大量字符串对象,撑爆内存,使之抛出OOM,对于永久代内存溢出报错:java.lang.OutOfMemoryError:PermGen space,对于堆内存溢出报错:java.lang.OutOfMemoryError:Java heap space,所以可以通过内存溢出报错来辨别串池位置。

问题:为什么要将串池位置从永久代换到堆中?

因为Java程序运行中会产生大量的字符串对象,如果不及时回收字符串对象内存,很有可能造成内存溢出,而永久代只有在Full GC时才会进行回收,而堆内存新生代会经常触发Minor GC,代价更小,频率更高。

问题:串池位置的变化对String::intern()方法返回结果的影响?

JDK6时,串池在永久代中,堆中字符串对象调用intern方法后,会尝试去串池中查找是否已存在对应字符串对象,若不存在,则复制堆中字符串对象到串池中,此时串池中复制对象和堆中原对象不是同一个对象。

JDK7时,串池在堆中,堆中字符串对象调用intern方法后,会尝试去串池中查找是否已存在对应字符串对象,若不存在,则串池直接引用堆中字符串对象,此时串池和堆中字符串对象是同一个。

JDK7串池垃圾回收

JDK7时,串池已经放在了堆中,所以串池可以被垃圾回收。

字符串引发的内存问题

字符串对象可以存在堆中(非串池内存),存储在堆中有一个坏处就是允许重复的字符串对象,所以用堆来存储字符串对象,会产生大量相同的字符串对象,从而导致浪费内存。

解决方案:将字符串对象存入串池,串池的数据结构是哈希表,哈希表可以保证不存入重复的字符串,对于重复的字符串对象,对外提供唯一的字符串对象引用。

串池调优

由于串池的数据结构是哈希表,所以字符串对象存入哈希表之前,需要计算其在表中的桶位置,如果对应桶已经有对象了,则比较二者是否相同,如果不相同,则继续和桶下链表节点对象比较。

可以发现在哈希冲突严重的串池中,存入字符串对象的效率很低。所以串池的调优就是针对降低其底层哈希表的哈希冲突。

我们知道当哈希表长度越长时,哈希冲突越少。所以可以增加串池底层哈希表的长度,即增加哈希表桶数目。

其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

《前端开发四大模块核心知识笔记》

最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值