@TryLoveCatch
2022-04-15T11:54:20.000000Z
字数 8237
阅读 14618
Java知识体系
Java源代码文件(.java)
会被Java编译器
编译为字节码文件(.class)
,然后由JVM
中的类加载器
加载各个类的字节码文件
,加载完毕之后,交由JVM执行引擎
执行。
JVM
在执行Java
程序的过程中会把它所管理的内存
划分为若干个
不同的数据区域。
JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。
了解清楚JVM的内存结构会更有助于我们理解Java的内存模型。
我们可以把上图的运行时数据区
分为线程私有
和共享数据区
两大类。
线程私有
的数据区包含程序计数器
、虚拟机栈
、本地方法栈
,即为本地区(native area)
线程共享
的数据区包含Java堆
、方法区
,在方法区
内有一个常量池
。虚拟机字节码
的地址。和计算机组成原理中提到的程序计数器PC概念类似,是线程私有的,用来记录当前执行的字节码位置。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。 虚拟机栈(JVM Stack)
也就是我们常常所说的栈。
方法执行的内存区,每个方法执行时会在虚拟机栈
中创建栈帧
,用于存储局部变量表(局部变量表需要的内存在编译期间就确定了所以在方法运行期间不会改变大小),操作数栈,动态链接,方法出口等信息。每一个方法从调用开始至执行完成的过程,就对应着栈帧
在虚拟机栈
中从入栈
到出栈
的过程。
这个区域有两种异常情况:
本地方法栈(Native Method Stack)
本地方法栈则为虚拟机使用到的Native方法提供内存空间。
每一个栈帧
都包括了局部变量表
,操作数栈
,动态连接
,方法返回地址
和一些额外的附加信息。
在编译代码的时候,栈帧
中需要多大的局部变量表
,多深的操作数栈
都已经完全确定
了,并且写入到了方法表
的Code属性
中,因此一个栈帧
需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。
一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎
来讲,活动线程中,只有虚拟机栈顶
的栈帧
才是有效的,称为当前栈帧(Current Stack Frame)
,这个栈帧
所关联的方法称为当前方法(Current Method)
。执行引用所运行的所有字节码指令都只针对当前栈帧
进行操作。栈帧的概念结构如下图所示:
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
可以这样理解,方法区存的是类的模版。比如说方法,其实就是行为,一个类的行为都是一致的,所以存在方法区;而变量,就是数据,每一个对象的数据都是不一样的,所以存在堆里
虚拟机栈的动态链接就是将符号引用(这些符号引用的集合就是常量池)转换为直接引用(符号引用对应的具体信息,这些具体信息的集合就是运行时常量池,存在方法区中)的过程。
我们写的每一个Java类被编译后,就会形成一份class文件(每个class文件都有一个class常量池)。 class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
常量池表会在类加载后存放到方法区的运行时常量池中。
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将 class常量池 中的内容存放到 运行时常量池 中,由此可知,运行时常量池 也是每个类都有一个。
在上面我也说了,class常量池 中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询 字符串常量池 ,以保证 运行时常量池所 引用的字符串与 字符串常量池 中所引用的是一致的。
字符串常量池存的是 引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
是在类加载完成,经过验证,准备阶段之后 在 堆 中生成字符串对象实例,然后 将该字符串对象实例的 引用值 存到 String Pool 中
版本 | 方法区位置 | 静态变量 | 字符串常量池 |
---|---|---|---|
1.6 | JVM内存 | 方法区 | 方法区 |
1.7 | JVM内存 | 堆区 | 堆区 |
1.8 | 本地内存 | 堆区 | 堆区 |
所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:年轻代(Young Generation Space)
,老年代(Old Generation Space)
。
年轻代
又可以划分为:伊甸区(Eden)
,幸存者区域(Survivor Sapce)
整体如下图所示:
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.log4j.Logger;
public class HelloWorld {
private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
public void sayHello(String message) {
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
LOGGER.info(today + ": " + message);
}
}
这段程序的数据在内存中的存放如下:
堆
是在 JVM
启动时创建的,主要用来维护运行时数据,如运行过程中创建的对象和数组都是基于这块内存空间。Java 堆
是非常重要的元素,如果我们动态创建的对象没有得到及时回收,持续堆积,最后会导致堆空间被占满,内存溢出。
因此,Java
提供了一种垃圾回收机制
,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把堆空间的垃圾全部进行回收,从而保证程序的正常运行。
也就是说,我们负责创建对象,GC负责来回收,那么如何认定是否是垃圾呢?
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。
上图中,object5
, object6
和 object7
便是不可达对象
,视为“死亡状态”,应该被垃圾回收器回收。
在Java语言里,可作为GC Roots对象的包括如下几种:
参考下图,黑色的表示垃圾,灰色表示存活对象,绿色表示空白空间。
结果如下:
这便是 标记-清理
方案,简单方便
,但是容易产生内存碎片
。
既然上面的方法会产生内存碎片,那好,我在清理的时候,把所有 存活 对象扎堆到同一个地方,让它们待在一起,这样就没有内存碎片了。
结果如下:
这两种方案适合 存活对象多,垃圾少
的情况,它只需要清理掉少量的垃圾,然后挪动下存活对象就可以了。
这种方法比较粗暴,直接把堆内存分成两部分,一段时间内只允许在其中一块内存上进行分配,当这块内存被分配完后,则执行垃圾回收,把所有 存活 对象全部复制到另一块内存上,当前内存则直接全部清空。
起初时只使用上面部分的内存,直到内存使用完毕,才进行垃圾回收,把所有存活对象搬到下半部分,并把上半部分进行清空。
这种做法不容易产生碎片,也简单粗暴;但是,它意味着你在一段时间内只能使用一部分的内存,超过这部分内存的话就意味着堆内存里频繁的 复制清空。
这种方案适合存活对象少,垃圾多
的情况,这样在复制时就不需要复制多少对象过去,多数垃圾直接被清空处理。
我们先来回忆一下,一块 Java 堆
空间一般分成三部分,这三部分用来存储三类数据:
局部变量
等在新创建后很快会变成 不可达 的对象
,快速死去 ,因此这块区域的特点是 存活对象少
,垃圾多
。即为新生代
;存活对象多
,垃圾少
。即为老年代
;永久代
。(永久带并不在java堆中,并且在 Java 8 里已经把 永久代 删除了。)也就是说,常规的 Java 堆
至少包括了 新生代
和 老年代
两块内存区域,而且这两块区域有很明显的特征:
对于新生代
区域,由于每次 GC
都会有大量新对象死去,只有少量存活
。因此采用 复制
回收算法,GC
时把少量的存活对象复制过去即可。
将新生代
区域分成8:1:1
,依次取名为 Eden
、Survivor A
、Survivor B
区,其中 Eden
意为伊甸园,形容有很多新生对象在里面创建;Survivor
区则为幸存者,即经历 GC
后仍然存活下来的对象。
工作原理如下:
Eden
区最大,对外提供堆内存。当 Eden
区快要满了,则进行 Minor GC
,把存活对象放入 Survivor A
区,清空 Eden
区;Eden
区被清空后,继续对外提供堆内存;Eden
区再次被填满,此时对 Eden
区和 Survivor
A 区同时进行 Minor GC
,把存活对象放入 Survivor B
区,同时清空 Eden
区和Survivor A
区;Eden
区继续对外提供堆内存,并重复上述过程,即在 Eden
区填满后,把 Eden
区和某个 Survivor
区的存活对象放到另一个 Survivor
区;Survivor
区被填满,且仍有对象未被复制完毕时,或者某些对象在反复 Survive 15
次左右时,则把这部分剩余对象放到Old
区;Old
区也被填满时,进行 Major GC
,对 Old
区进行垃圾回收。那么,所谓的 Old
区垃圾回收,或称Major GC
,应该如何执行呢?
根据上面我们知道,老年代
一般存放的是存活时间较久的对象,所以每一次 GC
时,存活对象比较较大,也就是说每次只有少部分对象被回收。
因此,根据不同回收机制的特点,这里选择 存活对象多,垃圾少
的标记整理
回收机制,仅仅通过少量地移动对象就能清理垃圾,而且不存在内存碎片化。
至此,我们已经了解了 Java 堆内存
的分代原理
,并了解了不同代根据各自特点采用了不同的回收机制,即 新生代
采用 复制回收
机制,老年代
采用 标记整理
机制。
jvm在执行java程序的时候,会把它所管理的内存,分为若干个不同的数据区域,这些区域可以分为线程私有和线程共享两部分。
线程私有的主要是程序计数器、虚拟机栈和本地方法栈:
程序计数器,是一段很小的内存空间,可以理解为字节码的行号指示器,字节码解释器通过改变它的值来选择下一条指令,这里是唯一的没有oom的内存区域
虚拟机栈,方法执行的内存区域,每一个方法在执行的时候,都会在栈中创建一个栈帧,一个方法从执行到结束,就是栈帧从入栈到出栈的过程。栈帧用于存储局部变量表、操作数栈、动态链接和方法返回地址等一些信息。对于执行引擎来说,只有栈顶的栈帧是有效的,称为当前栈帧,对应的方法为当前方法,执行引擎中运行的字节码只针对与当前栈帧。
这个区域会有两种异常:
1、当请求的栈深度大于jvm允许的深度时,会发生StackOverFlow异常
2、当扩展栈空间的时候,无法申请到足够的内存空间时,就会报OutOfMemory异常
线程共享主要是堆和方法区:
// 分配了一个又一个对象
放到Eden区
// 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:full GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...
栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法调用层次太深,请求的栈深度大于虚拟机所允许的深度
public class SOFTest {
public void stackOverFlowMethod(){
stackOverFlowMethod();
}
public static void main(String[] args) {
SOFTest sof = new SOFTest();
sof.stackOverFlowMethod();
}
}
通过递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常 :
https://www.jianshu.com/p/60ab4f63b59d
https://blog.csdn.net/xiaojin21cen/article/details/105300521
https://javaguide.cn/java/jvm/memory-area.html#%E5%89%8D%E8%A8%80
https://www.cnblogs.com/code-duck/p/13577103.html
JAVA的内存模型及结构
Android面试一天一题(Day 44:实战美团--Java内存模型)
Android中高效的显示图片 - Bitmap的内存模型
Java虚拟机的堆、栈、堆栈如何去理解?
Java-技术之垃圾回收机制
JVM内存模型解析
深入理解Java虚拟机笔记---运行时栈帧结构
深入探究JVM | Java的内存区域解析
Java 虚拟机面试题全面解析
深入理解JVM03--判断对象是否存活(引用计数算法、可达性分析算法,最终判定)