这段时间开始系统学习一下JVM,本系列笔记旨在记录JVM 的学习笔记。
这一篇主要记录JVM 的运行时数据区的相关内容
1. 介绍下 Java 内存区域(运行时数据区域)?
Java 堆、永生代(JDK8中已经移到直接内存的元空间中)、Java 虚拟机栈、本地方法栈、程序计数器
1. 【线程共享】:同JVM的生命周期(实例启动、关闭)保持一致
- Java 堆:存放对象实例及数组的区域
Java 内存区域中最大的一块,存放几乎所有对象实例及数组(JDK 1.7开始默认开启逃逸分析,如果对象引用没有返回或者未在外面使用,则对象可以直接在栈上分配内存)
垃圾收集器管理的主要区域
因为现在的GC基本采用分代垃圾收集算法,为了更好地回收内存及更快地分配内存,所以Java 堆还可以细分为新生代、老年代
上述新生代可以分为Eden区(大部分情况对象首先在Eden区分配)、From Survivor区、To Survivor区
容易出现
OutOfMemoryError
错误:GC时间久且回收少,堆空间不足(和配置的最大堆内存有关,受限于物理内存大小)
- 方法区:
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,又称非堆
HotSpot JVM中的永久代(类似于类)即是对方法区(类似于接口)的实现
JDK8开始,永久代取消了,取而代之的是变成了元空间(方法区的新实现),使用直接内存。原因是永久代最大上限无法调整,OOM 几率大于元空间,加载的类少于使用元空间,JDK 1.8合并 HotSpot 和 JRockit(没有永生代)
JDK 1.7中字符串常量池拿到了堆里,JDK 1.8取消永生代后运行时常量池挪到元空间了(新的方法区)
运行时常量池用于存放编译期生成的各种字面量和符号引用,其中引用类型常量存的都是引用,对象都在Java堆上
直接内存:又称堆外内存,在本地内存中,不属于JVM运行时数据区域的体系,受到本机的操作系统的管理和硬件的限制
2. 【线程私有】:以下区域生命周期同线程保持一致
Java 虚拟机栈: 存放每一个Java 方法
Java 方法执行的内存模型,由一个个栈帧组成
栈帧包括局部变量表、操作数栈、动态链接、方法出口信息
- 局部变量表中存放编译期可知的基本数据类型和引用类型
- 会出现两种错误:
StackOverFlowError
(线程请求深度超过当前虚拟机栈的最大深度)和**OutOfMemoryError**
(动态扩展虚拟机栈时无法申请到足够内存)。注:HotSpot JVM中的虚拟机栈容量不能动态扩展,所以一般只会出现SOF Error,只有线程申请栈空间失败时才会出现OOM Error
本地方法栈:与虚拟机栈作用类似,区别在于虚拟机栈使用Java方法服务,本地方法栈使用Native 方法服务。注:HotSpot JVM中二者合二为一
程序计数器
- 理解成一个int变量,记录当前执行的字节码的行号
- 各线程的程序计数器互不影响
- 作用:1)字节码解释器通过改变它来依次读取指令,以实现流程控制;2)多线程中记录当前线程执行的位置,线程切换中保持运行状态的记忆
- 唯一不会出现
OutOfMemoryError
的区域 - 执行Native 方法时为空
二、Java对象创建的过程?
类加载检查——分配内存——初始化零值——设置对象头——执行init方法
- 类加载检查:检查指令参数能否在常量池中定位这个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化过。若无,则先执行相应的类加载过程
- 分配内存:在Java 堆中划分一块确定大小的内存,方式分为指针碰撞、空闲列表两种
- GC 是否带有压缩整理功能 —> Java 堆是否规整 —> 用哪种分配内存的方法
- 规整(无内存碎片)—> 指针碰撞;不规整 —> 空闲列表
- 如何保证创建对象时的线程安全:先TLAB(Eden区预留一块内存),后保证更新操作的原子性CAS(乐观锁)+失败重试
- 初始化零值:内存空间初始化零值(不包括对象头),保证对象的实例字段不赋值也能用
- 设置对象头:设置一些信息,HotSpot JVM 中是运行时数据(哈希码,GC分代年龄,锁的形状标志)+类型指针(指向类的元数据)
- 执行
<init>
方法
三、Java 对象的内存布局是什么样的?
HotSpot JVM中:对象头、实例数据、对齐填充(非必须,占位作用,保证对象起始地址是8 byte的整数倍)
四、访问Java 对象的方法?优劣?
- 二者异同在于Java 堆里的对象内存布局设计中有没有指向方法区类的元数据的类型指针
- 使用句柄访问的好处是在每次对象位置发生变动时只需要改变句柄中指向实例数据的指针,不过开销大
- 使用直接指针的好处是速度快,不过对象位置动了需要修改引用。HotSpot 用的是直接指针
五、字符串常量池相关问题
- 字符串常量池jdk 1.7之前在方法区的运行时常量池中(non - heap),jdk 1.7开始放进java 堆里啦
- 使用双引号和new String() 创建String对象的区别?
使用双引号””创建String 对象,JVM直接在常量池中找,有内容相同的就指向它,没有就在池子中创建这个对象后再指向它
如果字符串常量池里有与这个String 对象内容相同的字符串,则返回常量池中的引用
new String() 创建对象时,1)在Java 堆创建一个字符串对象,2)检查字符串常量池中有无内容相同的常量,3)如果无先在字符串常量池中创建内容相同的常量,4)返回堆中对象的引用(要避免用这种方法玩)
- 字符串常量池的使用原则(jdk 1.7之前保存对象,jdk 1.7及以后可以有引用进字符串常量池)?
使用双引号“”创建String 对象,对象直接存在常量池中
非双引号声明的String 对象,通过使用String 的intern()方法(一个Native方法),也能达到存常量池的效果:
如果没有,在jdk1.7之前(字符串常量池在堆外),在常量池中创建一个内容相同的字符串对象,并返回常量池中的引用;jdk1.7及以后(字符串常量池在堆内),为了减少堆的内存开销,不放对象进常量池了,直接把堆中对象的引用放进常量池。总之,目前该方法保证内存中只有这个对象的一份拷贝。
- Javac 编译器的常量折叠是什么?
Javac 编译器会把常量表达式的值求出来作为常量嵌在最终生成的代码中
只有编译器在程序编译期就可以确定值的常量才能被常量折叠:基本数据类型及String常量、final 修饰的变量、字符串通过“+”拼接得到的字符串、基本数据类型间的加减乘除及位运算
- String “对象引用 + 对象引用”的本质是什么?
通过StringBuilder 调用 append() 方法实现,拼接结束后toString()
1 | String str1 ="str"; // 常量池中的对象 |
- 基本类型的包装类的常量池技术是什么?
Byte、Short、Integer、Long 默认创建了[-128, 127]的缓存数据,Character是[0, 127],Boolean 是 true和false
Float、Double没有常量池技术
Integer 的值比较用equals() 方法,“==”还是比较引用的地址