参考视频
运行时数据区
JVM架构总览图
绿色的:方法区,堆,是所有线程共享的
黄色的: 虚拟机栈,本地方法栈,程序计数器,是线程私有的
程序计数器
程序计数器是一块较小的内存空间,物理上用寄存器实现,可以看作当前线程所执行的字节码的行号指示器。
作用: 记住下一条JVM指令的执行地址
特点:
1 是线程私有的,随着线程的创建而创建,随着线程的消息而消息
2 是一小块内存
3 唯一一个不会内存溢出的内存区域
栈
介绍
栈:程序运行需要的内存空间
虚拟机栈: Java方法执行的线程内存模型,即每个线程运行时所需要的内存
数据结构:先进(压栈)后出(出栈)
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每个方法被调用执行完毕的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。
一个栈可以看成多个栈帧组成,每个栈帧可以看成每个方法的运行时需要的内存(参数,局部变量,返回地址等)
定义:
1 每个线程运行时所需要的内存,成为虚拟机栈
2 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
3 每个线程只能有一个活动栈帧,活动栈帧即当前正在执行的那个方法
问题辨析:
1 垃圾回收是否涉及栈内存?
答:不需要。 每次方法结束后都会出栈,自动被回收,所以不需要垃圾回收。
2 栈内存分配越大越好吗?
答:不是。内存是有限的,栈内存越大,线程越少。
Linux/MacOs/Oracle Solaris : 栈内存大小默认1024k
-Xss1024k
3 方法内的局部变量是否线程安全?
答:如果方法内 局部变量没有逃离方法的作用范围,它是线程安全的。如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出
- 栈帧过多,即调用的方法过多,最容易产生的:递归调用(测试2w多次递归会报错)
- 栈帧过大,不太容易出现
本地方法栈(native method stacks)
不是由java代码编写的方法,java用本地方法调用底层的c或c++使用的方法
与虚拟机栈发挥的作用非常相似,区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地(Native)方法服务。
给本地的方法的运行提供内存空间
线程私有
堆(Heap)
介绍
通过new关键字,创建对象都会使用堆内存
特点:
- 它是线程共享的,在虚拟机启动时创建
- 唯一目的:存放对象实例
- Java堆是垃圾收集器管理的内存区域,也被成为“GC堆”
- 虚拟机所管理的最大内存
- 可以处于物理上不连续的两块空间内,但在逻辑上应该被视为连续的。
堆内存溢出
配置堆内存大小:-Xmx 最大Java堆空间大小,-Xms 初始Java堆
方法区
定义
方法区是一个逻辑上的概念,也被称为非堆(Non-Heap),一般用来存储类加载信息、static静态变量、即时编译器编译后的代码缓存数据、常量池(Constants Pool)等。
不同版本的Java其方法区的实现方式不同,在JDK 8之前,采用的是“永久代”来实现方法区,而在JDK 8之后则是采用MetaSpace(元空间)的方式来实现,元空间移到本地内存(native memory)里,存储类的元数据信息,而字符串常量池(也成为串池)和静态变量移到堆中。
元空间的大小受限于本地内存的大小,可以动态地扩展,减轻了类元数据区溢出的问题
垃圾收集在这里非常少,这区域的回收主要是常量池的回收和对类型的卸载
- 共享性: 方法区与Java堆一样,是各个线程共享的内存区域。
- 创建和内存空间: 方法区在JVM启动时被创建,实际的物理内存空间和Java堆一样,可以是不连续的。
- 大小和可扩展性: 方法区的大小,就像堆空间一样,可以选择固定大小或可扩展。
- 溢出问题: 方法区的大小决定了系统能够保存多少个类。如果系统定义了太多的类,导致方法区溢出,虚拟机将抛出内存溢出错误,例如
java.lang.OutOfMemoryError: PermGen space
或java.lang.OutOfMemoryError: Metaspace
。 - 释放: 关闭JVM将释放方法区的内存空间。
常量池 和 运行时常量池
JVM的常量池主要有以下几种:
- class文件常量池表
- 运行时常量池
- 字符串常量池(StringTable, 也成为串池)
- 基本类型包装类常量池
Class文件常量池
每个class的字节码文件中都有一个常量池表,里面是编译后即知的该class会用到的字面量
与符号引用
,这就是class文件常量池
。这部分内容会在类加载后放到方法区的运行时常量池。
运行时常量池
class类信息及其class文件常量池是字节码的二进制流,它代表的是一个类的静态存储结构,JVM加载类时,需要将其转换为方法区中的java.lang.Class
类的对象实例;同时,会将class文件常量池中的内容导入运行时常量池
是方法区的一部分,自然受到方法区内存的限制
具备动态性,Java语言并不要求常量只有编译期才能产生,运行期间也可以产生新的常量。eg:String类的intern()方法
字符串常量池
运行时常量池中的常量对应的内容只是字面量,比如一个"字符串",它还不是String对象;当Java程序在运行时执行到这个"字符串"字面量时,会去字符串常量池
里找该字面量的对象引用是否存在,存在则直接返回该引用,不存在则在Java堆里创建该字面量对应的String对象,并将其引用置于字符串常量池中,然后返回该引用
基本类型包装类常量池
Java的基本数据类型中,除了两个浮点数类型,其他的基本数据类型都在各自内部实现了常量池,但都在[-128~127]这个范围内
参考文档:
【JVM系列】运行时Java类的内存营地——方法区详解 - 知乎
Java基础-JVM内存管理-常量池与运行时常量池 - 简书
串池练习(JDK8)
第一题: String s1 = "a"; String s2 = "b"; String s3 = "a"+"b"; String s4 = s1 + s2; String s5 = "ab"; String s6 = s4.intern();System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); // s3 == s4 false // s3 == s5 true // s3 == s6 true /* 推导: String s1 = "a" -> "a" 在串池中创建 StringTable["a"] String s2 = "b" -> 在串池中创建 StringTable["a","b"] String s3 = "a"+"b" -> "a","b"都在串池中存在,这里的+号在编译器优化时直接变成"ab",放到常量池中。s3 = "ab",在串池中创建 StringTable["a","b","ab"]; String s4 = s1 + s2; -> 字符串拼接,调用StringBuilder,所以最终生成一个堆里的对象, s4 = new String("ab"); String s5 = "ab"; -> 串池中已有"ab",引用串池中的 "ab"; String s6 = s4.intern(); -> intern 是一个 native 的方法,如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回所以 s6 = 串池中的"ab" s3 == s4 : (s3 = "ab") == (s4 = new String("ab")) false,一个串池,一个对象地址引用 s3 == s5 : (s3 = "ab") == (s5 = "ab") true ,两个都指向串池中的字符串 s3 == s6 : (s3 = "ab") == (s6 = "ab") true ,两个都指向串池中的字符串*/第二题: String x1 = "cd"; String x2 = new String("c") + new String("d"); System.out.println(x1 == x2); // false /* 推导: String x1 = "cd" -> "cd" 在串池中创建 StringTable["cd"] String x1 = "cd" -> "cd" 在串池中创建 StringTable["cd"]*/
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,但这部分内存也被频繁使用,而且也可能导致内存溢出。
本机直接内存的大小不会受到Java堆大小的限制,但是既然是内存,肯定受到本机总内存大小以及处理器寻址空间的限制。
虚拟机对象
对象的创建
创建对象(例外:复制,反序列化)是用new关键字,虚拟机中如何创建的对象(本次讨论不包括数组和Class对象)?
1 Java虚拟机遇到一条字节码new指令
2 检查指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否加载,解析和初始化过。
2.1 没有,则执行相应的加载过程
3 虚拟机为新生对象分配内存(内存大小在类加载完成后便确定)
4 内存分配后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值
5 Java虚拟机对对象进行设置
例如:对象是哪个类的实例,如何能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等
6 上面完成后,从虚拟机角度看一个新的对象已经产生。但Java来看,对象创建才刚开始——需要初始化构造函数,即Class文件的<init>方法,字段还没有赋值等。
备注:
1 简化流程:
new指令 -> 检查参数是否定位到一个类的符号引用,并检查加载情况 -> 分配内存 -> 初始化为0 -> 对对象设置信息 -> Java的构造函数
2 分配方式:指针碰撞
步骤3,为新生对象分配内存。假设Java堆中的内存是绝对规整的,所有被使用的内存都放在一起,空闲的放在另一边,中间放一个指针作为分界点的指示器,那分配内存就仅仅是把指针向空虚方向挪动一段与对象大小相等的距离
3 分配方式:空闲列表(Free List)
步骤3,为新生对象分配内存。假设Java堆里的内存是不规整的,虚拟机则必须维护一个列表,记录哪些内存块可用,在分配的时候从列表中找到一个足够大的空间划分给对象实例,并更新列表上的记录
以上选用哪种分配方式,由Java堆是否规整决定,而Java堆是否规整则由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。
Serial,ParNew 等带压缩整理过程的收集器时,采用指针碰撞,简单高效
CMS这种基于清除(Sweep)算法的收集器时,采用较为复杂的空闲列表
创建是一个频繁操作,并发时线程并不安全。解决这种情况有两种方案:
方案一:
对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
方案二:
内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。
哪个线程要分配内存,就在哪个线程的本地缓冲区中分配;只有本地缓冲区的内存用完,需要新的缓冲区时才需要同步锁定。
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设置
对象的内存布局
对象在堆内存中的存储布局可以划分为三个部分: 对象头(Header), 实例数据(Instance Data) 和对齐填充(Padding)
对象头:
一类数据:存储对象自身的运行时数据:如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这类数据称为“Mark Word”,是动态定义的数据结构。
另一类数据:类型指针,即对象指向它的类型元数据的指针,确定哪个类的实例。
实例数据:
对象真正存储的有效信息。
这部分存储顺序受到虚拟机分配策略参数(-XX: FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。
HotSpot虚拟机默认的分配顺序:long/doubles ,ints, shorts/chars, bytes/booleans,oops
对齐填充:
对齐填充,并不不是必然存在,也没有特别含义,仅起到占位符的作用。
Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此对象实例数据部分没用对齐的话,需要对齐填充来补全
对象的访问定位
Java程序会通过栈上的reference数据来操作对上的具体对象。
对象访问方式由虚拟机实现而定,主流的有两种:句柄 和 直接指针。
句柄访问:
Java划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自具体的地址信息。
句柄图如下
直接指针访问:
reference中存储的直接就是对象地址,如果访问对象本身的话,不需要多一次间接访问开销。
直接指针访问图:
两种对象访问各有优势。
句柄访问最大好处是reference存储的是稳定的句柄地址,在对象被移动(GC行为)时只改变句柄中的实例数据指针,而reference本身不需要被修改
直接指针访问最大好处就是速度快,节省了一次指针定位的时间开销。Hotspot采用的就是这种方式访问
垃圾收集器与内存分配策略
针对垃圾回收对堆内存回收前,判断对象是否存活有两种算法:1 引用计数算法,2 可达性分析算法
引用计数算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效,计数器值减1;任何时刻计算器为0的对象就是不可能再被使用的。
原理简单,判定效率高,是个不错的算法。
但JVM没有使用它,原因:这种算法有很多例外情况需要考虑,必须配合大量额外处理才能保证正确的工作,比如相互引用。
objA.child = objB
objB.child = objA
可达性分析算法
当前主流的商用程序语言(Java,C#)的内存管理子系统,都是通过可达性分析算法判断对象是否存活。
基本思路:
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”。
如果某个对象到“GC Roots”之间没有任何引用链相连,则证明此对象是不可能再被使用的。
在Java体系里,固定作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,eg: 当前正在运行的方法所使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,eg:Java类的引用类型静态变量
- 在方法区中常量引用的对象,eg:串池(String Table)里的引用
- 在本地方法栈中JNI(通常指Native方法)引用的对象
- Java虚拟机内部的引用,eg:基本数据类型对应的Class对象,一些常驻的异常情况等,还有系统类加载器
- 所有被同步锁(synchronized)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等
四大引用
Java对引用的概念分为四大类:强引用,软引用,弱引用,虚引用
强引用:
最传统的“引用”定义,即 "Object obj = new Object()"
无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:
描述一些还有用,但非必须得对象。
软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象 列进 回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会跑出内存溢出异常。
JDK1.2版之后提供SoftRelerence类来实现软引用。
弱引用:
描述那些非必须对象,但是它的强度比软引用更弱,被弱引用的对象只能生存到下一次垃圾收集发生为止。
当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉。
JDK1.2版之后提供WeakReference类来实现弱引用。
虚引用:
也被称为“幽灵引用”,或者“幻影引用”,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
JDK1.2版之后提供了PhantomReference类来实现虚引用
对象收集过程
可达性分析算法判定不可达的对象,也不是“非死不可”。
过程:
可达性分析算法判断不可达的对象 -》第一次标记 -》此对象是否有必要执行 finalize()方法 -》
1 假如对象没有覆盖finalize()方法,或finalize()已经被虚拟机调用过 -》则没必要回收
2 假如对象判定有必要执行finaliz() -》放置到一个F-Queue的队列中 -》由虚拟机自动建立、低调度优先级的Finalizer线程去执行它们的finalize()方法
备注:
执行Finalize方法指虚拟机会触发这个方法开始运行,但不承诺一定会等待它运行结束。原因:如果某个对象的finalize()方法执行缓慢,或者发生了死循环,很可能导致F-Queue队列中的其他对象出于等待,导致整个内存回收子系统的崩溃。
finalize()方法是对象逃脱死亡命运的最后一次计划,稍后收集器将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。
任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
方法区的垃圾收集
方法区的垃圾收集的性价比很低,主要收集两部分内容: 废弃的常量 和 不再使用的类型。
垃圾收集算法
垃圾收集算法 可以划分 :引用计数式垃圾收集 (Reference CountIng GC, 不讨论) 和 追踪式垃圾收集 (Tracing GC)
分代收集理论
分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分带假说之上:
- 弱分代假说: 绝大多数对象都是朝生夕灭的。
- 强分代假说: 熬过越多次垃圾收集过程的对象就越难以消亡。
基于此确定了垃圾收集器的设计原则:
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(即熬过垃圾收集过程的次数)分配到不同的区域之中存储。
朝生夕死的对方放在同一个区域,就能以较低代价收集到大量空间。
所以基于分代收集两个假说确定了Java堆划分不同区域来垃圾收集的基调。
Minor GC,Magor GC, Full GC
参考文档:「JVM」Full GC和Minor GC、Major GC_jvm什么时候minor gc-CSDN博客
垃圾回收操作分为:Partial GC(Minor/Young GC,Magor/Old GC) , Full GC
Partial GC :部分收集
- Minor GC:新生代收集
- Magor GC :老年代收集
- Mixed GC:混合收集,目前只在G1收集器有。
Full GC:整堆收集
java堆分为:新生代,老年代
新生代中每次垃圾收集(Minor GC)都有大量对象死去,少量的存活对象逐步晋升老年代。
垃圾收集算法
标记-清除算法,标记-复制算法,标记-整理算法
参考文档:JAVA开发(JAVA垃圾回收的几种常见算法)_java垃圾回收算法-CSDN博客