JVM内存结构图
总体来说,分为以下两部分:
- 线程共享区(方法区,堆)同时这也是JVM优化的主要问题所在
- 线程隔离区(java栈,本地方法栈,程序计数器)
JDK1.8之后改变如下:
元数据区取代了永久代(方法区),元空间的本质与永久代类似,都是对JVM规范方法区的实现,不过区别为:元空间使用的是本地内存,并不不在虚拟机中。
方法区(永久代)
方法区: 储存已经被虚拟机加载的类信息(版本、方法、字段),常量,静态变量,即时编译器编译后的代码,运行时常量池也在方法区中。
运行时常量池 :方法区的一部分,用于存放编译器生成的各种字面量和符号引用,位于方法区中。
字符串常量池:8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊,由字符串常量池进行协调,字符串常量池位于堆中。它的主要使用方法有两种:
(1)直接使用双引号声明出来的String对象会直接存储在常量池中。
(2)如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
JDK1.8后为什么要移除方法区
- 永久代中储存类信息,常量,静态变量等数据,很容易遇到内存溢出的问题,JDK1.8之后将字符串常量池和静态变量移入堆内存中,运行时常量池在元空间中。
- 对永久代调优是很困难的,同时将堆的垃圾回收机制与元空间进行隔离,避免了永久代引发的Full Gc 和OOM 问题。
堆
堆:主要存放类的实例对象和数组对象,堆是共享数据区,可以与各个线程共享同时也是JVM最大的内存区域,同时也是 垃圾收集器 最重要的工作区域
- 当Eden内存足够时,新创建的对象都被存放在Eden中(部分大对象会直接进入老年代)
- 大对象(如很长的字符串以及数组)会直接分配到老年代,这是为了避免在 Eden 区 和 Survivor区之间发生大量的内存复制(因为新生代会采用复制算法进行垃圾收集)
- 当Eden内存不足时,就会发生Minor GC,未被引用的对象就会被回收,如果此时对象仍然没有被回收,就会被移动到From Servivor区域中。
- Servivor中的对象每熬过一次GC增加一岁,默认超过15岁依然存活的对象被移入老年代(Tenured)
- 如果在Survivor区中 所有相同年龄对象的大小总和 大于
Survivor区内存大小一半时,所有大于或等于该年龄的对象都会直接进入老年代。 - 一般情况下,只有其中一个Servivor持有对象,另一个在下次GC之前总为空,发生GC之后,Eden中的对象被移入标记为To 的空Servivor中,From中未达到年龄的对象此时也会复制到To中,此时To被标记为From,原来的From 被标记为To,轮换的目的是为了防止因没有连续的内存空间导致 对象直接被移入老年代
- 当老年代空间使用达到一定比例时会触发Full GC
为了保证线程安全和避免内存争用,每一个线程会在Eden中设置一小块私有缓存区,成为TLAB,每一个TLAB都只有一个线程可以分配对象,因此可以避免使用全局锁来控制内存分配
Full GC是清理整个内存空间,包括新生代和老年代,如果Full GC后,堆中仍然无法存储对象,就会抛出【OOM】异常
堆的内存空间与链表相似,可以处于物理不连续空间中,但是逻辑上必须是连续的,当堆中没有内存完成实例分配并且堆也无法再增加空间时,就会抛出【OOM】异常
根据以上对于堆内存的介绍情况,在以后的学习过程中有以下两点注意事项
(1)严格限定对象的作用域,避免作用域溢出,导致对象总被引用而无法回收
(2)多使用单例,少用New
线程安全的本质
线程安全的本质是多个线程对堆内存中同一个Count变量进行操作,每一个线程会在内部创建一个Count变量的副本,线程内所有操作都是对count副本进行修改,如果这时其他线程修改了这个堆内存的count的值,改变了count值对这个线程是不可见的,当前线程修改完count变量的值,写入堆内存时,就会覆盖其他线程操作的count的值,引发线程不安全的问题。
程序计数器(PC寄存器)
程序寄存器:是一块较小的内存空间(不会发生内存溢出【OOM】问题),是当前线程执行的字节码的行号指示器
一个线程的正确执行,是通过字节码解释器改变当前线程计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行
- 在程序执行过程中,线程之间不断切换,为了确保线程切换后可以恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立储存,是线程的私有内存
- 如果线程执行的是java方法,计数器记录的是正在执行的虚拟机字节码指令地址,如果为native方法,计数器为空(Undefined)
java栈
如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出 StackOverflowError
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError【OOM】 异常。
Java栈:线程私有,生命周期与线程相同。
- 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,方法的调用直到执行完成的过程对应着栈帧的入栈到出栈的过程。
局部变量表中储存的是方法的参数以及方法内定义的局部变量,包括8中基本数据,对象引用 (但是不储存对象的内容,对象内容存放在堆中) 以及returnAddress类型,其中64位长度的long和double类型占用两个局部变量空间,其余类型占用一个局部变量空间,所需要的内存空间在编译期间完成分配。
如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
操作数栈是一个先进后出栈,操作数栈元素可以为任意java类型的数据,方法刚开始执行时,操作数栈是空的,方法执行过程中通过字节码指令对操作数栈中的元素进行入栈和出栈的操作,通常进行算数运算是通过操作数栈来完成的,或者调用其他方法时通过操作数栈进行参数传递,操作数栈可以理解为栈帧中用于计算的临时数据储存区。
动态链接: Class文件的常量池中存在大量的符号引用,字节码的方法调用指令就以指向常量池的引用作为参数。部分符号引用在运行期间转化为直接引用,这个转化的过程为动态链接。
直接引用:当类已经加载到虚拟机时,通过地址调用该类
符号引用(常量池中):在编译器不知道类是否被加载,先用符号代替该类,等实际运行时再用直接引用替换间接引用
静态链接: 符号引用在类加载阶段或者第一次使用时转化为直接引用。
方法出口信息:方法执行完毕后必须返回到调用它的地方,在栈帧中必须保存一个方法返回地址。
堆栈的区别
(1)功能上的区别如上
(2)线程共享和线程私有的区别:
- 堆是线程共享的
- 栈是线程私有的
(3)空间大小:
栈内存远远小于堆内存,栈的深度是有限的,如果递归没有及时跳出,很可能会发生StackOverFlowError问题
本地栈
与java栈类似,本地方法栈主要用于Native方法(可能底层调用C或者C++)服务,而java栈主要用于执行java方法(字节码)服务。