JVM深度解析

JVM内存管理深度剖析

JVM-java虚拟机

JVM --- Java Virtual Machine --- java虚拟机
所谓的java虚拟机,其实就是翻译软件,将.class.jar等文件,翻译成各个操做系统所能识别的机器语言,JVM是跨平台的,甚至是跨语言的,因此这就是Java的魅力。java

image.png

JVM、JRE、JDK

JDK:Java工具包
JRE:(Java Runtime Environment)Java 的运行时环境,也就是JVM+一堆基础类库,JRE提供的不少类库
JVM:翻译,把Class翻译成机器语言
因此 JDK>JRE>JVM面试

JVM 的结构

JVM运行过程
image.png算法

运行时数据区是重点!!!
运行时数据区又分红两部分:线程之间共享的(方法区、堆),线程私有的(虚拟机栈、程序计数器、本地方法栈)
image.png数组

程序计数器

用来记录当前线程的每一行代码所执行的字节码地址、操做等。是字节码的行号指示器。(本行对应的操做,内存物理地址,线程恢复等)
缘由:cpu的时间片轮转,由于时间片轮转,执行到一半停了,切其余线程了,因此得有一个记录当前执行到哪行,啥操做的记录器。
程序计数器很小,因此不会OOM缓存

虚拟机栈

常说的堆栈,其中的栈指的就是JVM的虚拟机栈。
栈是先进后出的结构(LIFO),类比于弹夹。安全

一个栈(虚拟机栈)相似于弹夹,都是先进后出的,每一个栈帧就是子弹(1个方法=1个栈帧),栈帧是一个一个压入栈的(子弹压入弹夹),而后一个一个射出。
image.pngmarkdown

栈帧的结构
image.png多线程

大小限制 -Xss
栈的大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k,若是超过限制就会OOM。栈放的栈帧是有大小的(弹夹有大小,放的子弹有限制)并发

局部变量表
存储局部变量,存储八大基本类型和对象的引用,Object对象会存放堆里,引用会存这。一个32位,64位的类型会一个占两个位置。工具

操做数栈
存放咱们方法执行的操做数的。刚开始栈是空的,有操做的时候会频繁入栈出栈进行计算。

动态链接
用来记录多态具体执行哪一个方法的,如 People wang = new Man(); wang.wc(); 在运行时动态链接到 wang.男厕所

返回地址
返回返回值的地址(程序计数器记录的物理地址),异常的时候不走这个,走异常处理器表

执行下面操做的流程:
image.png

  1. 局部变量表(第一个通常都是this,除了静态这样的)会声明两变量,x和y用1,2存储
  2. x=1压入操做数栈
  3. y=2压入操做数栈(由于是栈 先进后出,因此x=1原本在栈顶1号位,会被压到下面位置(相似子弹))
  4. 将xy都移出栈,去cpu执行 1+2操做,返回3放入操做数栈
  5. 将操做数栈里的3返回值取出来,赋值给局部变量表中的y=2

本地方法栈

功能相似于虚拟机栈,是native 方法,交给C来执行。

方法区

(永久代,在HotSpot 虚拟机中使用永久代来实现方法区,可是其余虚拟机并非。因此方法区≠永久代)

方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池 (String会存到堆里面,引用会存在常量池里)

JVM 在加载类时,会先加载.class文件。
.class文件包含类的版本、字段、方法和接口等描述+常量池
常量池用于存放编译期间生成的各类字面量和符号引用
字面量=String+final常量。符号引用=类、方法、字段的全名和描述符。

而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。 例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类以后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。

方法区与堆空间相似,也是一个共享内存区,因此方法区是线程共享的。 假如两个线程都试图访问方法区中的同一个类信息,而这个类尚未装入 JVM,那么此时就只容许一个线程去加载它,另外一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其他部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了以前的永久代,而且元空间的存储位置是本地

配置元空间大小参数:
dk1.7及之前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8之后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8之后大小就只受本机总内存的限制(若是不设置参数的话)

Java8 为何使用元空间替代永久代,这样作有什么好处呢?
1.两公司合并,为了融合
2.永久代内存常常不够用或发生内存溢出

堆是 JVM 上最大的内存区域,这里存储了 几乎全部对象+数组。垃圾回收,操做的对象就是堆。

程序启动就会申请堆空间,随着堆增大,会进行GC(垃圾回收)。
对象new出来就是在堆上建立出来的。基本数据类型:方法体内声明的(局部变量)存在栈里面,否则就是存在堆上。

配置堆的大小:
-Xmx:堆的最大值;
-Xms:堆的最小值(初始值);

堆分为新生代(容易被GC回收)和老年代(不容易被GC回收),物理地址是连续的。一个对象建立出来的时候在新生代,若是通过好几回GC都还活着,那就会移到老年代。 image.png

直接内存

不属于JVM运行时数据区的。可是使用NIO就能够在内存里申请一块区域供JVM使用。也会发生OOM
配置的大小:
-XX:MaxDirectMemorySize

HSDB工具

可使用HSDB工具来查看JVM运行时的状况,用线程id去那个工具搜索。

内存溢出

JVM各个部分除了程序计数器之外全部部分均可能发生内存溢出。

  • 栈溢出

1.每一个栈都有最小固定大小的,1M。无限建线程,就会无限建栈,而后机器内存不足就会OOM。
2.栈的栈帧是有大小上限的(弹夹子弹有上限)。栈帧=方法,因此无限执行方法,会java.lang.StackOverflowError。

image.png image.png

  • 堆溢出

堆上对象太多太大,致使内存溢出,Android大部分都是堆溢出。
1.代码正常能够经过 调大堆大小。 -Xms,-Xmx参数。
2.大部分都是内存泄漏致使的,就应该检查代码了(异常持有引用等)。
3.检查调整对象,是否不合理的设计,持有时间太长,对象太大手动清理一部分,对象生命周期太长。

  • 方法区溢出

(1) 运行时常量池溢出
(2)方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了咱们配置。

  • 本机直接内存溢出

申请更大空间,或者检查代码。

虚拟机优化技术

1. 方法内联:将简单的方法直接不调用,放调用的地方复制一份执行(调用方法会在栈里多一个栈帧,而后频繁的入栈出栈)

image.png

2. 栈帧之间数据的共享(虚拟机已经优化好的)
栈帧之间会共用一些数据,这时不会建立多分,会共用一份。如a(10); 10这个变量传递在两方法(栈帧)上,只有一份。

image.png

对象与垃圾回收机制

对象的建立

对象建立过程图,很是重要
别的步骤看图,分配内存的时候会根据内存是否工整采用不一样的方式分配内存,分别是指针碰撞和空闲列表。
可是由于堆是多线程共享的,因此会引起线程安全问题,因此有两种解决方案CAS机制和本地线程分配缓冲TLABimage.png

指针碰撞
image.png

空闲列表
image.png

CAS机制
CAS机制compare and swap比较而且交换,先找到空闲的区域,而后利用cpu的CAS指令,若是这块内存无人占据,那我就写入,若是在执行CPU CAS指令时,已经被别人占据了(比较compare之后跟我想要的不一样)那就继续循环找下一块区域
详情见线程 juejin.cn/post/695650…

本地线程分配缓冲TLAB
由于线程很少,并且所需的内存较小通常,因此 去 堆里的新生代Eden区直接给每一个线程划一块内存,供其使用。
这块区域很小,通常占用Eden区的1%,2% image.png

对象的构成

对象分为:对象头、实例数据、对齐填充
image.png

对象的访问定位

访问对象主流的有两种:句柄(堆里划出一个句柄池,用句柄池再来管理)和直接指针 image.png

判断对象的存活

判断对象是否存活,是否须要被GC回收,如今通常有两种方式:引用计数算法和可达性分析(根可达)
image.png

可达性分析
GC Roots的对象(系统定义好的,堆之外的指针):
● 虚拟机栈(栈帧中的本地变量表)中引用的对象。
● 方法区中类静态属性引用的对象。
● 方法区中常量引用的对象。
● 本地方法栈中JNI(即通常说的Native方法)引用的对象。
● VM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。
● 全部被同步锁(synchronized关键)持有的对象。
● JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
● JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代时)

各类引用

强>软>弱>虚 image.png

对象的分配策略-对象建立的完整流程

  1. 有些对象是在栈上分配的,只要知足逃逸分析的小对象,就能在栈上分配(也就是方法里的局部变量,别的方法、线程没引用到)

image.png

好处:没必要在堆上分配,速度快。栈是方法执行完了,里面的内存就释放了 不用GC
image.png

  1. 本地线程分配缓冲(TLAB)

image.png

  1. 是不是大对象,大对象直接分配到老年代(大的String,数组)。缘由是老年代不用频繁GC、移动。并且老年代空间大。新生代:老年代=1/3 : 2/3

image.png

4.通常对象new出来都是在新生代的Eden(伊甸园)区。而后被频繁GC活下来之后晋级到老年代。
1.对象出生在Eden区,对象头上存储的GC年龄age为空(0岁)
2.第一次GC之后,会有90%的对象都被GC回收掉,剩下的10%会晋级到from区,这时age=1
3.第二次GC若是仍是活下来会晋级到To区,age=2,第三次第四次...会反复在from区和to区跳而后年龄age++,直到age到达一个临界值15(或者本身设置或者根据不一样算法得出)
4.年龄age到达临界值15,或者由于空间分配担保会晋级到老年代Tenured区
image.png

image.png

image.png

image.png

对象的回收

对象的生讲完了,如今讲对象的死。也就是GC。

分代收集理论

GC垃圾回收器,在新生代和老年代的回收算法是不同的。新生代里用的是复制算法。老年代用的是标记清除算法和标记整理算法
空间大小:新生代:老年代 = 1:2 。新生代里 Eden:from:to = 8:1:1 image.png

复制算法(Copying)

image.png

标记-清除算法(Mark-Sweep)

优势:不要整理,快,对象不须要移动
缺点:内存碎片 image.png

标记-整理算法(Mark-Compact)

image.png

JVM中常见的垃圾收集器

一代目:单线程。Serial--Serial Old---复制算法、标记整理算法
二代目:多线程并行。Parallel Scavenge--Parallel Old--复制算法、标记整理算法。就从单线程改为多线程没啥区别
三代目:多线程并发。ParNew--CMS---复制算法、标记清除算法

image.png

单线程与多线程并行

一代目、二代目 image.png

CMS垃圾回收器

Android采用的垃圾回收器
CMS将GC分红好几个阶段:
1.先单独执行初始标记(标记可达性分析的第一层)---执行速度快
2.并发标记 可达性分析根之外的叶子节点 --- 时间长,因此跟用户线程并发执行
3.从新标记 在执行期间新new出来的对象
4.并发清理并重置线程 iii.png
image.png

垃圾回收器暂停用户线程,而后再去执行GC的现象叫Stop The World

G1垃圾回收器

image.png

总结与面试

常量池与String

image.png

JVM内存结构说一下!

image.png image.png

什么状况下内存栈溢出?

java.lang.StackOverflowError 若是出现了可能会是无限递归。
OutOfMemoryError:不断创建线程,JVM申请栈内存,机器没有足够的内存。

描述new一个对象的流程!

image.png

Java对象会不会分配在栈中?

能够,若是这个对象不知足逃逸分析,那么虚拟机在特定的状况下会走栈上分配。

若是判断一个对象是否被回收,有哪些算法,实际虚拟机使用得最多的是什么?

引用计数法和根可达性分析两种,用得最可能是根可达性分析。
image.png

GC收集算法有哪些?他们的特色是什么?

复制、标记清除、标记整理。复制速度快,可是要浪费空间,不会内存碎片。标记清除空间利用率高,可是有内存碎片。标记整理算法没有内存碎片,可是要移动对象,性能较低。三种算法各有所长,各有所短。

JVM中一次完整的GC流程是怎样的?对象如何晋级到老年代?

对象优先在新生代区中分配,若没有足够空间,Minor GC;
大对象(须要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。
若是对象在新生代出生并通过第一次MGC后仍然存活,年龄+1,若年龄超过必定限制(15),则被晋升到老年态。

Java中的几种引用关系,他们的区别是什么?

image.png

final、finally、finalize的区别

在java中,final能够用来修饰类,方法和变量(成员变量或局部变量)
当用final修饰类的时,代表该类不能被其余类所继承。当咱们须要让一个类永远不被继承,此时就能够用final修饰,但要注意:
final类中全部的成员方法都会隐式的定义为final方法。
使用final方法的缘由主要有两个:
(1) 把方法锁定,以防止继承类对其进行更改。
(2) 效率,在早期的java版本中,会将final方法转为内嵌调用。但若方法过于庞大,可能在性能上不会有多大提高。所以在最近版本中,不须要final方法进行这些优化了。
final成员变量表示常量,只能被赋值一次,赋值后其值再也不改变。

finally做为异常处理的一部分,它只能用在try/catch语句中,而且附带一个语句块,表示这段语句最终必定会被执行(无论有没有抛出异常),常常被用在须要释放资源的状况下

Object中的Finalize方法 即便经过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,须要通过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(若是对象覆盖了finalize),咱们能够在finalize中去拯救。 因此建议你们尽可能不要使用finalize,由于这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议你们忘了finalize方法!由于在finalize方法能作的工做,java中有更好的,好比try-finally或者其余方式能够作得更好

相关文章
相关标签/搜索