文章目录
-
- JVM(Java virtual machine)
-
- 1.JVM内存模型(JDK8)
- 2.内存区域分类
- 3.各区域的介绍及其作用详解
-
- 3.1 程序计数器
- 3.2 虚拟机栈
- 3.3 本地方法栈
- 3.4 堆
- 3.5 方法区
JVM(Java virtual machine)
1.JVM内存模型(JDK8)
Java1.8的改动:
将永久代移除,取而代之的是元空间。
元空间不再与堆是连续的物理内存,而是改为使用本地内存(Native Method)。也就意味着只要本地内存足够,就不会出现OOM(OutOfMemory)的错误。
2.内存区域分类
线程私有的区域:程序计数器、虚拟机栈、本地方法栈
线程共享的区域:堆、方法区、直接内存
3.各区域的介绍及其作用详解
3.1 程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号的指示器。
如果线程正在执行的是一个Java方法,程序计数器记录正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,程序计数器的值为Undefined。
程序计数器的主要作用:
- 在单个线程执行的过程中,字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、循环、异常执行。
- 在多线程并发运行的情况下,程序计数器用于记录当前线程执行的位置,从而保证线程切换的时候能够从上一次的位置继续工作。
值得注意的是,程序计数器是JVM虚拟机规范中唯一一个没有规定OOM(OutOfMemory)Error的内存区域。
显然,由于程序计数器保存的是字节码指令的地址,因此它占用的空间为一个很小的定值。所以,这个区域不会出现OOM的情况。
程序计数器的生命周期同它所属于的线程的生命周期。
3.2 虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就是对应的栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表:
局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float和reference类型的数据。reference类型表示对一个对象实例的引用。
操作数栈:
操作数栈是数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。操作数栈可理解为虚拟机栈中的一个用于计算的临时数据存储区。
:将局部变量表中的i压入操作数栈,将局部变量表中i自增,然后取栈顶的i使用。
i++
:将局部变量表中的i自增,然后将i压入操作数栈中,最后取栈顶的i使用。
++i
这两个操作分为多步,不能保证原子性。
如何使其具有原子性?使用循环CAS操作。
动态链接:
每个栈帧中包含一个在常量池中对当前方法的引用。
方法返回地址:
方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
- 返回值压入上层调用栈帧。
- 异常信息抛给能够处理的栈帧。
- PC计数器指向方法调用后的下一条指令。
虚拟机栈可能会出现两种错误:
StackOverFlowError
和
OutOfMemoryError
。
-
:若虚拟机栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出StackOverFlowError
。StackOverFlowError
-
:若虚拟机栈的内存大小可以动态扩展,在虚拟机动态扩展栈的时候无法申请到足够的内存空间,就会抛出OutOfMemoryError
。OutOfMemoryError
3.3 本地方法栈
和虚拟机栈所发挥的作用非常相似。
区别是: 虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
本地方法栈也会出现
StackOverFlowError
和
OutOfMemoryError
。
3.4 堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
JDK1.7之后,字符串常量池从方法区被移动到了堆中。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代。
再细致一点,新生代又可以分为Eden、From Survivor、To Survivor 空间等。
进一步划分的目的是更好地回收内存,或者更快地分配内存。
默认新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
默认eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )
常见的堆设置:
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值
3.5 方法区
方法区与Java堆一样,是各个线程共享的内存区域。
它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
JDK7之后,废弃了永久代的概念,改用在本地内存中实现的元空间(Metaspace)来代替。
-XX:MetaspaceSize=N //设置 Metaspace 的初始大小
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
JDK7之后,字符串常量池被从方法区拿到了堆中,但是运行时常量池还在方法区之中。
class常量池和运行时常量池:
class常量池简介:
我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
每个class文件都有一个class常量池。
什么是字面量和符号引用:
字面量包括:
1.文本字符串
2.八种基本类型的值
3.被声明为final的常量等;
符号引用包括:
1.类和方法的全限定名
2.字段的名称和描述符
3.方法的名称和描述符。
运行时常量池
存在于方法区中,也就是class常量池被加载到内存之后的版本,不同之处是:
它的字面量可以动态的添加
(如使用String#intern())
,符号引用可以被解析为直接引用
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。