文章目录
- 运行时数据区的组成概述
- 程序计数器(Program Counter Register)
- Java虚拟机栈(Java Virtual Machine)
- 本地方法栈(Native Method Stack)
- Java堆
- 方法区
运行时数据区的组成概述
JVM的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚拟机规范,Java8虚拟机规范中规定,Java虚拟机所管理的内容将会包括以下几个运行时数据区域:
-
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,可以把它看作是当前线程所执行的字节码的行号指示器。 -
Java虚拟机栈(Java Virtual Machine)
描述的是Java方法执行的内存模式,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。 -
本地方法栈(Native Method Atack)
与虚拟机栈的作用一样,只不过虚拟机栈是服务于Java方法的,而本地方法栈是为虚拟机调用Native(本地)方法服务的。 -
Java堆(Java Heap)
是Java虚拟机中内存最大的一块,是被所有线程所共享的,在虚拟机启动的时候就创建好了,Java堆唯一的目的就是存放对象的实例,几乎所有的对象实例都是在这里分配内存的。 -
方法区(Method Area)(也叫元空间)
方法区就是用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数。
方法区是很重要的系统资源,是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行。
JVM内存布局规定了Java在运行的过程中内存申请,分配,管理的策略,保证了JVM的高效稳定的运行,不同的JVM在对于内存的规划方式和管理机制存在着部分的差异。
我们现在以使用最为流行的HotSpot虚拟机为例讲解。
Java虚拟机定义了序列行期间会使用到运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是随着线程一一对应。这些与线程对应的区域会随着线程开始和结束而创建销毁的。
程序计数器(Program Counter Register)
概念
JVM中的程序计数寄存器(Program Counter Register)中的Register命名源自于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载放在计数器上才能运行。
这里的程序计数器并不是物理意义上所用CPU上的寄存器,可以将它翻译为PC寄存器(指令寄存器)会更加的合适(也称为程序钩子),并且不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
计数器是线程私有的
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此未来线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们成这类内存区域为 “线程私有” 的内存。
作用
● 程序计数器的作用主要就是用来存储下一条指令的地址,也就是将要执行的指令代码,由执行引擎来读取下一条的指令。
● 它是一块很小的内存空间,几乎是可以忽略不计的,它也是运行速度最快的存储区域。
● 在JVM的规范当中,每个线程都有它自己的程序计数器,属于线程私有的,所以它的生命周期是和线程的生命周期保持一致的。
● 在任何时间里面里面一个线程都只能有一个方法来执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。
● 程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
● 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
● 程序计数器也是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,就是唯一一个不会出现内存溢出的区域。
以单核CPU为例,计算机在运行的时候并不是将一个线程做完,再去干下一个线程。而是通过时间的控制,在各个线程之间来回运行,也就是实现线程上宏观的并行。那么在线程之间转换的时候,就通过PC寄存器记录线程中当前栈帧运行到第几行。以方便下次轮到这个线程的时候,接着向下运行。
寄存器记录当前线程做执行的位置。
Java虚拟机栈(Java Virtual Machine)
虚拟机栈出现的背景
由于Java只一种跨平台的设计,所以Java的指令都是根据栈来设计的。因为不同平台的CPU机构不同,所以是以面向对象的设计思想是不能基于寄存器来设计的。
基于栈的指令设计的优点就是跨平台,指令集少,编译器容易实现,缺点是性能下降,实现同样功能就需要更多的指令集才能实现。
栈和堆的区别
就可以理解为:
栈:就是解决程序的运行问题,就是程序如何的去执行,或者说如何的处理数据。也是由操作系统来代替我们进行分配。(加载方法运行的)
堆:就使解决的数据的存储问题。就是数据处理前或者处理后,应该把数据放在那个地方。就是由程序员来对其进行管理。(存储对象的)
什么是Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack),早期也叫做Java栈,每个
本地方法栈(Native Method Stack)
● 本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机中使用到的native(本地)方法服务。
● 本地方法是线程私有的。
● 允许被实现成固定或者是可动态的内存大小。内存溢出方面也是相同的。
如果线程请求分配的栈容量超过本都方法栈的最大容量则就抛出StackOverflowError
● 本地方法栈是用C语言写的。
● 它的具体做法是在Native Method Stack 中登记的Native方法,在Execution English执行时加载本地方法库。
就是当程序中需要调用本地方法时,会将本地方法加载到本地方法栈中执行。
Java堆
概述
-
一个jvm实例只存在一个堆内存,堆也是Java内存管理的核心区域。
-
Java堆区在jvm启动的时候就被创建,它的内存大小也就被确认好了。也是jvm管理里面占用最大的一块内存。
-
堆内存的大小是可以被调整的。
例如:-Xms:10m(堆的起始大小) -Xmx:30m(堆最大内存的大小)
在一般情况下是可以将起始值和最大值设置为一致的。这样会减少垃圾回收之后对内存重新分配大小的次数,提高效率。
如果在Java堆中没有内存完成实例的分配,并且堆也无法再扩展的时候,Java虚拟机将会抛出OutOfMemoryError异常。 -
在《Java虚拟机规范》中规定,堆是可以在物理上是不连续的,但逻辑上它是应该连续的。
-
在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”
-
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区。
-
在方法结束后,堆中的对象是不会马上就消失的,仅在垃圾收集的时候才会被移除。
-
堆是GC(Garbage Collected Heap ,垃圾收集器)执行垃圾回收的重点区域。
堆内存区域的划分
Java8之后堆内存划分为:新生区(新生代)+老年区(老年代)
新生区中又划分为Eden(伊甸园)和Survivor0(幸存者1、From)区以及Survivor1(幸存者2、To)区
为什么堆内存要分区?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间以及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
对象在堆内存中的过程:
图解:
对象创建内存分配的过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何去分配。在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC(垃圾回收)执行完内存后是否会在内存空间中产生内存碎片。
-
new的行对象首先是在新生代的伊甸园,这个区域是有大小所限制的。
-
当伊甸园的空间填满的时候,程序又需要创建对象的时候,JVM的垃圾回收器就会对伊甸园进行垃圾的回收(Minor GC),将伊甸园中的不再被引用的对象进行销毁,伊甸园就可以加载新的对象。
-
然后将伊甸园中还没有被销毁的对象放到新生区的幸存者0区域
-
当伊甸园又满的时候,再次发生垃圾回收,将伊甸园和放到幸存者0区的对象 ,还没有被回收的全部对象放到幸存者1区。
每次始终都要保持一个幸存者区域是空的 -
如果再次经历垃圾回收,此时又会将所有的幸存对象全部放到幸存者0区域,下次再垃圾回收的时候又放到幸存者1区域。
-
什么时候去养老区?在将对象放到幸存区的时候,会给对象赋上一个属性,叫阈值,默认情况下,阈值的大小是15次,也是可以自己来设置这个阈值大小的,最大为15次。
-XX:MaxTenuringThreshold = < N >
在对象头中,它是由4为数据来对GC的年龄(阈值)来保存的,所以最大值就位1111,转为十进制就是15。 所以在对象的GC 年龄到达15的时候,就会将对象从新生代转为老年代。 -
在老年区的话,就相对比较悠闲,不会对对象进行不断的垃圾回收,只会在当老年区内存不足的时候,会触发Major GC,来进行养老区的内存清理。
-
如果养老区执行了Major GC之后,发现依然无法对对象进行保存。就会产生一个OOM的异常。
java.lang.OutOfMemoryError:Java heap space
例子:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Demo1 {
public static void main(String[] args) {
List<Integer> list = new ArrayList();
while (true) {
list.add(new Random().nextInt());
}
}
}
在代码运行的时候,打开任务管理器,就能明显的看到,内存的大幅度上升,因为现在电脑都是有着自我保护机制,是不会跑满内存的。然后在idea报出异常后,内存才会就会出现垃圾回收,释放出内存。
也可以在下载的jdk目录文件中bin目录下找到jvisualvm.exe程序打开
在工具插件里面下载此插件,查看,堆内存的使用情况
然后执行代码,打开对应的查看
新生代和老年代的配置比例
配置新生代和老年代在堆结构的占比(一般不会调)
-
默认下的是:** -XX:NewRatio**=2
表示新生代占1,老年代占2,新生代占整个堆的三分之一。 -
可以修改为:** -XX:NewRatio**=4
表示新生代占1,老年代占4,新生代占整个堆的五分之一。 -
在发现,在整个项目当中,生命周期长的对象偏多,就可以通过调整老年代的大小,来进行调整优化。
在HotSpot虚拟机中,Eden(伊甸园)空间和另外两个Survivor(幸存者)空间所占的比例是8:1:1,当然开发人员也是可以通过选项** -XX:SurvivorRatio**来调整这个空间比例的。比如:-XX:SurvivorRatio=8
在新生区的对象默认生命周期超过15依然存活就会去老年区养老。
JVM调优
一般所说的JVM调优,就是调整jvm相关各区的参数。
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
不同代的回收思想
总共为Minor GC、Major GC、Full GC
JVM在进行垃圾回收的时候,并不是每次在新生代和老年代同时回收的,在大部分说回收的时候,都是指的新生代。
针对HotSpot虚拟机 的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。
部分收集:不是完整收集整个java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Yong GC):只是新生代(Eden,S0,S1)的垃圾回收器。
- 老年代收集(Major GC / Old GC):只是老年代的收集。
整堆收集((Full GC):收集整个JAVA堆和方法区的垃圾收集。
整堆收集的情况:
①System.gc();时;
②老年代空间不足的时候;
③方法区空间不足的时候。
在开发期间要尽量避免整堆的收集。(在垃圾回收的时候,会STW(Stop The World)停止java中的所有线程)
TLAB机制
TLAB(Thread Local Allocation Buffer(本地线程缓存))
为什么会有TLAB机制
由于堆区域在运行是数据区是线程共享的,所以任何的线程都是可以访问到堆区域中所有的共享数据。
又由于对象实例的创建在JVM中非常的频繁,因此在并发环境之下从堆内存中划分区域是线程不安全的。
所以为了避免多个线程同时操作堆中的同一地址,就需要使用加锁的机制,进而影响分配的速度。
什么是TLAB
TLAB的全称是Thread Local Allocation Buffer。即线程本地分配缓存区,这是一个线程专有的内存分配区域。存在新生代里面。
如果设置了虚拟机参数-XX:UseTLAB,那么在线程初始化的时候,同时也会申请一块指定大小的内存空间,只给当前的线程来使用。这样每个线程都有自己单独拥有的一个空间,如果需要分配内存,那么就在自己的空间上分配,每个线程都有自己单独的空间,这样就不会存在竞争的情况。那么就可以大大提高分配的效率。
JVM使用TLAB来避免多线程的冲突,在给对象分配内存的时候,每个线程使用自己的TLAB,这样就可以避免线程同步,提高了对象分配的效率。
在堆内存里的Eden区,TLAB空间的内存其实非常小,缺省情况下仅仅占有整Eden(伊甸园)空间的1%,也是可以通过-XX:TLABWasteTargetPercent这个选项来设置TLAB空间所占Eden空间大小的百分比。
字符串常量池
jdk7之前,将字符串常量池位置在方法区(永久代)中存储. 。
jdk8之后方法区又称为元空间,将字符串常量池的位置放到了堆空间.。
为什么将字符串常量池要调整位置?
因为在方法区当中的回收效率很低,在只有Full GC的时候才会执行永久代的垃圾回收将方法区的内存进行GC,而Full GC是老年代的空间不足、方法区空间不足的时候才会触发。
这就导致字符型常量池的回收效率不高,而我们开发中会有大量的字符串创建,回收效率低,就会导致永久代的内存不足。将它放到堆内存中就能及时回收。
方法区
方法区的基本理解
方法区,也是一个被线程共享的内存区域。其中主要存储加载的是类字节码、class / method / field 等元数据、static final常量、static变量、即时编译器(执行引擎)编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
在《Java虚拟机规范》中明确说明“尽管所有的方法区在逻辑上是属于堆的一部分,但在HotSpotJVM中而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是将它和堆做出区分来。”
所以,方法区看作是一块独立与java堆的内存空间。
方法区的特性
-
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
-
方法区的大小,和对空间是一样的,可以选择固定大小或者课扩展的。
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出。虚拟机就同样会抛出内存溢出的错误。
测试:
public class Demo2 {
public static void main(String[] args) {
String temp = "world"; //字符串是常量,值存储在字符串的常量池当中
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = temp + temp;
temp = str;
str.intern();//将字符串存储到字符串常量池中
}
}
}
也可以观察到方法区的空间瞬间就爆了,然后控制台就报出异常。
关闭JVM就会释放出这个区域的内存。
方法区大小的设置
Java方法区的大小也不是一成不变的,JVM是可以根据应用的需要来动态的调整。
● 元数据大小是可以使用参数-XX:MetaspaceSize和-XX:MaxMataspaceSize指定,替代上述原有的数据。
● 默认情况下是依赖于平台的,Windows下,-XX:MetaspaceSize是21MB
● 将-XX:MetaspaceSize的值给为 -1 时,级是没有限制的。
● 这个-XX:MetaspaceSize初始值为21M,也就称为高水位线,一旦到达就会响应的触发Full GC进行回收。
● 因此为了减少Full GC那么这个-XX:MetaspaceSize可以设置一个较高的值。
方法区的内部结构
方法区主要是用于存储已被虚拟机加载的类型信息、常、静态变量、即时编译器编译后的代码缓存,运行时常量池等。
可以通过反编译字节码文件来查看。
反编译字节码文件,并输出值文本文件中,便于查看。参数-P确保能保证查看 private权限类型的字段和方法。
在编译后的文件中操作:
然后输入就可以了。
就会在当前目录下生成test.txt文件,就可以查看了。
方法区的垃圾回收
有些人认为在方法区(在HotSpot虚拟机中的元空间是没有垃圾回收的)是没有垃圾收集的行为。在《Java虚拟机规范》中对方法区的约束是比较松的,有提到过是虚拟机可以不在方法区中实现垃圾收集。
一般来说在这个区域里面的垃圾回收效果是比较难以让人满意的,尤其是类型的卸载,条件相对于比较苛刻。但是这个部分的垃圾回收有时又确实是很有必要的。
在方法区的垃圾收集主要是回收两个部分的:运行时常量池中废弃的常量和不再使用的类型。
在方法区的垃圾收集也可以称为类卸载。
判定一个常量是否“废弃”还是相对比较简单的,但是要判断一个类型是否属于“不再被使用的类”的时候,条件就相对于比价苛刻了。需要同时满足三个条件:
- 当前该类所有的实例都已经被回收,也就是java堆中不存在该类及其任何派生的子类实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景。(如OSGI、JSP的重加载),否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。