参考: 地址
运行时内存区域
内存区域图解
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域,且 JDK 版本不同也有所区别。
JDK 1.8之前
JDK 1.8 之后
线程共享的:
- 堆内存
- 方法区
- 直接内存
线程私有:
- 虚拟机栈(Java栈)
- 本地方法栈
- 程序计数器
注:
- 永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现
- 直接内存不是虚拟机规范中定义的内存区域
虚拟机栈
虚拟机栈的主要内容:
- 虚拟机栈描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
- Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
- 局部变量表存放了各种数据类型以及对象引用
- 虚拟机栈会抛出两种异常:
StackOverFlowError:虚拟机栈的内存大小不允许动态扩展,线程请求栈的深度超过最大深度。
OutOfMemoryError:虚拟机栈的内存大小运行动态扩展,线程请求栈时内存已经用完。 - 每一次函数调用都用一个对应的栈帧被压入,调用结束则会有一个栈帧弹出。
本地方法栈
本地方法栈的作用和虚拟机栈相似,区别是:
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法(一个Java调用非Java代码的接口)服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
程序计数器
两个主要的作用:
- 字节码解释器是通过改变程序计数器来依次读取指令的,从而实现代码的流程控制(选择、循环、异常处理等)。
- 多线程情况下,由于程序计数器是线程私有的,所以互不影响;在线程切换时可以知道继续执行的位置。
堆
堆内存是所有线程共享的一块最大的内存区域,用于存放几乎所有的对象实例。
堆也是垃圾回收的主要区域,根据垃圾的收集算法,可以细分为如下:
新生代: eden,s0,s1
老年代: tentired
一般刚创建的对象都会先被分配在 eden 区域,在一次新生代垃圾回收后,如果对象还存活则进入 s0 或 s1(复制算法),并且对象的年龄加 1,当到达老年代的阈值时就会被放入老年代(如果一开始对象太大放入不了新生代则会直接放入老年代)。
方法区
方法区、永久代和元空间
方法区也是线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量等数据。
方法去也被称为永久代,通过下面两个参数调节大小:
XX:PermSize=N //方法区初始大小
-XX:MaxPermSize=N //方法区最大大小
在 JDK 1.8 的时候方法去被彻底移除,取而代之的是直接内存中的元空间,而其与方法区的区别:
- 方法区的内存大小是由 JVM 本身设置的固定大小,无法进行调整,即受虚拟机的限制。
- 元空间使用直接内存,其内存大小只受系统内存的限制。
运行时常量池
运行时常量池是方法区的一部分,主要存放两大类的常量,即存储 Java 类文件常量池中的符号信息:
- 字面量:文本字符串,final型常量值等。
- 符号引用量:类和接口的全限定名,字段名称和描述符、方法名称和描述符。
对象的创建过程
创建对象
有五步操作:
- 类加载检查: 检查类中引用的类是否已被加载过、解析和初始化过。
- 分配内存: 新生代分配内存,两种分配方式,即指针碰撞和空闲列表。
对应两种垃圾收集算法:标记-压缩和标记-清除。
内存分配并发问题的两种解决方式:
1.CAS+失败重试:乐观锁的一种实现,,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
2.TLAB:为每个线程预留内存,对象优先放入此内存,若超过则采用第一种方法。 - 初始化零值: 将分配到的内存空间初始化为零值,保证 Java 不赋值可以直接使用。
- 设置对象头: 存放属于那个类的实例,对象哈希码,对象的分代年龄等信息。
- 执行 init 方法: 上面的是 new 的过程,而要被 Java 程序所使用,还需要按照程序员的意愿进行初始化,即执行 init 方法。
访问对象
两种方式:
- 句柄: Java堆中划分出一块句柄池,访问过程为 栈->ref->句柄池->对象具体地址。
- 直接指针: 栈 ref 中存储的就是对象的地址。
String 类和常量池
String 对象的三种创建方式:
String s1 = "moke";
String s2 = new String("moke");
String s3 = s2.intern();
区别:
- 直接使用引号声明的 String 对象会直接存储在常量池。
- 使用 new 则是先将 moke 放入常量池后,再在堆内存创建一个新的对象,且指针指向堆对象。
- 使用 String 的 intern 方法则会先判断常量池中是否等于此 String 对象内容的字符串,有则直接返回;没有则创建后返回。
字符串的拼接
String s1 = "moke1";
String s2 = "moke2";
String s3 = "moke1" + "moke2";
String s4 = s1 + s2;
s3 是存放在常量池,而 s4 是在堆上创建。
基本类型的包装类和常量池
包装类与常量池:
- Byte,Short,Integer,Long,Character,Boolean 这五种基本类型的包装类实现了常量池技术。但超出范围(-128-127)还是在堆创建对象.
- Float,Double 两个浮点数类型没有实现常量池技术。
注: 当包装类进行符号运算时会自动拆箱,所以==号会比较数值是否相等,而不是地址。