Skip to content
  1. 静态变量和常量存放在什么区域(方法区)
  2. 为什么需要字符串常量池,它在堆区还是方法区(堆区)
  3. 运行时常量池和字符串常量池有什么区别,它存放什么?(方法区,存放的类的字面量和符号引用)

Carson带你学JVM:图文解析 Java虚拟机的内存结构终于搞懂了Java8的内存结构,再也不纠结方法区和常量池了! - 腾讯云开发者社区-腾讯云

image.png

Java的内存结构

Java虚拟机的内存结构主要分为5个区:堆区、栈区、方法区、本地方法栈、程序计数器。 各个区各自的作用:

  1. 本地方法栈:用于管理本地方法的调用,里面并没有我们写的代码逻辑,其由native修饰,由 C 语言实现。
  2. 程序计数器:它是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、线程恢复等都依赖于计数器。
  3. 方法区(Java8叫元空间):用于存放已被虚拟机加载的类信息,常量,静态变量等数据。
  4. Java 虚拟机栈:用于存储局部变量表、操作数栈、动态链接、方法出口等信息。(栈里面存的是地址,实际指向的是堆里面的对象)
  5. 堆:Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

按线程私有、公有分类:

  • 线程私有:每个线程在开辟、运行的过程中会单独创建这样的一份内存,有多少个线程可能有多少个内存

Java虚拟机栈、本地方法栈、程序计数器是线程私有的

  • 线程全局共享的:堆和方法区

栈虽然方法运行完毕了之后被清空了,但是堆上面的还没有被清空,所以引出了GC(垃圾回收),不能立马删除,因为不知道是否还有其它的也是引用了当前的地址来访问的 image.png

虽然Java中有基本数据类型和引用数据类型,但是 Java中只有值传递 值传递:对象形参的修改不能改变实参 引用传递:对形参的修改能改变实参 基本类型创建值的副本,引用类型创建引用的副本。

堆区(Heap)

堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的通过new创建的对象实例都在这里分配内存。当对象无法在该空间申请到内存时抛出OutOfMemoryEroor异常。同时也是垃圾收集器管理的主要区域。

java
public Math{
	public static void main(String[] args){
		Math math = new Math();
		//在Java堆中存储一个Math对象。
        //主线程的Java栈中main的栈帧中局部变量表中存储着
        //一个math的引用,这个引用指向堆中的Math对象,
        //堆中的Math对象利用了方法区的类信息。
		System.out.println(math.math());
	}
}
}

Java的堆内存基于Generation分代收集算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。 分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。 三区域及对象的迁移过程:

新生代

Young Generation(1/3堆空间) 几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。大部分对象在Eden区中生成。当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。

老年代

Old Generation(2/3堆空间) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象。

元数据

MetaData Space(直接内存JDK1.8后) 不属于堆内存,属于内存空间。真正与堆隔离。方法区是类逻辑上的一个抽象模板,而元空间是方法区的实现,是真实存在的内存。

对象会首先会进入年轻代的Eden中.在GC之前对象是存在Eden和from中的,进行GC的时候Eden中的对象被拷贝到To这样一个survive空间中,From中的对象到一定次数会被复制到老年代。如果没到次数From中的对象会被复制到To中,复制完成后To中保存的是有效的对象,Eden和From中剩下的都是无效的对象,这个时候就把Eden和From中所有的对象清空。在复制的时候Eden中的对象进入To中,To可能已经满了,这个时候Eden和From中的对象就会被直接复制到Old Generation中.复制完成后,To和From的名字会对调一下,因为Eden和From都是空的,对调后Eden和To都是空的,下次分配就会分配到Eden。一直循环这个流程。好处:使用对象最多和效率最高的就是在Young Generation中,通过From to就避免过于频繁的产生FullGC(Old Generation满了一般都会产生FullGC) 虚拟机在进行MinorGC(新生代的GC)的时候,会判断要进入OldGeneration区域对象的大小,是否大于Old Generation剩余空间大小,如果大于就会发生Full GC。 刚分配对象在Eden中,如果空间不足尝试进行GC,回收空间,如果进行了MinorGC空间依旧不够就放入Old Generation,如果OldGeneration空间还不够就OOM了。 比较大的对象,数组等,大于某值(可配置)就直接分配到老年代,(避免频繁内存拷贝) 年轻代和年老代属于Heap空间的,Permanent Generation(永久代)可以理解成方法区,(它属于方法区)也有可能发生GC,例如类的实例对象全部被GC了,同时它的类加载器也被GC掉了,这个时候就会触发永久代中对象的GC。 如果OldGeneration满了就会产生FullGC。老年代满原因:1、from survive中对象的生命周期到一定阈值2、分配的对象直接是大对象3、由于To 空间不够,进行GC直接把对象拷贝到年老代(年老代GC时候采用不同的算法) 如果Young Generation大小分配不合理或空间比较小,这个时候导致对象很容易进入Old Generation中,而Old Generation中回收具体对象的时候速度是远远低于Young Generation回收速度。 因此实际分配要考虑年老代和新生代的比例,考虑Eden和survives的比例,提升系统性能。 Permanent Generation中发生GC的时候也对性能影响非常大,也是Full GC。

JVM栈(JVM Stacks)

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。Java栈描述的是Java方法执行的内存模型:一个线程对应一个栈,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。不存在垃圾回收问题,只要线程已结束栈就出栈,生命周期与线程一致。 方法出口指向下次执行的栈帧(方法) 内存说明:

  1. 基础数据类型直接在栈空间分配
  2. 方法的形式参数直接在栈空间分配,方法调用完成后从栈空间回收
  3. 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量。栈中的地址空间指向堆空间的对象区。
  4. 方法的引用参数,在栈空间分配一个地址空间,指向堆空间的对象区,方法调用完成后从栈空间回收。
  5. 创建new的局部变量,在栈中和堆中分配空间,当局部变量生命周期结束后,栈空间立刻回收,堆空间区域等待GC回收。
  6. 字符串常量,static静态变量在方法区分配空间。

本地方法栈(Native Method Stacks)

线程私有,可理解为java中JNI调用。用于支持native方法执行,存储了每个native方法调用的状态。本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。执行引擎通过本地方法接口,利用本地方法库(C语言库)执行。

方法区(Method Area)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,类的所有字段和方法的字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中

程序计数器(Program Counter Register)

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。他是线程私有的。可看做一个指针,指向方法区中的方法字节码(用来存储指向下一跳指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。 每个方法在运行时都存储着一个独立的程序计数器,程序计数器是指定程序运行的行数指针。

运行时常量池

到底什么是常量池,什么是运行时常量池? Math类,生成的对应的class文件,class文件中定义了一个常量池集合,这个集合用来存储一系列的常量。这时候的常量池是静态常量池。 当程序运行起来,会将类信息加载到方法区中,并为这些常量分配内存地址,这时原来的静态常量池就转变成了运行时常量池。 符号引用在程序运行以后被加载到内存中,原来的代码就会被分配内存地址,引用这个对象的地方就会变成直接引用,也就是我们说的动态链接了。 深刻理解运行时常量池、字符串常量池 - 掘金

对象分配规则

  1. 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  2. 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  3. 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值(默认是15)对象进入老年区。
  4. 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  5. 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。