Bootstrap

JVM--内存模型、垃圾回收、常见面试题、JVM调优

目录

一、JVM内存模型

JVM运行时数据区

Java对象的创建过程

Java对象的内存布局

Java对象怎么定位

Java对象怎么分配

二、JVM垃圾回收

如何定位垃圾?

如何清理垃圾?

JVM堆内存分带模型

JVM常见的垃圾回收器

三色标记算法

三、面试问题

CPU突然100%问题排查

内存充裕,为什么会发生FullGC

一个Object占多少个字节

四、JVM调优

JVM参数分类

arthas

如何解决OOM问题


一、JVM内存模型

JVM运行时数据区

方法区:

1:线程共享

2:它是一个逻辑分区:JDK8之前方法区又称永久代,被分配在JVM内存中,JDK8开始方法区被分配到JVM内存区域外的内存区域(MetaSpace),原因是无需指定这块区域的大小

3:它主要存放:类元信息、运行时常量池、静态变量、JIT代码缓存、域信息、方法信息

4:类元信息:类的成员变量信息、类的方法信息、类的常量池信息等等(具体见oop-klass模型)

5:运行时常量池:

  • 每个class文件都有一个常量池,里面存放字符串常量、类名、接口名,字段名和其他一些在class中引用的常量
  • 每一个类被加载后,其中的常量池就会被加载到内存里(方法区运行时常量池)方便运行时调用
  • 每个类都在运行时常量池有一份自己的数据
  • 运行时常量池中有两种类型,分别是symbolic references符号引用和static constants静态常量;String a = "123",其中a就是符号引用,"123"就是静态常量
  • 符号引用可以随着程序的运行进行更新,比如a指向了新的字符串
  • 静态常量可以随着程序的运行不断的添加新的常量;静态常量包含数字常量和字符串常量,其中字符串常量存储只是字符串常量池的引用,字符串常量池JDK8之前存放在方法区JDK8之后存放在堆内

6:方法区位于MetaSpace,MetaSpace的初始大小为21M(-XX:MetaspaceSize),最大大小为系统内存的1/64(-XX:MaxMetaspaceSize)

堆:

1:线程共享;存放实例对象数据、字符串常量池和.class对象(JDK1.8之后)

2:堆内存初始为系统内存的1/64(可通通过-XX:Xms指定)最大为系统内存的1/4(可通通过-XX:Xmx指定)

虚拟机栈:

1:线程独有;每个线程使用虚拟机栈完成线程的代码执行过程

2:每个线程的栈大小,一般情况下为1024K,可以通过-XX:Xss设置

程序计数器:线程独有;存放栈中执行的下一条指令的指令地址

本地方法栈:调用Native方法时使用的栈空间

Java对象的创建过程

  • 步骤一:在运行时常量池中找到Class对象的符号引用,如果找到则直接进入步骤三
  • 步骤二:类加载过程
  1. 加载:从class文件中将类数据加载到内存(Bootstrap-Extension-Application-Custom,双亲委派机制;什么时候使用CustomClassLoader?由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载;也有可能从非标准的来源加载代码,比如从网络来源,此时需要CustomClassLoader进行指定源的类加载)
  2. 链接:验证(保证加载进来的字节流符合虚拟机规范)-准备(类静态变量分配内存并赋默认值)-解析(将常量池内的符号引用替换为直接引用)
  3. 初始化:对类静态变量进行初始化过程
  • 步骤三:为对象分配内存
  • 步骤四:为成员变量赋默认值
  • 步骤五:执行构造函数

Java对象的内存布局

 Mark Word存储了对象的hashCode、GC信息、锁信息三部分

当锁状态为11时,对象处于GC过程中

class Pointer存储了指向方法区内对象类元信息的指针

Java对象怎么定位

1:直接引用(HotSpot虚拟机用的是这种方式)

2:句柄方式引用

 使用直接指针定位方式优点是快,使用句柄池定位方式有点是提升GC效率

Java对象怎么分配

在这里插入图片描述

  1:进入栈区:对对象进行逃逸分析和大对象分析,如果对象不逃逸且不是大对象,则对象被分配进入栈区

逃逸分析:对象不会在方法外被引用

大对象:Eden区分配不下;超过-XX:PretenureSizeThreshold参数(只对Serial和ParNew两种eden区垃圾回收器有效)

2:进入TLAB区:如果TLAB区域足够装下对象则直接进入,如果装不下则根据refill_waste(JVM运行时动态维护的一个变量)会有两种情况:1:请求对象大于refill_waste时将当前TLAB区域剩余的区域用dump object填满然后新开辟一块TLAB区域存放该对象;2:请求对象小于refill_waste时直接将请求对象放入Eden区

TLAB:Thread Local Allocation Buffer;多个对象被分配到堆上时会出现指针碰撞引起冲突,此时便出现了TLAB(也可以用CAS来解决指针碰撞问题),每个线程被创建出来之后JVM会在Eden区开放一块儿内存(约占Eden区的1%)作为该线程独有的内存区域

3:进入Eden区:1里已经进行过大对象分析,所以TLAB进不去对象就直接进入Eden区了

二、JVM垃圾回收

如何定位垃圾?

根可达算法:从根元素(栈里引用的对象、本地方法栈引用的对象、常量池内对象、静态变量引用的对象、.class对象)开始寻找,凡是通过根元素可以搜索到的对象,都不是垃圾

如何清理垃圾?

  • 标记-清除算法:找到垃圾,然后标记垃圾,然后清除垃圾;缺点:位置不连续,会出现内存碎片
  • 拷贝算法:开辟相同大小的内存空间,将不是垃圾的对象拷贝过去,拷贝完成后将旧的内存地址正片删除;缺点:虽然解决了内存碎片问题但太浪费内存
  • 标记-压缩算法:先标记垃圾,然后清除垃圾的同时,将后面的非垃圾对象往前移动整理,最后将清理后的内存地址回收;缺点:算法效率低

JVM堆内存分带模型

1:传统垃圾回收器使用的模型(例如G1垃圾回收器就不适用新生代+老年代的模型)

2:新生代+老年代+永久代(JDK1.7)/元数据区(JDK1.8)

        永久代和元数据区都是装.class对象

        永久代必须指定大小限制,元数据区可以不指定大小限制

        字符串常量存放在永久代(JDK1.7)/字符串常量存放在堆里(JDK1.8)

3:新生代=eden区+2个survivor区域(8:1:1)

        1:YoungGC后,回收eden的大部分对象,eden区活着的对象 -> survivor0区
        2:再次YoungGC后,eden区域的对象+survivor0区的对象 -> survivor1区
        3:再次YoungGC后,eden区域的对象+survivor1区的对象 -> survivor0区

        4:每次YoungGC回收都会给对象增加年龄,年龄到了直接进入老年代

        5:每次往survivor区拷贝对象时,如果survivor区内存不够了,则对象直接进入老年代

        6:这种GC的模式比较适合使用拷贝算法

4:老年代

        老年代满了,则进行FullGC(新生代和老年代进行一次整体GC,采用压缩算法)

5:占用内存

  • JVM的堆内存初始为系统内存的1/64(可通通过-XX:Xms指定)最大为系统内存的1/4(可通通过-XX:Xmx指定)
  • 新生代和老年代的大小比例默认比例为1:2,可以通过–XX:NewRatio指定
  • 新生代里的Eden和Survivor区默认比例为8:1:1,可以通过–XX:SurvivorRatio指定(同时需要关闭-XX:UseAdaptiveSizePolicy)

MinorGC和FullGC的触发时机

MinorGC触发时机:Eden 区没有足够的空间分配给新创建的对象

FullGC触发时机:老年代空间不足、方法区空间不足、MinorGC后需要进入老年代的对象大于老年代的剩余空间、MinorGC前历史平均每次MinorGC进入老年代的对象大小大于老年代的剩余空间

JVM常见的垃圾回收器

1.Serial:应用于新生代,串行回收器

2.Parallel Scavenge:应用于新生代,并行回收器 

3.ParNew:应用于新生代,并行回收器 ,因为Parallel Scavenge无法和CMS配合使用,因而产生

4.Serial Old:就是将Serial算法应用于老年代

5.Parallel Old:就是将Parallel Scavenge算法应用于老年代

6.CMS:应用于老年代,并发,与应用程序并行运行,降低了STW时间,复杂

7.G1(10ms):未深究,视频有1小时40分钟专门讲G1

8.ZGC(1ms):使用三色标记法

9.Shenadoah

10.Eplison

JDK1.8默认的垃圾回收器是2+5

三色标记算法

回收对象时对对象进行三色标记,黑色、灰色、白色
存在浮动垃圾问题,CMS和G1的解决方案不同

三、面试问题

CPU突然100%问题排查

原因:

1:cpu的内核上下文切换频率过高。上下文切换需要经历保存运行线程的执行状态、让处于等待中的线程恢复执行这2个过程,这2个过程需要CPU执行内核指令,所以频繁的切换会占据大量的CPU资源;在java中文件IO、网络IO、锁等待都会促使CPU执行上下文切换
2:CPU资源过度消耗。过多的线程和执行时间较长的业务代码,都会导致CPU资源的消耗

排查步骤:

第一步:top 找出哪个进程占用cpu

第二步:top -Hp 进程号 找出这个进程的哪个线程占用cpu

第三步:printf "%x\n" tid将线程id转换为16进制,jstack进程号 |grep 线程号 -A 30,查看堆栈信息进行分析

分析:

情况1:占用CPU的线程总是同一个

jstack查看程序堆栈,分析是哪块儿业务逻辑在消耗CPU资源

情况2:占用CPU的线程总是不断变化

此时需要挑选出几个线程进行逐个分析

内存充裕,为什么会发生FullGC

有大对象;连续空间不够;方法区或MetaSpace满了;手动调用System.gc();

一个Object占多少个字节

内存小于32G:MarkWord占8个,klass pointer占4个,对象实例数据0个,padding占4个,一共16

内存大于32G:MarkWord占8个,klass pointer占8个,对象实例数据0个,padding占0个,一共16

四、JVM调优

JVM参数分类

标准:-开头,所有hotspot版本都支持

非标准:-X开头,不是所有hotspot版本都支持 查看 java -X

不稳定:-XX开头,有可能下个版本会移除 查看 java -XX:+PrintCommandLineFlags


java -XX:+PrintFlagsInitial  --查看出厂默认值

java -XX:+PrintFlagsFinal  -version --查看修改更新

java -XX:+PrintFlagsFinal -version |grep HeapSize --查找指定参数配置(:=为被更新过的值)

java -XX:+PrintCommandLineFlags -version  --打印命令行参数(可以看默认垃圾回收器)

arthas

dashboard:展示所有线程的cpu和mem占用,展示GC情况

thread:打印线程详细信息
trace:跟踪类或者函数的所有调用并记录时间

jad:反编译class或者method代码

redefine:在线上修改类后,javac生成class文件,redefine重新加载class文件,redefine不能给类添加新的field或method

tt:追踪某个method的详细信息,入参 返回值等等

如何解决OOM问题

方法1:

设置JVM参数,-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/tomcat,让程序发生OOM时自动生成文件


方法2:当很久才会发生一次OOM,不可能等到OOM才分析时

使用jmap -histo [进程号]命令直接分析内存中哪些实例有可能会导致OOM,不推荐该方法,因为jmap本身非常占用内存

方法3:当使用jmap -histo [进程号]需要看的东西太多时

使用jmap -dump:live,format=b,file=[文件名].hprof [进程号]生成hprof文件,然后用MAT或VisualVM分析文件


 

;