三、运行数据区

Java程序执行前先编译为字节码,由Java虚拟机执行。虚拟机内存分为线程共享的堆和方法区,以及线程私有的虚拟机栈、本地方法栈和程序计数器。对象创建涉及内存分配,如指针碰撞和空闲列表策略。访问定位可通过句柄或直接指针方式。异常情况包括堆、方法区和栈的溢出,以及内存泄漏分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概论:

Java程序在执行前首先会被编译成字节码文件,然后再由Java虚拟机执行这些字节码文件从而使得Java程序得以执行。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些数据区域都有各自的用途,以及创建和销毁的时间,并且它们可以分为两种类型:线程共享的方法区和堆,线程私有的虚拟机栈、本地方法栈和程序计数器。下面探讨了在虚拟机中对象的创建和对象的访问定位等问题,并分析了Java虚拟机规范中异常产生的情况。

内存模型:

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些数据区域可以分为两个部分:一部分是线程共享的,一部分则是线程私有的。其中,线程共享的数据区包括方法区和堆,线程私有的数据区包括虚拟机栈、本地方法栈和程序计数器。

一、线程私有数据区

包括了程序计数器、虚拟机栈和本地方法栈三个区域。

  1. 程序计数器:

程序计数器是线程私有的一块较小内存空间,可以看做是当前线程所执行的字节码行号指示器。如果线程正在执行的是一个java方法,计数器记录的是下一条指令的地址;如果正在执行的是Native方法,则计数器的值为空。

不会抛出OutOfMermoryError。

    2.虚拟机栈:

描述的是java方法执行的内存模型,是线程私有的。每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,栈帧可以理解为一个方法的运行空间。而且每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。其中局部变量主要存放一些基本类型的变量(int,short,long,byte,float,double,Boolean,char)和对象句柄,它们可以是方法参数,也可以是方法的局部变量。这里说明一下,一个方法就相当于一个栈帧,栈帧里存储这该方法的一些信息;一个线程拥有自己的栈,栈里存放着多个栈帧。

虚拟机栈有两种异常情况:StackOverflowError和OutOfMermoryError。我们知道,一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss 参数可以设置虚拟机栈大小),若线程请求的栈深度大于虚拟机允许的深度,则抛出 StackOverFlowError 异常。栈的大小是固定的,也可以是动态扩展的,若虚拟机栈可以扩展,但是扩展时候无法获取足够的内存(比如没有足够的内存为一个新创建的线程分配栈空间时),则抛出OutOfMermoryError

虚拟机栈最小可设置成108k,默认是1024k。

 

    3.本地方法栈

本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机JAVA方法服务,而本地方法栈为虚拟机执行native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError 和 OutOfMemoryError 异常。

二、线程共享数据区

包括了java堆和方法区两个区域。

  1. java堆

Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。Java堆是线程共享的,类的对象从中分配空间,这些对象通过new、newarray、 anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。

注意,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。而且,Java堆在实现时,既可以是固定大小的,也可以是可拓展的,并且主流虚拟机都是按可扩展来实现的(通过-Xmx(最大堆容量) 和 -Xms(最小堆容量)控制)。如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出 OutOfMemoryError 异常。

   2. 方法区

方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译(JIT)后的代码等数据。也可以抛出OutOfMermoryError异常。

运行时常量池是方法区的一部分,用于存放编译器生成的的各种字面量和符号引用。运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。

三、JIT、逃逸分析、TLAB

1、JIT

JIT(just in time)即时编译器,能够加速java程序的执行速度。通常都是java源文件经过javac命令编译成为字节码文件(class文件),JVM将字节码翻译成机器指令,逐条读入,逐条进行解释翻译。经过解释执行,其执行速度必须会比可执行的二进制字节码程序(这里不是class文件)慢很多,为了提高速度所以引入JIT,它会在运行时把翻译过来的机器码保存起来,以备下次使用。说白了就是在原本的解释翻译的过程中加入这项技术来加快速度。

 

2、逃逸分析

逃逸分析是一种有效减少java程序同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,编译器能够分析出一个新对象的引用的使用范围从而决定是否将这个对象分配到堆上

若一个方法体内的对象不被其他方法或者线程得到,我们可以把对象直接存放在栈上,当JVM能证明一个对象不会逃逸到方法或者线程外,则可能(因为逃逸分析不是百分百都执行的)为这个变量进行一些高效的优化(指的就是把该对象分配到栈中,指的是虚拟机栈)。因此也得出不是所有的对象都会分配到堆中。

我们知道在堆上的对象是被多个线程共享的,共享就要考虑多线程的安全问题,那么就需要锁的消耗,而且即使不使用该对象,也不会立即回收。如果把对象放在栈上,会随着栈的出栈一起释放。减轻GC的压力。

3、TLAB

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。但对象还是分配在堆上的,只不过是堆上的一块区域而已

Java对象在虚拟机中的创建过程和访问定位

一、对象在虚拟机中的创建过程

  1. 检查虚拟机是否加载了所要new的类,若没加载,则首先执行相应的类加载过程。虚拟机遇到new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个引用代表的类是否已经被加载、解析和初始化过。
  2. 在类加载检查通过后,对象所需内存的大小在类加载完成后便可完全确定,虚拟机就会为新生对象分配内存。一般来说,根据Java堆中内存是否绝对规整,内存的分配有两种方式:   a)指针碰撞:如果Java堆中内存绝对规整,所有用过的内存放在一边,空闲内存放在另一边,中间一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相同的距离。                                                                                             b)空闲列表:如果Java堆中内存并不规整,那么虚拟机就需要维护一个列表,记录哪些内存块是可用的,以便在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
  3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
  4. 在上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,此时会执行<init>方法把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。

直白点就是:如果类已经被加载了,那么创建对象步骤就是分配内存,然后初始化为零值,最后就是执行构造函数赋值。如果没类没被加载,那么就先加载类。

二、对象在虚拟机中的访问定位

创建对象是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。在虚拟机规范中,reference类型中只规定了一个指向对象的引用,并没有定义这个引用使用什么方式去定位、访问堆中的对象的具体位置。目前的主流的访问方式有使用句柄访问和直接指针访问两种。

  1. 句柄访问方式

使用句柄访问对象,会在堆中开辟一块内存作为句柄池,句柄中储存了对象实例数据(属性值结构体)的内存地址,访问类型数据的内存地址(类信息,方法类型信息),

对象实例数据一般也在heap中开辟,类型数据一般储存在方法区中。使用句柄访问的好处是句柄中储存的是稳定的对象地址,当对象被移动时候,只需要更新句柄中的对象实例部分的值即可,句柄本身不用被移动修改。 

   2.指针访问方式

直接指针访问方式指reference中直接储存对象在heap中的内存地址,但对应的类型数据访问地址需要在实例中存储,使用直接指针的好处相对于句柄来讲,少了一次指针定位时间的开销,缺点是,当对象被移动时(如进行GC后的内存重新排列),对象的引用(reference)也需要同步更新。

 

使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,reference本身不需要修改;而使用直接指针访问的最大好处就是速度快,节省了一次指针定位的时间开销。

三、案例分析:

内存异常产生情况分析

一、方法区和运行时常量池溢出(OOM)

二、java堆溢出(OOM)

对象数量到达最大堆的容量限制后就会产生内存溢出异常。

  1. public class TestJvm {  
  2.     public static void main(String[] args) {  
  3.         List list = new ArrayList();  
  4.         while (true){  
  5.             int[] tmp = new int[1000000000];  
  6.             list.add(tmp);  
  7.         }  
  8.     }  
  9. }  

要解决这个异常,一般先通过内存映像分析工具对堆转储快照分析,确定内存的对象是否是必要的,即判断是 内存泄露 还是 内存溢出。

三、虚拟机栈和本地方法栈溢出 (SOF/OOM)

1、SOF

如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出StackOverflowError异常。我们知道,每当Java程序启动一个新的线程时,Java虚拟机会为它分配一个栈,并且Java虚拟机栈以栈帧为单位保持线程运行状态。每当线程调用一个方法时,JVM就压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。 那么可以想象,如果方法的嵌套调用层次太多,比如递归调用,随着Java虚拟机栈中的栈帧的不断增多,最终很可能会导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,从而产生StackOverflowError溢出异常。

  1. public class TestJvm {  
  2.     public static void main(String[] args) {  
  3.         method();  
  4.     }  
  5.   
  6.     // 递归调用导致 StackOverflowError  
  7.     public static void method(){  
  8.         method();  
  9.     }  
  10. }  

2、OOM

如果虚拟机在拓展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。在虚拟机栈和本地方法栈发生OOM异常场景如下:当Java 程序启动一个新线程时,若没有足够的空间为该线程分配Java栈(一个线程Java栈的大小由-Xss设置决定),JVM将抛出OutOfMemoryError异常。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值