


一. 线程私有数据区:
1)程序计数器:
线程作为CPU基本执行单位。当多线程情况下,线程数量超过了CPU数量或CPU核心数的时候,线程之间要根据时间片论询抢夺CPU时间资源。也就是说,在任何一个特定的时刻,一个处理器只会处理一个线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址。
程序计数器是线程私有的一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,计数器记录的是正在执行的字节码指令的地址;如果正在执行的是native方法,则计数器的值为空。
程序计数器是唯一一个没有标定任何outofmemoryError的区域。
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系“线程私有”
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄漏
2)虚拟机栈:
描述的是Java方法执行的内存模型,是线程私有的。
每个方法在执行的时候都会创建一个栈帧 ,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
其中,局部变量表主要存放一些基本类型的变量(int,long,short,byte,boolean,double,float,char) 和对象句柄,它们可以是方法参数,也可以是方法的局部变量。
虚拟机栈有两种异常情况,stackOverflowError和outOfMemoryError。我们知道,一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度(递归层次,或者嵌套多少层),若线程请求的栈深度大于虚拟机允许的深度,则抛出StackOverflowError异常。此外,栈的大小可以是固定的,也可以是动态扩展的,若虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存(比如没有足够的内存为一个新创建的线程分配空间时),则抛出OutOfMemoryError异常。


递归为什么会引发 java.lang.StackOverflowError异常
- 递归过深,栈帧数超出虚拟机栈深度
3)本地方法栈:
和虚拟机栈的主要区别是,本地方法栈为native方法服务。
二. 线程共享数据区:
1)堆区:
Java堆唯一的目的就是存放对象实例,几乎所有的对象实例(数组)都这这里分配内存。类的对象从中分配空间,这些对象通过new、newarray、anewarray和multinewarray等指令建立,它们不需要程序代码来显式的释放。
它又被称作GC堆。从内存回收角度看,为了方便垃圾回收Java堆还被分为新生代和老年代。
新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。新生代又可以进一步细分为eden、survivorSpace0、survivorSpace1。刚创建的对象都放入 eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一段时间仍存在,则进入老年代。

Java堆可以存在物理上不连续的地址控件,只要逻辑上连续即可。而且,Java堆在实现时,既可以固定大小,也可以是扩展的。并且主流虚拟机都是按照可扩展来实现的(通过-Xmx(最大堆容量)和-Xms(最小堆容量)控制)。如果堆中没有内存完成实例分配,抛出outofmemory异常。
TLAB(Thread local allocation buffer 线程私有分配缓冲区)
为了提高对象内存分配的效率单独开出了这样一片空间。每个线程在堆中画出一小片内存空间,哪个线程需要分配内存就在自己的TLAB上进行分配,若TLAB空间用完,就开新的TLAB,再加同步锁定,大大提升了对象内存分配的效率。
元空间(MetaSpace)与永久代(PermGen)的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存
java.lang.OutOfMemoryError: PermGen space
MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难
- 永久代会GC带来不必要的复杂性
- 方便HotSpot与其他JVM入Jrockit的集成
JVM 三大性能调优参数-XMS -XMX -Xss的含义
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值
Java内存模型中堆和栈的区别——内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时运行时模块入口都无法确定,动态分配
Java 内存模型中堆和栈的区别
-
联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
元空间、堆、线程独占部分间的联系——内存角度
public class HelloWorld{
private String name;
public void sayHello(){
System.out.println("Hello "+name);
}
public void setName(String name){
this.name = name;
}
public static void main(String[] args){
int a=1;
HelloWorld hw = new HelloWorld();
hw.setName("test");
hw.sayHello();
}
}

2)方法区:
方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
(1)、运行时常量池
它是方法区的一部分,用于存放编译期生成的各种字面量 和 符号引用。
其中,字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等;
而符号引用则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符。
运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如字符串的手动入池方法intern()。
3)Java堆与方法区的区别?
Java堆是 Java代码可及的内存,是留给开发人员使用的;而非堆(Non-Heap)是JVM留给自己用的。
所以方法区、JVM内部处理或优化所需的内存 (如JIT编译后的代码缓存)、每个类结构 (如运行时常量池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
4)方法区的回收
常量池的回收 和 对类型的卸载
回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
满足下面3个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。
特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
二.Java对象在虚拟机中的创建与访问定位
1、对象在虚拟机中的创建过程
(1) 检查虚拟机是否加载了所要new的类,若没加载,则首先执行相应的类加载过程。虚拟机遇到new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个引用代表的类是否已经被加载、解析和初始化过。
(2) 在类加载检查通过后,对象所需内存的大小在类加载完成后便可完全确定,虚拟机就会为新生对象分配内存。根据Java堆中内存是否绝对规整,内存的分配有两种方式:
- 指针碰撞:如果Java堆中内存绝对规整,所有用过的内存放在一边,空闲内存放在另一边,中间一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相同的距离。
- 空闲列表:如果Java堆中内存并不规整,那么虚拟机就需要维护一个列表,记录哪些内存块是可用的,以便在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
除了如何划分可用空间之外,还需要考虑修改指针(该指针用于划分内存使用空间和空闲空间)时的线程安全问题,因为存在可能出现正在给对象A分配内存,指针还未修改,对象B又同时使用原来的指针分配内存的情况。解决这个问题有两种方案:
-
对分配内存空间的动作进行同步处理:采用CAS+失败重试的方式保证更新操作的原子性;
-
把内存分配的动作按照线程划分的不同的空间中:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在自己的TLAG上分配,如果TLAB用完并分配新的TLAB时,再加同步锁定。
(3) 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。如果使用TLAB,也可以提前到TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
(4) 在上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,此时会执行<init>方法把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。
2、对象在虚拟机中的访问定位
创建对象是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。在虚拟机规范中,reference类型中只规定了一个指向对象的引用,并没有定义这个引用使用什么方式去定位、访问堆中的对象的具体位置。目前的主流的访问方式有使用句柄访问和直接指针访问两种。
-
句柄访问:Java堆中会划分出一块内存作为句柄池,栈中的reference指向对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示。
-
直接指针访问:reference中存储的就是对象地址。
这两种对象访问定位方式各有千秋。
使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,reference本身不需要修改;
而使用直接指针访问的最大好处就是速度快,节省了一次指针定位的时间开销。
强软弱虚

网友评论