堆:线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
堆结构:
- 存放数据:
对象实例,数组。
- 控制参数:
参数 | 描述 |
---|---|
-Xms | 堆内存初始大小 |
-Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
-XX:NewRatio | 新生代和老年代的比值,值为4 表示 新生代:老年代=1:4,即年轻代占堆的1/5 |
-XX:SurvivorRatio=8 | 年轻代中Eden区与Survivor区的容量比例值,默认为8,表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10 |
-XX:+HeapDumpOnOutOfMemoryError) | 内存溢出时,导出堆信息到文件 |
-XX:+HeapDumpPath | 堆Dump路径 -Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump |
-XX:OnOutOfMemoryError | 当发生OOM内存溢出时,执行一个脚本 -XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat |
-XX:MaxTenuringThreshold=7 | 表示如果在幸存区移动多少次没有被垃圾回收,进入老年代 |
- 垃圾回收
从结构上来分可以分为新生区、老年区、永久区(JDK 8)时取消永久区使用元空间取代;新生区又分为eden区以及survivor0区和survivor1区。所有新生成的对象首先会存放在新生区中的eden 区,在进行垃圾回收时,先将eden 区存活的对象复制到survivor0中,当survivor0 区存满就会将eden 区、survivor0 区存活对象复制到survivor1 区中,清空eden 区、survivor0 区;survivor1 区与survivor0 区互换角色。始终保持一个survivor 区是空的。以此往复… 当最终eden区 与某一个survivor 区存满时,存活对象就会被移送到老年区中。如果老年区都存满对象就会触发Full GC对所有区域进行垃圾回收。新生区发生的垃圾回收称Minor GC,并且发生频率非常高,不一定是Eden区满了才执行。
- 异常情况
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常(OOM 异常)
方法区:线程共享。存储类元数据,常量池,方法信息,静态类变量等。(JDK 7 永久区)(JDK 8 元空间)
- 存放数据:
类元数据,常量池,方法信息,静态类变量等。
- 控制参数:
-----------------------JDK 7 永久区------------------------
-XX:PermSize 设置最小空间。
-XX:MaxPermSize 设置最大空间。
-----------------------JDK 8 元空间------------------------
MetaSpaceSize 初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize 限制元空间大小上限,防止异常占用过多物理内存
- 垃圾回收
对此区域会涉及但是很少进行垃圾回收。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
- 错误信息
如果方法区太大,超过设置,会报OutOfMemoryError:PermGen space错误。
注意:JDK1.8使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。为了释放管理压力,把运行时常量池交给堆去管理。
常量池
- 什么是常量池?
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真是地址
- 常量池的作用?
常量池的作用就是避免重复创建和销毁对象影响性能,实现对象共享。
- Integer 常量池
public class Demo {
public static void main(String[] args) {
Integer integer = new Integer(10);
Integer integer2 = new Integer(10);
// 自动装箱:实际上是调用了 Integer.valueOf(int)
Integer integer3 = 10;
Integer integer4 = 10;
Integer integer5 = 600;
Integer integer6 = 600;
System.out.println(integer == integer2); // false
System.out.println(integer3 == integer4); // true
System.out.println(integer5 == integer6); // false 为什么上面是 true 而下面是 false?原因就是底层在常量池中存在一个Integer 缓冲数组
}
}
常量池中的Integer 缓冲数组,源码:
- String 常量池(串池)
常量池底层使用StringTable数据结构保存字符串引用,实现和HashMap类似,根据字符串的hashcode定位到对应的数组,遍历链表查找字符串,当字符串比较多时,会降低查询效率。
String 通常有两种方式来创建对象
String str = new String("abc");
String str2 = "abc";
第一种方式是通过new 实例化对象,new String(“abc”) 会保存到堆中。每次调用都会创建一个新对象。
第二种方式现在栈上创建一个String 类对象引用变量str ,然后通过符号引用取字符串的常量池寻找是否存在“abc”,如果没有就将“abc” 放到常量池中,如果存在就引用常量池中的“abc”。
使用第二种创建方法查看,是否引用字符串常量池的同一个字符串:
public class Demo{
public static void main(String[] args){
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); // true
}
}
因为对于String 来说,String 类属于引用类型,equals方法判断值内容是否相同,==判断值地址是否相同,经上述的比较发现内容相同的情况下地址是相同的,说明引用的都是常量池中的值也就是“abc”。
注意点:
- 字符串的+ 号拼接问题:
public class Demo{
public static void main(String[] args){
String str1 = "abcde";
String str2 = "abc"+"de";
System.out.println(str1==str2); // true
}
}
原因:对于字符串常量的 + 号连接,在程序编译期,JVM就会将其优化为 + 号连接后的值。所以在编译期其字符串常量的值就确定了。
- 字符串引用的+ 号拼接问题:
public class Demo{
public static void main(String[] args){
String str1 = "abc";
String str2 = "abcde";
String str3 = str1+"de";
System.out.println(str2==str3); // false
}
}
原因:对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象。
发生了什么?
反编译:
解决方法:
1. 使用final 是否可以解决字符串引用的 + 号连接问题呢?
public class Demo{
public static void main(String[] args){
final String str1 = "abc";
String str2 = "abcde";
String str3 = str1+"de";
System.out.println(str2==str3); // true
}
}
因为:使用了final 修饰在编译期就可以确定str1 的值。所以 str1 + "de"就等同于 “abc” + “de”,所以结果是 true。
2. String对象的intern()方法主动将串池中还没有的字符串对象放入串池
在我们调用String.intern的时候会往hashtable里插入一项,这个table就是stringtable
JDK1.8:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
JDK1.6:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
public class Demo{
public static void main(String[] args){
String str1 = "abc";
String str2 = "abcde";
String str3 = str1+"de";
System.out.println(str2==str3.intern()); // true
}
}
方法栈:线程私有。存储局部变量表(基本数据类型、形参)、操作栈、动态链接、方法出口,对象指针。
区别于本地方法栈,java虚拟栈为jvm 执行Java方法服务,本地方法栈则是jvm 执行native 方法服务。
- 存放数据:
局部变量表(基本数据类型、形参)、操作栈、动态链接、方法出口,对象指针。
- 控制参数:
-Xss控制每个线程栈的大小。
- 垃圾回收
见 Slot 复用?
- 错误信息
在Java 虚拟机规范中,对这个区域规定了两种异常状况:
-
StackOverflowError: 异常线程请求的栈深度大于虚拟机所允许的深度时抛出;例如:不合理的方法递归调用
-
OutOfMemoryError 异常: 虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出。
- 拓展
什么是栈与栈帧?
栈就是一条先进后出的队列,而栈帧就是栈中的元素。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间。
入栈和出栈?
压栈(入栈)和弹栈(出栈)都是对栈顶元素(栈帧)进行操作的。
栈帧中储存什么?
每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表(基本数据类型、形参)、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
局部变量表?
栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。
JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。
验证Slot 存在,以int 和long类型为例:
操作数栈?
操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。
看一个例子:
public class Demo {
public static void main(String[] args) {
test(10, 20);
}
public static int test(int i, int j) {
int k = i + j;
return k;
}
}
cmd 反汇编得到
说明:test 方法刚开始执行时,操作数栈是空的。当执行 iload_0 时,把局部变量 0 压栈,即 100 入操作数栈。然后执行 iload_1,把局部变量1压栈,即 98 入操作数栈。接着执行 iadd,弹出两个变量(100 和 98 出操作数栈),对 100 和 98 进行求和,然后将结果 198 压栈。然后执行 istore_2,弹出结果(出栈)。
下面通过一张图,对比执行100+98操作,局部变量表和操作数栈的变化情况。
Slot 复用?
为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。通过一个例子来理解变量“失效”。
看一个例子,看看什么是 slot 复用:
再看一个例子,看看垃圾回收,如何判定:
本地方法栈:线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。jvm调用一些本地方法,为这些本地方法提供的内存空间。
- 异常情况
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
程序计数器:线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。
为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。多线程执行程序依赖于CPU分配时间片执行,画个简单的图,看看多线程怎么利用CPU时间片的。如下图,线程0和线程1分配cpu时间片交替执行程序,假设此时线程0先获取到了时间片,时间片用完后CPU会将时间片再分配给线程1,线程1执行完毕后,此时,时间片又回到线程0来执行,那么问题来了,线程0上次执行到哪儿了呢?具体是代码的多少行了呢,该行代码有没有执行完毕?此时程序计数器就发挥作用了,程序计数器保存了线程的执行现场,方便下次恢复运行。这也是为什么程序计数器是线程独享的原因。
如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。