注意:不同的虚拟机实现方式上也有差别,如果没有特别指出,这里的JVM指的是sun的HotSpot
在文章《java学习笔记2》中已做过相关笔记
JVM在运行时,将内存主要分为了:虚拟机栈、堆、方法区、本地方法栈、程序计数器这几个部分。
程序计数寄存器(Program Counter Register),这里并不是广义上物理寄存器,JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。
作用:用来存储指向下一条指令的地址,即将要执行的指令代码。
特点总结:
1.内存空间非常小,是运算速度最快的存储区
2.每一个线程都有一个自己的程序计数器,是线程私有的
3.如果执行的是java方法,pc寄存器记录的是下一条指令的地址;如果是本地方法,则是为指定值(undefined)
4.它是唯一一个在JVM规范中没有规定任何OutOfMemoryError
情况的区域
Java 虚拟机栈(Java Virtual Machine Stacks),早期也叫 Java 栈。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。
作用:程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。
特点总结:
1.线程私有的,访问速度仅次于程序计数器
2.JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈,方法执行结束出栈
3.栈不存在垃圾回收问题
4.虚拟机栈的大小可以是固定的,也可以是动态的。固定的情况,如果超过了栈的容量会发生StackOverflowError
异常;动态的情况,如果没办法申请足够的内存会发生OutOfMemoryError
异常。
5.栈中存储局部变量的基本存储单元的Slot(变量槽)。其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。
作用:本地方法栈用于管理本地方法的调用,本地方法由C/C++语言实现的。
特点总结
1.线程私有的
2.和虚拟机栈一样允许固定和动态扩展。固定的情况,如果超过了栈的容量会发生StackOverflowError
异常;动态的情况,如果没办法申请足够的内存会发生OutOfMemoryError
异常。
3.当线程调用本地方法时,不再受虚拟机的限制,拥有和虚拟机相同的权限。
4.在HotSpotJVM中,直接将本地方法栈和虚拟机栈合二为一。
注: 栈是运行时的单位,而堆是存储的单位。栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。
对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例。
为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):
年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1
特点总结:
1.大多数创建的对象都在伊甸园区域
2.当伊甸园区域满时,执行轻GC。如果有未被清理的对象则移入一个幸存者空间。
3.轻GC检查幸存者对象,并将他们移动到另外一个幸存者空间。所以每次一个幸存者空间总是空的。
4.经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代。
特点总结:
1.经过多次 GC 循环后存活下来的对象被移动到老年代。
2.老年代内存满时执行垃圾收集。老年代垃圾收集称为主GC(Major GC),通常需要更长的时间。
3.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝。
从逻辑上讲,元空间是堆的一部分, 然而在物理实现上却不是在堆空间中。所以元空间也叫非堆(Non-Heap)。元空间与后面的方法区结合起来介绍。
1.new 的对象先放在伊甸园区,此区有大小限制
2.当伊甸园区满了,JVM的垃圾回收器会对伊甸园区进行垃圾回收(minor GC),将不再受其他对象引用的对象销毁。再加载新的对象放到伊甸园中。
3.然后将伊甸园中剩余对象移动到幸存者0区
4.如果再次经历垃圾回收,此时上次幸存者0区的对象,如果没有回收,会被放到幸存者1区中
5.如果再次经历垃圾回收,会重新将幸存者1区中的幸存者放回幸存者0区
6.默认15次回收标记以后,幸存者会进入养老区
7.当养老区内存不足时,会触发major GC,对养老区内存进行清理
8.如果养老区清理了以后依然无法为新的对象分配内存(说明新生代,养老区都满了),会产生OOM异常。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)
TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区) 是 Java 中内存分配的一个概念,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。
作用: 在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。因为如果没有启动TLAB,多个并发执行的线程申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS ;启用TLAB之后(-XX:+UseTLAB, 默认是开启的),JVM 会针对每一个线程在 Java 堆中预留一个内存区域,预留动作会使用加锁或者CAS来避免同时预留给别的线程。一旦某个区域划分给某个线程后,该线程会优先从分配给自己的区域申请。
注:TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。
特点:
1.是所有线程共享的内存区域;
2.虽然Java虚拟规范把方法区逻辑上归为堆的一部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开;
3.方法区除了保存类的信息以外,还包括常量池(JDK1.8之前),用于存放编译期生成的各种字面量以及符号引用。受到方法区内存限制,如果常量池无法申请到内存时,会抛出OOM异常。
4.方法区允许大小固定和动态扩展。方法区决定了系统可以存放多少个类,如果类太多,导致方法区溢出,同样会抛出OOM异常。
5.JVM关闭后方法区立即会被释放。
方法区、永久代、元空间的区别:
方法区:(逻辑上)
1.是JVM的一个规范,所有虚拟机必须要遵守的。
2.是JVM所有线程共享的,主要用于存储类的信息、常量池、方法数据、方法代码等
3.方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆(Heap)区
永久代:是JDK1.8之前,HotSpot虚拟机基于JVM规范对方法区的一个落地实现
元空间:JDK1.8及之后,HotSpot虚拟机对方法区的新实现,与永久代最大的区别在于元空间并不在虚拟机中,而是使用本地内存。
常量池:JDK1.6在方法区,JDK1.7在堆,JDK1.8及之后在元空间。
方法区的垃圾回收:
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
对于常量池中的常量:只要常量池中的常量没有被任何地方引用,就可以被回收。
对于不在使用的类,需要满足以下三个条件,才允许被回收:
1.该类的所有实例都被回收,不存在该类和任何派生子类的实例
2.加载该类的类加载器已经被回收
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
区别于JVM的内存结构,JMM更多注重于线程并发过程中各线程之间的如何通信以及如何同步问题。
线程之间的通信机制有两种:共享内存和消息传递。
Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象角度看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存,每一个线程都有一个私有的本地工作内存,本地工作内存中存储着该线对共享变量读/写的副本。(本地工作内存是JMM的一个抽象概念,并不是真实存在的)
JMM抽象内存模型示意图:
一个变量从主内存拷贝到工作内存,再从工作内存同步回主内存的流程为:
|主内存| -> read -> load -> |工作内存| -> use -> |Java线程| -> assign -> |工作内存| -> store -> write -> |主内存|
Java 内存模型中的 8 个原子操作:
lock:作用于主内存,把一个变量标识为一个线程独占状态。
read:作用于主内存,把一个变量的值从主内存传输到线程工作内存中,供之后的 load 操作使用。
load:作用于工作内存,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use:作用于工作内存,把工作内存中的一个变量传递给执行引擎,虚拟机遇到使用变量值的字节码指令时会执行。
assign:作用于工作内存,把一个从执行引擎得到的值赋给工作内存的变量,虚拟机遇到给变量赋值的字节码指令时会执行。
store:作用于工作内存,把工作内存中的一个变量传送到主内存中,供之后的 write 操作使用。
write:作用于主内存,把 store 操作从工作内存中得到的变量值存入主内存的变量中。
unlock:作用于主内存,释放一个处于锁定状态的变量。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
1.编译器优化重排序;
2.指令级并行重排序;
3.内存系统重排序
as-if-serial语义:
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(通过语义分析来判断是否数据有依赖型)
内存屏障
为了保证有序性和内存可见性,java编译器会在适当位置插入特定类型的内存屏障,来禁止处理器重排序。JMM 把内存屏障指令分为下列四类:
happens-before概念用来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
常见的happens-before规则有:
happens-before规则和JMM的关系:
也就是说对于程序员而言JMM的实现的透明的,happens-before是JMM的进一步抽象的结果,了解happens- before 规则能满足程序员的需求。
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
缺点:两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。(幸存from区和幸存to区就是使用该算法)当标记15次还没有被清理,则移动到养老区。
优点:没有内存的碎片
缺点:使用了内存的一半,(极端情况:100%存活,则需要将大量数据进行复制,效率会变低)
最佳使用场景:对象存活率较低的场景,比如新生代
标记清除算法有两个阶段:
优点:不需要额外的空间
缺点:需要扫描两次,浪费时间,并且会产生内存碎片。
在标记清理算法的步骤上多一步压缩处理,即再扫描一次把碎片整理到一起,防止碎片的产生。
优点:不再有内存碎片
缺点:在标记清除算法基础上多一次扫描,增加扫描成本。