文章目录
内存结构
程序计数器(寄存器)
Java源代码不能被cpu直接执行,需要经过编译,编译成二进制的字节码,二进制字节码中的一行行代码就是jvm指令
Java跨平台技术就是靠这一条条jvm指令,对任何操作系统都是一致的
这些指令再经过解释器解释成机器码,机器码可以被cpu执行
程序计数器的作用就是在解释器解释jvm指令的过程中记住下一条jvm指令的执行地址
在物理上程序计数器是通过寄存器(cpu中读取最快的一个单元)实现的(因为读取地址是非常频繁的)
极小的一块内存
每条线程都需要一个独立的程序计数器(程序计数器是线程私有的)
是在jvm规范中,唯一一个不会存在内存溢出的区
当前线程所执行的字节码的行号指示器
字节码解释器通过改变计数器的值选取下一条要执行的字节码的指令
是程序控制流的指示器
虚拟机栈
- 每个线程运行所需要的内存,称为虚拟机栈
- 每个栈由栈帧构成,对应每次方法调用时所占的内存
- 每个线程只能有一个活动栈帧,对应当前正在执行的那个方法
也是线程私有的,生命周期与线程相同,线程结束,栈结束,所以不存在垃圾回收问题
栈内存不是越大越好,栈的内存大了,线程数就少了,内存大了只能增快方法的递归调用
方法内局部变量是否具有线程安全问题:
如果这个变量是共享的,如static,就会有线程安全问题, 如果这个变量逃离了方法的作用范围,也会有线程安全问题
否则,不会有线程安全问题
每个方法被执行时,jvm会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信 息
**栈帧:**每个方法运行时需要的内存(如参数,局部变量,返回地址,这些都要提前分配内存)
8大基本类型,对象引用,实例方法
局部变量表
- 存放了各种jvm基本数据类型,对象引用(reference类型),returnAddress类型,这些数据类型在局部变量表中的存储空间以局部变量槽(Slot),来表示。
- 64位的long,double类型占用两个变量槽,其余数据类型占用一个。
- 所需要的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小(指变量槽的数量)
两类异常状况
- 如果线程请求的栈深度超过了虚拟机允许的深度(栈帧过多,栈帧过大),抛出StackOverflowError异常
- 如果虚拟机栈容量可以动态扩展,当栈扩展时无法申请足够的内存抛出OutOfMemeoryError异常
HotSpot虚拟机的栈容量是不可以动态扩展的,所以在HotSpot上不会由于·虚拟机栈无法扩展而导致OutOfMemeoryError异常,但是如果线程申请栈空间失败,仍会出现OOM异常
线程运行诊断
- cpu占用过多
- 程序运行很长时间没有结果
本地方法栈
与虚拟机栈发挥作用非常相似,和虚拟机栈一样,也会在栈深度溢出和栈扩展失败时抛出异常StackOverflow,OOM
区别:
虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的本地(Native)方法服务
堆
VM options 控制堆内存大小为8mb -Xmx 8m
- 虚拟机管理内存中最大的一块
- 是被所有线程共享(堆中对象需要考虑线程安全问题)的一块内存区域,一个jvm只有一个堆内存,堆内存大小可以调节
- 在虚拟机启动时创建
- 唯一目的是存放对象实例,几乎所有对象实例都在这里分配内存
- 是垃圾收集器管理的内存区域,也被称为GC堆
从回收内存角度来看
现代垃圾收集器大部分都是基于分代收集理论设计的,如新生代,永久代等,而这些仅仅是一部分垃圾收集器的设计风格而已,不是虚拟机固有布局,也不是对堆的进一步细致划分
从分配内存角度看
所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,用来提升对象分配时的效率
无论从哪个角度,堆中存储的都只能是对象的实例,将Java堆细分只是为了更好的回收内存,更快的分配内存
Java堆不需要连续的内存
Java堆既可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是按照可扩展来实现的,如果堆中没有内存完成实例分配,并且堆无法扩展时,将抛出OOM异常
方法区
-
被所有线程共享
-
静态变量,常量,类信息(构造方法,接口定义),运行时常量池存在方法区中,但是实例变量存在堆内存中和方法区无关(重点)
static , final,Class模板,常量池
-
方法区是堆的一个逻辑部分(关于他到底是不是堆的一部分,不同的jvm厂商实现方式不同),但是有个别名叫
非堆
,从而与Java堆区分开来
JDK8之前是使用永久代实现的方法区,考虑到HotSpot的发展,这种实现方式被逐步放弃,改为用本地内存来实现方法区
JDK7时,将原本放在永久代的字符串常量池,静态变量等移出
JDK8,完全废弃的永久代的概念,改用本地内存中实现的元空间,把JDK7中剩余的内容全部移到元空间中
和Java堆一样不需要连续的内存,可以选择固定大小和可扩展,甚至可以选择不实现垃圾收集
相对来说,方法区垃圾收集比较少见,这部分内存回收的目标是针对常量池的回收和对类型的卸载
JDK8前会导致永久代内存溢出
JDK8后会导致元空间内存溢出
运行时常量池
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
- 是方法区的一部分
- 常量池是.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变成真实的地址
串池(StringTable)
StringTable特性:
常量池中的字符串仅仅是符号,只有用到时才变成对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接原理是StringBuilder (1.8)
字符串常量拼接原理是编译器优化
可以用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象放入串池时,如果有则不会放入,没有就将这个对象放入,最后返回串池中的对象
- 1.6如果有不会放入,如果没有把对象复制一份放入串池,最后返回串池中的对象
区别:1.8放的是堆中的地址,1.6放的是对象副本的地址
String对象的加载是延迟的,只有走到才会将对象放到串池
/**
* @author gwj
*/
public class HelloWorld {
public static void main(String[] args) {
//常量池中的信息,运行时被加载到运行时常量池,这是a b ab都是常量池中的符号,还没有变成Java字符串对象
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
StringTable 是哈希表结构,不能扩容
字符串的拼接
public class HelloWorld {
public static void main(String[] args) {
//常量池中的信息,运行时被加载到运行时常量池,这是a b ab都是常量池中的符号,还没有变成Java字符串对象
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //new StringBuilder().append("a").append("b").toString()
//new String("ab")
System.out.println(s3 == s4);
//s3在串池中,s4在堆中,所以false
//javac 在编译期间的优化,结果在编译期已确定为ab
//而上一行代码中s1和s4是变量,只能在运行期间用StringBuilder动态拼接
String s5 = "a" + "b";
}
}
StringBuilder的toString源码,new了一个String对象
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
从字节码中能看出,在执行String s4 = “a” + “b”
时,生成字节码与String s3 = “ab”
相同
串池的位置
- JDK1.6时,StringTable在常量池中,常量池在方法区中,方法区使用永久代实现
- JDK1.8时,StringTable从永久代转到了堆中(因为串池中存着大量字符串,放在永久代中使用效率低,且永久代垃圾回收较难触发
StringTable垃圾回收
当内存空间不足时,StringTable中那些没有被引用的字符串就会被回收
StringTable性能调优
- 如果系统中字符串常量个数非常多,建议将StringTableSize调大些,减少哈希冲突,提高查找效率
- 可以通过intern方法让字符串入池,从而减少重复字符串的个数,减少内存的占用
直接内存
直接内存不是虚拟机运行时数据区的一部分,也不是定义的内存区域,是操作系统内存,但是也被频繁使用,也可能导致OOM异常
不受jvm内存回收管理,直接内存的释放可以通过调用Unsafe对象的freeMemory方法
常见于NIO操作中,用于数据缓冲区
JDK4中加入了NIO类,引入了一种基于通道和缓冲区的I/O方式
可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样避免了在Java堆和Native堆中来回复制数据