【JVM】运行时数据区——自问自答

Q:Java 运行时数据区解构,哪些数据线程独占,哪些是线程共享?每个区域会产生GC和异常吗?

运行时数据区:

1、PC寄存器

2、堆区

3、JVM栈

4、Native栈

5、方法区

其中,PC寄存器、Native栈、JVM栈是线程独占的。

堆区、方法区是线程共享的。

PC寄存器 没有垃圾回收,没用任何异常

Native栈、JVM栈会抛出StackOverFlowError 和OutOfMemory:Stack 异常,没有GC

堆区分为新生代和老年代,新生代中的Eden区满触发YGC/Minor GC ,老年代满触发Major GC。

堆区有OutOfMemory :heap 异常

方法区:存在GC (Full GC),在JDK7 以前 存在OutOfMemory:PremGen

JDK8及其以后是 OutOfMemory:MetaSpace

【补充一条】 关于指令

零地址指令、基于栈的指令、基于寄存器的指令。

java字节码之所以能实现跨平台,主要是因为字节码可以被操作系统之上的JVM虚拟机识别,执行。

普通机器指令是基于CPU寄存器的,由于CPU架构不同,导致各个指令集有差异,有一地址,二地址等,这个地址指的就是寄存器地址:

比如 用汇编助记符表示: MOV 0x02 10 ;//这里写的很不严谨,只是为了好理解,这个指令第一个Mov表示操作为将数10,移动地址为0x02的寄存器上。

而零地址,是用基于栈的指令来实现的,因为栈是虚拟的数据结构,并不是实际的硬件。并且,它不需要地址来标记操作数,因为可以通过入栈和出栈来完成移动的效果。

基于栈的指令集就可以避免不同CPU架构造成的指令集不同引起的跨平台性。

但是基于栈的指令集也是有缺点的。

寄存器指令集:指令集大,但是完成一个操作所需要的指令少

栈指令:指令集小,完成一个操作所需要的指令很多,相对效率就低。

Q:具体说一说PC寄存器

设计参考CPU的PC寄存器,目的是存储指令相关的现场信息。

java的PC寄存器 准确的说理解为 指令计数器 (也有人翻译成程序钩子)。

看一下这里的起始PC,其实就是指令对应的编号(地址),也就是将要被放在PC上的数据

作用:PC 寄存器用来存储指向下一条指令的地址,也即将执行的指令代码。由执行引擎读取下一条指令。

PC上,没用GC,也没有任何异常。

Q:具体说一说虚拟机栈

一个线程有一个JVM栈,当这个线程中进行一次方法调用的时候,会形成一个栈帧。压入这个栈中。

当这个方法执行结束的时候,栈帧会被弹出JVM栈。随着线程的生命周期结束,它对应的JVM栈也会被销毁。

JVM Stack中的栈帧的结构:

局部变量表、操作数栈、方法返回地址、动态链接 和一些附加信息。

JVMStack 不涉及垃圾回收。

它在以下情况会抛出异常:

1、当栈的大小是固定的,当前线程中一个方法中不断的去嵌套式的调用其他方法(比如递归),

从内存的角度来说,就是不断的有栈帧被压入当前线程的JVM栈中,如果最后一次栈帧所需要的空间不足时,会抛出 StackOverFlowError

2、当栈的大小是动态扩容的,当前栈进行扩容时,如果物理内存空间小于栈要扩充的空间,就会抛出OutofMemory :stack 异常

栈帧内的结构说明:

局部变量表:

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,

局部变量表在线程的栈上,是线程的私有数据,因此不存在数据安全问题。

局部变量表的容量大小是在编译期确定下来的(前段编译 javac 生成字节码),并保存在方法的Code属性的maximum local variables数据项中,

在方法运行期间是不会修改局部变量表的大小的。

在方法调用结束后,局部变量表会随着方法栈帧的弹出而销毁。

局部变量表的Slot,是其最基本的存储单元。

LV中存放着编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。

LV中,32位内的类型只占用一个slot,64位的类型(long double)占两个slot

byte short char 都会被转成int boolean也会转成int 0false 非0 表示true (字节码指令集里可以看到 bint,sint)

普通对象方法和构造函数的LV中,index0的变量是 this,而静态方法没用。

slot是可以被复用的比如:

java 复制代码
public class SlotTest{
    public void localVar(){
        int a =0;
        Sout(a);
        int b=0;
    
    }

    public void localVar(){
    {
        int a =0; //a作用域在代码块中,出离了代码块,a这个局部变量就失去作用域,从LV角度来看,a的Slot位就空了,而下面的局部变量b可以対之复用
        Sout(a);
    }    
        int b=0;
    }

}

需要注意的一点是,局部变量表不存在系统默认初始化的过程,(这一点区别于静态变量)

所以,使用局部变量时,务必手动初始化。

局部变量也是可达性分析的根节点,只要是被局部变量表中直接或间接引用的对象,都不会被回收。

操作数栈、

方法返回地址、

动态链接

Q:具体说一说Java的堆

Q:具体说一说Java的方法区

Q:类加载系统

//单独开文

Q:执行引擎的结构和工作

//单独开文

Q:String的不可变性分析

首先说明String的几个问题:

String 是被final修饰的,表示它不可再被继承

jdk8及其以前 String 内部是 final char[] value,Jdk9 之后改成了 byte[]

真正想解释这个所谓的不可变性,还是得从JVM内存结构来分析。

不可变性的表现: 对字符串重新赋值时,需要重新制定内存区域赋值,不能使用原来的value进行赋值。

当对现有字符串进行链接操作时,也需要重新指定内存区域赋值,不能使用原来的value进行赋值。

调用String的replace() 修改指定字符或字符串时,同上

1、字面量创建一个字符串的过程:

此时字符串的值声明在字符串常量池中。

pc =0 ldc #2是什么?:

从运行时常量池中取一个元素进操作数栈。

紧接着:astore_1 表示 将操作数栈的引用型变量 保存入局部变量表index1的位置

如果运行时常量池中的条目是字符串常量,即对string类实例的引用,则将对该实例的引用value压入操作数栈。

注意点:

字符串常量池不会存储相同的字符串。

String的 String pool 是一个固定大小的Hashtable,默认长度是 1009,若放入的String 过多,会导致hash冲突严重,从而导致链表很长,链表最大的特点就是插入删除简单,但是遍历时间复杂度高,这样就导致调用String.intern性能大幅下降(intern是会先查重,再插入的,有个遍历过程)

可以使用-XX:StringTableSize设置StringTable的长度

jdk6 StringTableSize 默认1009,大小可以随便修改

jdk7 StringTableSize 默认是60013 大小可以随便设置

jdk8开始,设置StringTableSize长度,不可低于1009

2、new String("xxx")的过程

java 复制代码
   public void test2(){
        String ok666 = new String("OK666");

    }

整个过程是 创建对象,从常量池里找到字符串实体对象的引用,通过构造函数给新建对象赋值。

所以:一次 new String("XX") 其实底层有两个对象

1、new 关键字在堆空间创建的对象

2、字符串常量池中的对象,字节码指令为 ldc

3、字符串连接符 +

1、常量与常量的拼接结果在常量池,原理是编译期的优化

2、常量池中不会存在相同内容的常量

3、只要其中一个是变量,结果就存在堆中。变量拼接的底层原理是Stringbuilder

4、如果拼接的结果调用了intern(),则主动将常量池中还没有的字符串对象放入常量池中,并返回此对象的地址。

常量拼接

java 复制代码
 @Test
    public void test3(){
        String s1 ="a"+"b"+"c";  //常量拼接
        String s2 ="abc";

        System.out.println(s1 == s2); //true
    }

带变量的拼接

java 复制代码
 @Test
    public void test3(){
        String s1 ="a"+"b"+"c";  //常量拼接
        String s2 ="abc";

        String s3 =s2+"test";

        System.out.println(s1 == s2); //true
    }
复制代码
字节码执行流程如下:

含有变量的字符串拼接操作,用的是Stringbuilder 对象,append()操作。 最后 toString()返回,存入局部变量表

问题:在new String("a")+new String("b")中,一共创建了几个对象?

5个:

new StringBuilder()

new String("a")

常量池里的a

常量池里的b

new String("b")

new String("ab")

特别强调!!

Stringbuilder的toString()被调用,并不会在常量池中生成 "ab"实例

这里有个细节:

通过查看字StringBuilder的toString()节码, 我们发现他和普通的new String()并不一样。它没用ldc指令。

这个是StringBuilder的 toString(),它的内部看上去好像就是new String(),但是看看他的字节码

它并不是普通的new String("xx")构造法,会用ldc 从常量池加载一个字面量对象的引用变量入栈,直接进行 invokespecial <java/lang/String.<init>>操作。因此常量池在字节码编译期并没有创建一个字面常量对象 "ab"。

可以对比参考 2、new String("xxx")的过程

String intern()的使用

jdk6 中将这个字符串对象尝试放入常量池

  • 如果池中有,并不会放入。返回已有的池中对象的地址。

  • 如果没有,会把对象复制一份,放入池中,并返回池中对象的地址

jdk7 起 中将这个字符串对象尝试放入常量池

  • 如果池中有,并不会放入。返回已有的池中对象的地址。

  • 如果没有,则会把对象的引用地址复制一份,放入池中,并返回池中的引用地址。

之所以jdk7有这个改变,我分析原因如下:

jdk6(发布时间 2006年12月) Stringtable 和静态变量还是在堆外的,严格的放在方法区中(永久代)

jdk7(发布时间 2011年7月) 中 Oracle收购JRocket虚拟机(2008年)之后,重新定义了关于JVM的一些落地实现方案,

它把Stringtable 和静态变量移到了堆内,虽然这个时候方法区还是用的JVM内存,落地实现还叫永久代。

由于字符串常量已经放在了堆中,为了节省堆内存空间,就没用必要再在堆内常量池中再创建一个副本对象了,直接通过指针引用就可以了。

Q:i++和++i的底层理解

**首先,我是真的想问候一下出这种面试题的人,你们的目的何在?你认为一个普通程序员会去关注底层字节码吗?刷存在感呢?让程序员死记硬背这种问题有意思吗,活该你们一辈子当资本鹰犬,当奴才。

java 复制代码
   public void test1(){
        int i =10;
        i++;
        System.out.println(i);
    }

上述方法:局部变量表最大slot数是2 index0 =this,index1 =i;

操作数栈的最大深度 2

i++ 具体执行过程如图示

此时,index1位置的变量 i 变成 11

再看看++i

会发现执行结果是一致的:

这里总结一句:

自加操作在底层字节码表示的方式是一致的,但是要注意,自加指令是 inc index by 1

这个操作是在局部变量表中完成的,并没有移入移出栈。这个原则会导致下面现象发生:

java 复制代码
  public void test3(){
        int i =10;
        i=i++;
        System.out.println(i);
    }

输出结果是10,这里有个关键问题就是,自加操作,对变量i本身再次复制:

赋值操作实际上是将 操作数栈中最新的变量,去覆盖对应局部变量表中旧的变量。

pc =3 iload_1 指把LV中 index1 的变量i 取到操作数栈中,

pc=4 iinc 1 by 1 ,LV中 index1 进行自加操作

pc=7 将操作数栈的内容写回 LV index=1的位置。

所以,此时出现取出i=10, i在局部变量表中自加(LV index1 i=11),写回i=10,重新覆盖了 局部变量表中的 i.

相关推荐
吾爱星辰3 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
介亭3 小时前
internal.KaptWithoutKotlincTask$KaptExecutionWorkAction 问题 ---Room数据库
jvm
小飞猪Jay4 小时前
C++面试速通宝典——13
jvm·c++·面试
Ray Wang17 小时前
3.JVM
jvm
java6666688881 天前
Java中的对象生命周期管理:从Spring Bean到JVM对象的深度解析
java·jvm·spring
生产队队长1 天前
JVM(HotSpot):字符串常量池(StringTable)
jvm
Yuan_o_1 天前
JVM(Java Virtual Machine) 详解
jvm
派大星-?2 天前
JVM内存回收机制
jvm
G丶AEOM2 天前
Hotspot是什么?
jvm·hotspot
太阳伞下的阿呆2 天前
Java内存布局
jvm·内存对齐·内存布局·压缩指针