JVM 内存模型 、对象分配流程、 垃圾收集器

 JVM参数调优利器 —— XXFox    HeapDump - Java虚拟机参数分析 

自动获取本地进程分析优化-XPocket   PerfMa 笨马网络 网站性能优化,高并发模拟,影子库,常用的生产环境,全链路,接口,服务器性能测试/监测-网络性能分析

1. Java 类加载机制

https://www.processon.com/view/link/5e69db12e4b055496ae4a673#map 儒猿群分享JVM知识点

jvm | ProcessOn免费在线作图,在线流程图,在线思维导图 |  JVM图

类加载器 加载顺序

破坏双亲委派机制:

  (1) tomcat      (2) SPI        (3) OSGi  以上3个会是破坏双亲委派机制的



2. JVM 内存模型

Method Area(方法区)

  • 方法区是各个线程共享的内存区域,在虚拟机启动时创建   .   别名叫做Non-Heap(
    ),目的是与Java堆区分开来
     
  • 用于存储已被虚拟机加载的    类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 当方法区无法满足内存分配需求时,将抛出  OutOfMemoryError  异常

常量池】主要存储两方面内容:字面量(Literal)和符号应用(Symbolic References)

  • 字面量 : 文本字符串, final 修饰等
  • 符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符

javap -v -p Person.class 进行反编译,查看字节码信息和指令等信息

JDK1.8之后,这块区域叫 Metaspace ,可以认为是 “元数据空间” 的意思 。方法区与堆、原空间关系:方法区中的 静态变量和字符串常量池是放在 堆中的


程序计数器

那么在执行字节码指令的时候,JVM里就需要一个特殊的内存区域了,那就是“程序计数器” 。

这个程序计数器就是用来 记录当前执行的字节码指令的位置的 ,也就是记录目前执行到了哪一条字节码指令。

JVM是支持多个线程的。所以就会有多个线程来并发的执行不同的代码指令 。
因此每个线程都会有自己的一个程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了。如下图


Heap 堆

  • 堆是存储Java对象的地方。被所有线程共享 ,存储Java对象实例以及数组
  • 堆也是java垃圾收集器管理的主要区域
  • 堆内存结构还可以分块成: 老年代(old),年轻代(Young) . Young:Old 为 1:2
ReplicaManager replicManager = new ReplicManager()

 局部变量表里的  “replicaManager”  指向了Java堆内存里的 ReplicaManager 对象

静态变量 在方法区 ,其实例也是在Java堆内存中的
public class Test {
    private static User user = new User();
}
静态变量 user 引用了 User对象,这是长期驻留在内存里的。
但是哪怕是这种对象,其实刚开始你通过“new User()”代码来实例化一个对象时,他也是分配在新生代里的

 年轻代(Young)区 

    分为:有Eden区、两个 Survivor区;(From Survivor空间(s0)、To Survivor空间(s1)),比例:8:1:1

  • 所有新创建的对象都在Eden区,当Eden区满后,出发minorGc将Eden区仍然存活的对象复制到其中一个Survivor区,另一个Survivor区中存活的对象也复制到这个Survivor区中,以保证始终有一个Survivor区是空的(两个Survivor 是交替的 一会from 一会 to)
  • 使用标记整理算法。

老年代(old) 与  年轻代 比例:


Java Virtual Machine Stacks(虚拟机栈)

  • 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个 Java 线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创
  • 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
  • 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
JVM必须有一块区域是来保存每个方法内的 局部变量 等数据的,这个区域就是 Java虚拟机栈 。 每个线程都有自己的Java虚拟机栈

图解栈和栈帧

出栈和入栈顺序如下 :  a 调用 b , b 调用 c
void a(){
 b(); 
}
void b(){ 
    c();
 }
void c()
{ }

出栈和入栈顺序 : 先进后出,后进先出

随着一些方法执行完毕(栈帧出栈了),大部分新生代里的对象就没有人引用了,就成了垃圾对象.

栈帧

栈帧 : 每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括      
  • 局部变量表(Local Variables)
    • 方法中定义的局部变量以及方法的参数存放在这张表中

      局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。

  • 操作数栈(Operand Stack)
    • 操作数栈:以压栈和出栈的方式存储操作数的
  • 指向运行时常量池的引用(A reference to the run-time constant pool)
    • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调
      用过程中的动态连接 (Dynamic Linking)
  • 方法返回地址(Return Address) 和 附加信息。
    • 方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇
      见异常,并且这个异常没有在方法体内得到处理
栈 和 栈帧 内部关系
    栈中包含多个栈帧,  栈帧中包含有   局部变量表、操作数栈、动态链接、方法返回地址


3. 一个对象创建流程

第一步 :栈上分配 如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,这样,对象所占用的内存空间就可以随栈帧出栈而销毁。如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载

栈上分配的技术基础:
一是逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体
二是标量替换:允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。

第二步 :TLAB分配 (Thread Local Allocation Buffer)即线程本地分配缓存区,这是一个线程专用的内存分配区域。TLAB是在堆上 Eden区的 。名字上看是一个线程专用的内存分配区域,是为了加速对象分配而生的,每一个线程都会产生一个TLAB区域,该线程独享工作区域,Java虚拟机使用这种TLAB区域来避免多线程冲突问题,提高了对象分配的效率。TLAB空间一般不会太大,当大对象无法再TLAB分配时,则会直接分配到堆上 。Tlab 分配独享,使用共享(堆是线程共享的)

-XX:+TLABSize   设置TLAB 大小  TLAB区域大小是Eden区域的1%

-XX:+TLABRefillWasteFraction      设置维护进入TLAB空间的单个对象大小,它是一个比例值,默认为64。即如果对象大于整个空间的1/64,则在堆创建对象

第三步 :是否满足进入老年代

-XX:PretenureSizeThreshold=1000   单位是字节。1000byte 设置新生代中的对象大小超过指定大小后,直接晋升老年代。

第四步 :新生代分配


 4. JVM内存参数

在JVM内存分配中,有几个参数是比较核心的,如下所示。

  1. -Xms:Java堆内存的大小

  2. -Xmx:Java堆内存的最大大小

  3. -Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了

  4. -XX:PermSize:永久代大小

  5. -XX:MaxPermSize:永久代最大大小

  6. -Xss:每个线程的栈内存大小

-Xms和-Xmx,分别用于设置Java堆内存的刚开始的大小,以及允许扩张到的最大大小。通常来说,都会设置为完全一样的大小

-XX:PermSize和-XX:MaxPermSize,分别限定了永久代大小和永久代的最大大小。通常这两个数值也是设置为一样的

JDK 1.8以后的版本,那么这俩参数被替换为了-XX:MetaspaceSize和-XX:MaxMetaspaceSize


5. 垃圾收集器

1. serial  

单线程收集器,   简单高效,    收集过程会有 STW,  用于新生代。   使用复制算法

2. serial  Old

serial   的老年代版本:  单线程收集器,   简单高效,    收集过程会有 STW,  用于老生代。   使用标记-整理算法

3. ParNew

多线程版本,  多核, 收集过程会有 STW,但是停顿时间减少,吞吐量更大 。

-XX:+UseParNewGC 新生代使用 ParNew

业务代码时间 / (业务代码时间+垃圾收集时间) = 吞吐率   99s / 99s + 1s(垃圾收集时间) = 99%

4. Parallel Scavenge

新生代垃圾收集器   使用复制算法

5. Parallel Old  

Parallel Scavenge的老年代版本:  更加关注吞吐率。  使用标记整理算法

 -- 能否实现 业务代码线程与垃圾收集线程 同时进行呢 ?  >   分治算法

分治算法, 把垃圾收集线程分为多步骤。细化每一个步骤    > 


5.1 CMS 垃圾收集器

CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:
1、初始标记(CMS initial mark)。
2、并发标记(CMS concurrent mark)。
3、重新标记(CMS remark)。
4、并发清除(CMS concurrent sweep)。

CMS 垃圾收集器收集完整步骤:

  • Phase1 :Initial Mark【初始标记】
  • Phase2 : Concurrent Mark 【并发标记】
  • Phase3 : Concurrent Preclean【并发预先清除】
  • Phase4 : Concurrent Abortable Preclean【并发可能失败的预先清除】
  • Phase5 : Final Remark【最终重新标记】
  • Phase6 : Concurrent Sweep【并发清除】
  • Phase7 : Concurrent Reset【并发重置】

-XX:+UseConcMarkSweepGC, 这个参数表示对于老年代的回收采用CMS

CMS 垃圾收优点:

1.并发收集(concurrent)
2.低停顿

CMS是基于“标记-清除” 那么就会有空间碎片

CMS 垃圾收集器  :

      缺点:1. 并发标记过程,抢占CPU资源。不是4核以上,不如 serial Old。因为会抢Cpu,

                 2. 基于“标记-清除” 那么就会有空间碎片

                 3.  并发模式失败。 并发收集器在老年代填满之前,完成对象回收, 或者  空闲内存不能满足一个内存的分配要求, 会强行 STw 。 并发模式失败 ,会  serial  Old  作为备选方案


5.2    G1  收集器 

让CMS停顿时间更短, 短到开发者自己设置, 某种程度上可以解决空间碎片 ,  4核8G,最低配置要求使用G1

G1 先回收优先级高的,剩下来不及回收的,下次再来回收。

G1 把内存划分为大小相等的 Region 。把大对象(超过 Regiony一般大小)放在 Humongous 中

JDK11出现的垃圾收集器   ZGC   10ms内的停顿时间。

串行收集器: serial  、 serial Old

并行收集器: ParNew 、Parallel Scavenge 、Parallel Old

并发收集器(垃圾收集线程和业务线程同时运行):CMS 、  G1

G1收集器使用要求

1. 堆内存6GB是最小要求,会卡

2. 50%以上堆内存都被存活对象占用

3. 对象的分配,  垃圾收集频率高,时间长(0.5-1s)


6   JDK 参数

标准参数:不随JDK版本变化而变化的参数

非标准参数

  • -X  :  随JDK版本变化而变化的参数(用处不是特别大)
  • -XX
    • boolean 类型     -XX:[ +/- ]value    。比如 -XX:UserG1GC    -XX:+UserSerialGC     value前面的+代表启用,- 就是禁用,不使用。  启用:-XX:+UserSerialGC            禁用 :-XX:-UserSerialGC
    • K-V 类型     name = value 。比如  -XX:initialHeapSize=100M 初始化堆内存100M  ,  -XX:MaxHeapSize = 100M

其它参数:

比如     -Xms100M    其实等价于    -XX:initialHeapSize=100M       这是简写

             -Xmx100M    等价于              -XX:MaxHeapSize = 100M

             -Xxx100     等价于              -XX:ThreadStackSize = 100M    栈深度

7 IDEA 查看 Java 的默认参数 

IDEA 查看 Java 的默认参数  java -XX:+PrintFlagsFinal -version

输出到文件夹: java -XX:+PrintFlagsFinal -version > 1.txt

在IDEA 中设置启动是打印JVM参数:参数单位是 byte 字节

打印如下:

' = ' 前面的冒号 ,代表JVM启动的时候修改过,最右边一列,{manageable} 代表可以 JVM启动的时候实时修改参数值。{product} 代表不可以实时修改JVM参数 。

JVM 调试

-XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=/usr/local/app/oom(这个目录自己写一个)   输出 dump文件


JVM 垃圾收集器:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM内存模型是指Java虚拟机在运行时对内存的组织和管理方式。它定义了JVM内存的不同区域以及各个区域的作用和特点。 JVM内存模型可以分为以下几个部分: 1. 程序计数器(Program Counter Register):每个线程都有自己的程序计数器,用于记录当前线程执行的字节码指令的地址。 2. Java虚拟机栈(Java Virtual Machine Stacks):每个线程在执行Java方法时会创建一个对应的栈帧,栈帧用于存储方法的局部变量、操作数栈、方法返回值等信息。 3. 堆(Heap):堆是JVM中最大的一块内存区域,被所有线程共享。它用于存储对象实例和数组。堆内存由垃圾回收器自动管理,负责对象分配和释放。 4. 方法区(Method Area):方法区用于存储已加载类的信息、静态变量、常量、即时编译器编译后的代码等。在JDK 8及以后的版本中,方法区被元空间(Metaspace)所取代。 5. 运行时常量池(Runtime Constant Pool):每个类或接口在编译后都会生成一个运行时常量池,用于存放编译器生成的字面量和符号引用。 6. 本地方法栈(Native Method Stacks):本地方法栈用于执行本地方法(Native Method)的栈。 7. 直接内存(Direct Memory):直接内存不是JVM管理的堆内存,而是通过操作系统本地IO直接分配的内存。一般在使用NIO(New Input/Output)时会使用到直接内存。 这些内存区域共同组成了JVM内存模型,对于Java程序的运行和性能有着重要的影响。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值