读《Java虚拟机精讲》之-------JVM内存分配的总结

版权声明:本文为博主原创文章,转载请指明出处: https://blog.csdn.net/qq_34178598/article/details/78774063

一 ,  JVM的运行时内存结构

JVM内部定义了多个程序在运行时需要使用到的内存区,如下图所示:




二, 线程共享内存区


如上面的图所示,JVM中内存区可以根据访问权限不同定以为线程共享和线程私有两大类。所谓线程共享是指的是可以允许被所有线程共享访问的一类内存区,包括堆区,方法区,运行时常量池三个内存区。


1. Java堆区(heap)

Java堆区在JVM启动的时候被创建,并且它在实际的内存空间中可以是不连续的。Java堆区是一块用于存储对象实例的内存区,是GC执行的重点区域。

既然Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设置好了,可以通过选项 “-Xmx” 和“-Xms”来进行设置,“-Xms”用于表示堆区的起始内存,“-Xmx”则用于表示堆区的最大内存。一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常。

基于分代概念,Java堆区如果还要更进一步细分的话,还可以划分为新生代(YoungGen 生命周期较短的瞬时对象)和老年代(OldGen  生命周期非常长的对象),其中新生代内又可以划分为Eden空间,From Survivor空间和To Survivor空间。那么对象实例究竟是存储在堆区中的哪一个区域下?由于对象实例的创建在JVM非常频繁,因此在并发环境下从堆区中划分内存空间是非线程安全的,所以务必需要保证数据操作的原子性。基于线程安全的考虑,如果一个类在分配内存之前已经成功完成类装载步骤之后,JVM就会优先选择在TLAB(本地线程分配缓冲区Thread Local Allocation) 中为对象实例分配内存空间,TLAB在Java堆区是一块线程私有区域,它包含在Eden空间内,除了可以避免一系列的非线程安全问题外,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

2. 方法区(Method Area)

方法区中存储了每一个Java类的结构信息,比如:运行时常量池,字段和方法数据,构造函数和普通方法的字节码内容,以及类,实例,接口初始化时需要用到特殊方法等数据。

在HotSpot中,方法区仅仅是逻辑上的独立,实际上还是包含在Java堆区内。

方法区在JVM启动的时候被创建,并且它在实际的内存空间中和Java堆区一样可以是不连续的。

方法区除了可以通过选项“-XX:MaxPermSize” 设置内存大小进行动态扩展外,并不会像Java堆区那样频繁地被GC执行回收,并且还可以显式指定是否需要在程序运行时回收方法区中的数据。如果在没有显示要求不对方法区进行内存回收情况下,GC目标仅针对方法区中的常量池和类型卸载。

3. 运行时常量池

运行时常量池属于方法中的一部分,运行时常量池就是字节码文件中常量表的运行时表示形式。

当类加载成功将一个类或者接口加载进JVM中,就会创建与之对应的运行时常量池。

常量池中主要用于存放字面量(Literal)和符号引用(Symbolic References)两大类数据常量。字面量由文字字符串,final常量值等构成,而符号引用则包括了类和接口的全限定名(类似绝对路径,如 java.util.Date),字段的名称和描述符,以及方法的名称和描述符。


三, 线程私有内存区


不允许被所有线程共享访问的。线程私有内存区是只允许被所属的独立线程进行访问的一类内存区,包括:PC寄存器,Java栈及本地方法栈3个内存区


1.Java栈(Stack)

在Java虚拟机规范中,Jav栈也可以被称为Java虚拟机栈,它同PC寄存器一样都是线程私有,并且生命周期和线程的线程周期保持一致。Java栈用于存储栈帧,而栈帧中所存储的就是局部变量表,操作数栈,以及方法出口等信息。Java堆区既然存储的是对象实例,那么Java栈中的局部变量表就是用于存储各类原始数据类型,对象引用以及 returnAddress类型。returnAddress类型在JAVASE7之前的规范中被定义为Java虚拟机内部的原始数据类型,该类型用于表示一条字节码指令的操作码。在Java7之前,该类型被用于finally 子句实现,但returnAddress类型在Java语言中没有相应的类型,开发人员无法在程序中直接使用returnAddress类型。

Java栈允许被实现成固定大小的内存或者是动态拓展的内存大小,在此如果Java栈被设定为固定大小内存,一旦线程请求分配的栈容量超JVM所允许的最大值的时候,JVM将会抛出一个StackOverFlowError异常,反之OutOfMemoryError异常。

2. 本地方法栈 

本地方法栈(Native Mthod Stack) 用于支持本地方法(native方法,比如使用C/C++ 代码编写的方法)的执行,它和Java栈的作用类似。Java虚拟机规范并没有明确要求本地方法栈的具体实现方式,甚至如果JVM产品并不打算支持native 方法,也不依赖于传统栈,则可以无需实现本地方法栈。不过一旦JVM中实现有本地方法栈时,那么它将会和Java栈一样,允许被实现成固定或则时可动态扩展的内存大小,并且本地方法栈同样也会抛出StackOverflowError或者OutOfMemoryError异常。

3.PC寄存器

PC寄存器也叫程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。

每一条JVM线程都有自己的PC寄存器

在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)

如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址

如果该方法是native,那PC寄存器的值是空(undefined)。

JVM字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码命令。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。


内存分配:JVM中包含三种引用类型:类类型(Class Type), 数组类型(Array Type) 和接口类型(Interface Type)


四,面试题


有了上面的知识我们再来看看老生常谈的基础面试题:

(分别比较 == ,equals() ,并说出每条语句分别开辟了哪些内存。假设下面变量都是局部变量。)


第一组 :

1.String s1 = "hello"

2.String s2 = new String("hello");

 s1 == s2  false。

s1.equals(s2) true。

因为s1 首先在方法区的常量池创建了"hello",String对象引用(存储在栈区)s1 指向 "hello"。s2 引用指向一个 开辟在堆内存的new String("hello"),而参数hello,就是在方法区常量池的。显然地址不同。



第二组:

1.String s1 = new String("hello");

2.String s2 = new String("hello");

 s1 == s2  false。

s1.equals(s2) true。

因为每次分配的堆内存不是同一块地址。


第三组:

1.String s1 = "hello";

2.String s2 = "hello";

 s1 == s2  true。

s1.equals(s2) true。

因为 s2的"hello" 会先去方法区常量池里找有没"hello",有就用,没有才新建,显然是有的。


第四组 :

1.String s1 = "hello";

2.String s2 = "world";

3.String s3 = "helloworld";


s3 == s1+s2   false.

s3.equals(s1+s2)  true

s3 == "hello"+"world"  true

s3 == s1 + "world" false

s3.equals("hello"+"world")  true


因为:字符串如果是变量相加,先开辟空间,再拼接。字符串如果是常量相加,是先相加,再在常量池里找,有就返回,没有就新建。



猜你喜欢

转载自blog.csdn.net/qq_34178598/article/details/78774063