一、内存模型
JVM的内存模型如下图所示,由堆、方法区、java栈、本地栈和程序计数器组成。
image
- 堆和方法区 是所有线程共享的。并且是归GC 管理的内存区域。
- 程序计数器、java栈和本地方法栈 是每个线程所独有的。
下面逐个分析:
1、程序计数器
- Java运行时数据区中的一小块内存区域。在JVM中多个线程轮流获得时间片,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰。程序计数器是每个线程所私有的。
- 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
- 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变。
2、Java 栈
imageJava栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。
3、本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
4、方法区
image方法区 是各个线程共享的内存区域,用于存储被虚拟机加载的类的信息(类的全限定名、属性的名称和修饰符、方法的名称和修饰符)、常量、静态变量等。
4.1、类型信息
- 类型的全限定名
- 超类的全限定名
- 直接超接口的全限定名
- 类型标志(该类是类类型还是接口类型)
- 类的访问描述符(public、private、default、abstract、final、static)
4.2、类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)。
4.3、字段信息(该类声明的所有字段)
- 字段的类型
- 字段名称
- 字段修饰符(public、protect、private、default)
4.4、方法信息
方法信息中包含类的所有方法,每个方法包含以下信息:
- 方法修饰符
- 方法返回类型
- 方法名
- 方法参数个数、类型、顺序等
- 方法字节码
- 操作数栈和该方法在栈帧中的局部变量区大小
异常表
4.5、类变量(静态变量)
指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。
4.6、 指向类加载器的引用
每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。
7、指向Class实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
4.7、方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的,类似C++虚函数表vtbl。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
Class 文件中除了有类的版本、字段、 方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字 面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java 虚拟机对 Class 文件的每 一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求, 这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的 提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。
运行时常量池相对于 Class 文件常量池
的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员
利用得比较多的便是 String 类的 intern()方法。
运行时常量池 与类class文件常量池
class 文件的格式:
image image image image javac Test.java
javap -v Test.class > info.txt
总结 一
-
对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成CONSTANT_Class_info常量池项放置到常量池中;
-
类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有对应的CONSTANT_Class_info常量池项;
-
类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中。
总结 二
- Int 的字面量 仅仅[-128,127] 中的值 才会缓存到运行时常量池中;
- float 和double的字面量 均未实现运行时常量池,所以不会保存到运行时常量池。
- String类型的字面量都会保存到运行时常量池。
https://blog.csdn.net/songwenbinasdf/article/details/79421107
5、堆内存
Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java堆被所有线程共享。所有被new出来的对象要在堆上分配。
java堆分为新生代和老年代。
- 新生代主要存储刚刚产生的对象,如果对象的生命足够长,就把老年对象移入老年代。 新生大分为三级:eden(刚出生)、survivor space0(幸存者0)、survivor space1(幸存者1)。
新生代发生的GC 称为 Young GC。
eden、survivor0 和 survivor1的比率是8:1:1
- 老年代:经历过若干次YoungGC
幸存下载的对象,会被移动到Old Generation 老年代。
当老年代区域无法为新“晋级”到Old generation的对象分配内存空间时,就会发送GC,称为Old GC
二、对象的加载和访问
1、对象的创建过程
1.1 new的过程
- 进行类加载检查
当遇到一个new指令,首先检查能否在方法区的常量池中能否定位到这个类的符号引用,并且检查类有没有进行加载、解析和初始化; - 分配空间
有两种常见的分配方式,一是指针碰撞,二是空闲列表,分别针对连续分配内存和不连续的,有空隙的,取决于虚拟机是否会压缩整理。内存分配的大小是在类加载完成之后就已经确定的,但是分配的时候修改指针的指向位置应该是线程安全的(栈上的Reference),第一种方式就保证原子性。 - 初始化
将分配的内存初始化为各中数据类型的默认值,如整型数据初始化为0值(对象头除外) - 对象初始化init<>
进行基本的设置,确定这个对象是哪个类的实例,对象的HASH码,对象的年龄等等。
1.2 对象的内存布局
对象在内存中的存储的布局可以分为3块区域:对象头、实例数据和对齐填充。
- 对象头(Header)
对象头包含两个部分的信息,第一部分是对象自身的运行时数据,如哈希码、GC分代年龄、持有的锁等等;第二部分是类型指针,指向它的类元数据的指针,通过这个虚拟机来确定这个对象是哪个类的实例。
- 实例数据(Instance Data)
对象真正存储的数据,就是程序代码中定义的字段内容。 - 对齐填充(Padding)
用于使对象的开头必须是8字节的整数倍,无特殊意义。
2、对象的访问
Java程序通过栈上的reference数据来操作堆上的具体对象,由于reference类型在Java虚拟机规范中只规定了一个指向对象引用。而没有规定这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,它取决于Java虚拟机实现。
目前主要有两种实现方式:
使用句柄(类似间接指针):在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例数据与类型各自具体地址信息。示例图如下图所示。
image直接指针访问:Java堆中的对象布要考虑如何放置访问类型数据相关的信息,而reference中存储的直接就是对象地址。示例图如下图所示。
image句柄的好处:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问的最大好处就是速度更快,节省一次指针定位时间开销,因为对象访问在Java中非常频繁,这类开销积少成多也非常可观。目前HotSpot 虚拟机是采用的直接指针访问策略。
https://www.cnblogs.com/chenyangyao/p/5296807.html
三、垃圾回收机制
1、垃圾回收算法
标记清除算法
标记清除算法是最基础的垃圾收集算法。算法分为标记和清除两个阶段。
首先标记处所有可回收的对象,在编辑完成后统一回收掉所有被标记的对象。
它有两个缺点:一个是效率问题,标记和清除过程效率都不高。另一个是空间问题:标记清除之后会差生大量不连续的内存碎片。空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。
image
标记整理算法
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间。所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法。
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
image
分代收集算法
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-整理”算法来进行回收。
2、垃圾回收器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
image
-
jvm将内存空间(堆)分为老年代和新生代,然后垃圾收器是针对不同年代作用的。
-
除了G1收集器外,其他收集器都是只服务于新生代和老年代中的一个。
-
连线表示新生代的垃圾收集器和老年代的垃圾收集器可以协同工作。
Serial 收集器:新生代收集器。
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。垃圾收集的过程中会Stop The World(服务暂停)。
- 采用“复制”算法
- 单线程,即只会使用一个CPU或一条收集线程去完成垃圾收集的工作。
- 在垃圾回收时,必须暂停其他所有线程的工作线程,即所谓的“Stop The World。
参数控制:-XX:+UseSerialGC 串行收集器
image
Serial old 收集器:属于老年代收集器
- 采用“标记-整理”算法
- 单线程,即只会使用一个CPU或一条收集线程去完成垃圾收集的工作
-
Serial old收集器可以和Serial收集器协同工作,实现”分代回收“。
image
ParNew收集器
ParNew收集器 属于新生代收集器,Serical收集器的多线程版本
- 采用”复制“算法
- 多条线程进行垃圾回收
- 需要停止所有的用户线程
- 多CPU模式下,ParNew搭配CMS将是很好的选择。
参数控制:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
Parallel Scavenge收集器
Parallel Scavenge可控制的吞吐量、新生代收集器。
所谓吞吐量就是CPU用于运行代码的时间和CPU总消耗时间的比值,即吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
- 采用”复制“算法,并行收集。
- 精确控制吞吐量:XX:MaxGCPauseMillis控制最大垃圾收集停顿时间;XX:GCTimeRatio直接设置吞吐量
- 高吞吐量可以有效的利用CPU,尽可能完成程序的任务,也就是越适合在后台运算而不需要太多交互任务。
Parallel Old
Parallel Old 属于老年代收集器,使用“标记-整理”算法,只能和Parallel Scavenge配合使用。
Parallel Scavenge 和 Parallel Old 组合常用于注重吞吐量以及CPU资源敏感的场合。
image
CMS
Concurrent Mark Sweep - 并发的垃圾收集器,且采用标记-清除算法,属于老年代收集器。
CMS的垃圾清理分为四个阶段:
- 初始标记(CMS initial mark):这一阶段仍然需要Stop The World,该阶段的任务仅仅是标记一下GC Roots能直接关联到的对象。这一阶段速度是很快的。
- 并发标记:这一阶段是GC Roots后的延伸,即找出GC Roots能关联到的对象,即GC Roots Tracing的过程。该阶段是和用户线程并发执行的
- 重新标记(CMS remark) :由于上一阶段并发标记是并发的,这意味着在进行GC Roots Tracing时,用户进行仍会改变已标记的对象的状态。故该阶段重新标记也是Stop The World,是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分,该阶段的时间比初始标记略长,但比并发标记时间短。
- 并发清除(CMS concurrent sweep):和用户程序一起运行。清除上述标记的垃圾。
CMS的四个阶段中耗时最长的并发标记和并发清除,他们是和用户线程一起执行的,而耗时较短的初始标记和重新标记则耗时较短。
缺点:
- 对CPU资源非常敏感。并发导致垃圾收集线程和用户线程竞争资源, 当CPU数量少时,垃圾收集线程将和用户线程抢占CPU。因此,当CPU数较高时,才建议使用CMS。
- 无法有效处理浮动垃圾。
由于并发标记和并发清除阶段仍有用户线程,因此会导致不断有垃圾产生,这部分垃圾无法在当次阶段被清理,这部分垃圾就被称为“浮动垃圾”。
由于在清理的同时伴随着用户线程,因此堆上还需要预留空间给用户线程使用。这导致CMS运行期间,可能预留的空间无法满足程序的需要,此时就会出现“Concurrent Mode Failure”。CMS垃圾收集器失效,将会启用Serial Old。 - 垃圾碎片 。
“标记-清除” 会产生垃圾碎片。当无法容纳大对象时,就会触发一次Full GC,会在Full GC发生前启用一次内存整理合并。
G1 收集器
Garbage-First G1既可以作用于新生代又可以作用于老年代。采用”标记-整理“算法
工作原理:
对于G1来说,java堆被划分为多个大小相等的独立区域(Region),并跟踪每个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收时所需要的经验值),在后台维护一个优先列表,并根据允许的收集时间,优先回收价值最大的Region。
G1的垃圾回收分为四个步骤:
- 初始标记(Initial Marking) :仅仅是标记GC Roots能直接关联到的对象。需要Stop The World。
- 并发标记(Concurrent Marking):同CMS的并发标记,耗时较长,但可以与用户线程并发执行。
- 最终标记(Final Marking):
同样是为了修正在并发标记期间引起的变动。该阶段是需要Stop The World的,但垃圾回收线程却可以并行执行。 - 筛选回收(Live Data Counting and Evacuation):
对各个Region的回收价值和成本进行排序,根据用户所希望的GC停顿时间来执行回收计划。
G1 的特点:
- 并行与并发
并发标记阶段,用户线程和垃圾回收线程并发执行,最终标记和筛选回收阶段是垃圾回收线程并行执行。 - 空间整合
与CMS的“标记-清理”不同,G1从整体来看是基于“标记-整理”的,从局部(两个Region之间)来看是基于“复制”算法的。所以G1不会产生垃圾碎片。 - 可预测的停顿
能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
如何查看虚拟机的使用的垃圾收集器 和 配置参数:
https://blog.csdn.net/earthhour/article/details/76468084
参考链接
https://blog.csdn.net/u012882134/article/details/78422258
https://www.cnblogs.com/xiaoxi/p/6486852.html
https://baijiahao.baidu.com/s?id=1565631804713416&wfr=spider&for=pc
网友评论