JVM 的了解:
java 虚拟机是运行 java 字节码的虚拟机。JVM 有针对不同零碎的特定实现,目标是应用雷同的字节码,它们都会给出雷同的后果。(跨平台性)
JVM 的组成
JVM 是虚拟机,是一种标准规范,虚拟机有很多 实现版本。如果没有非凡阐明,都是针对的是 HotSpot 虚拟机。
JDK1.8 之前:
JDK1.8:
上图 JVM 的 组成(3 局部):
- 执行 class 文件时,JVM 底层首先把字节码文件通过 类装载子系统 加载到内存区域。
- 而后应用 字节码执行引擎 去执行内存外面的代码
运行时数据区域
1、虚拟机栈(线程栈)
- 一个办法对应一块栈帧内存区域,寄存这个办法的局部变量。
- 寄存栈帧的就是数据结构里的 栈构造
(先调用的办法先分配内存,后调用的办法后分配内存,后调用的办法是先开释内存的——“先进后出”)
栈里次要寄存这几类数据:局部变量表、操作数栈、动静链接、办法进口
局部变量表
放各种数据类型的变量。如果局部变量是一个 对象 ,它的数据会寄存在 堆里,而局部变量表会寄存这个对象 指向堆的内存地址(建设了援用关系)
操作数栈
例如执行:int a = 1;在 JVM 编译中,先将 int 常量 1 压入 操作数栈 ,让后再取出来 存入 变量a。
操作数栈作用:操作数在程序运行中要做操作,做操作也须要有内存空间暂存一下,这块空间就是操作数栈。
动静链接
这个办法在运行过程中,它的代码在哪。依据办法入口对应的内存地址,能够找到这些代码
这个内存地址就是放在动静链接外面的
办法进口
调用办法执行完后,我要晓得从哪一行代码继续执行。
调用办法的时候,就曾经把过后应该返回的地位,放入了这个办法对应的栈中 办法进口 外面了
- 栈和堆的关系:因为栈会寄存对象的地址,所以栈会有很多 援用指针 指向堆。
- 那么办法 / 函数如何调用?
每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用完结后,都会有一个栈帧被弹出。
2、程序计数器
只有一个线程运行了,JVM 就会在内存区域拿出一个程序计数器调配给它。
-
程序计数器作用:
- 字节码解释器通过改变程序计数器来顺次读取指令,从而实现代码的流程管制
- 在多线程的状况下,程序计数器用于记录以后线程执行的地位,从而当线程被切换回来的时候可能晓得该线程上次运行到哪儿了。
- 因为字节码文件是字节码执行引擎执行的,那么程序计数器当然也是字节码执行引擎 批改 的。每执行一行代码,就棘手把计数器的值批改一下。
吗
3、元空间(办法区)
办法区放的是 间接内存(非运行时数据区的一部分),是线程共享的内存区域。
办法区的组成:常量 + 动态变量 + 类的信息 + 即时编译器编译后的代码等数据
如果动态变量是一个对象,那么办法区和堆的关系和栈堆的关系一样,是指针援用的关系
运行时常量池:
运行时常量池在 JDK6 及之前版本的 JVM 中是办法区的一部分,而在 HotSpot 虚拟机中办法区的实现是永恒代。所以运行时常量池也是在永恒代的。
然而 JDK1.7 及当前的 JVM 将常量池从办法区中移了进去,在 堆中开拓了一块内存寄存字符串常量池。
4、本地办法栈
作用和虚拟机栈施展的作用十分类似,区别是 本地办法栈是为虚拟机应用到的 Native 办法服务的
本地办法被执行的时候,在本地办法栈也会创立一个栈帧,用于寄存该本地办法的局部变量表、操作数栈、动静链接、进口信息。
这是 c 语言 c ++ 有的货色,java 刚出的时候很风行,为了和 c 的老代码进行交互,就是用的这种本地办法接口。
5、堆
JVM 治理的内存中最大的一块,是所有线程共享的。此内存区域的惟一目标是寄存对象实例简直所有的对象实例以及数组都在这里分配内存。
咱们 new 进去的对象会放在 Eden
区,当放不下时,JVM 会 minor gc
收集垃圾。当老年代满时,会执行full gc
- Minor GC:清理年老代空间(包含 Eden 和 Survivor 区域)
- Full GC:清理整个堆空间(包含年老代和永恒代)
-
Minor GC 触发条件
eden 区满时,触发 Minor GC。即申请一个对象时,发现 eden 区不够用,则触发一次 Minor GC。
-
full GC 触发条件
Minor GC 降职空间大小 > 老年代间断残余空间,则触发 full GC。
GC 的过程(使用了可达性剖析算法):
- 从
GC root
登程,找援用着的对象,这一连串的对象会被标记为非垃圾对象 - 把这些非垃圾对象复制到空着的
Survivor
区外面去。 Eden
区里剩下的对象都作为垃圾对象解决掉- Eden 区就空进去,新的对象就能够往里放了
Survivor 区也是会放满被回收的。s0
区满了 复制到 s1
区;而后 Eden 满了会回收 Eden
区和 s1
区;所以一个对象在年老代的流转是从 Eden 区到 s0 区到 s1 区,而后再 s0 和 s1 区之间来来回回。(每次复制到新的区,分代年龄会 +1)
当对象的分代年龄加到 15
后,JVM 会把这种“固执”的对象挪动到老年代。
对象头:组成对象的一类数据。能够看到对象的分代年龄、锁状态等信息。
JVM 调优
JDK 自带一个十分好的调优工具。咱们能够在 cmd 里输出 jvisualvm
应用。这个工具一运行,就会辨认计算机的所有 JVM 过程。咱们能够看 Visual GC 里的参数。
JVM 调优的目标:缩小 GC 的次数,根本原因是缩小 STW 的次数。
- JVM 只有做 GC,都会触发 STW 机制,不过
full gc
的 STW 工夫会比拟长(优先优化 full gc)- STW(stop the world):当执行引擎在进行 gc 时,会让程序暂停。(用户会感觉卡顿了一下,如果 gc 次数比拟多,会造成用户体验差)
- 为什么要设计 STW 机制?让 GC 进行的时候,这些对象的状态不会发生变化。
除了长期存活的对象会进入老年代,其实还有几个机制:
- 大对象间接进入老年代:放入对象大小的参数能够设置
- 对象动静年龄判断:如果以后放对象的 Survivor,一部分对象的总大小大于这块 Survivor 内存的 一半,那么大于这部分对象年龄的对象,能够间接进入老年代
- 老年代空间调配担保机制:当在新生代无奈分配内存的时候,把新生代的对象转移到老生代,而后把新对象放入凌空的新生代
如果有下面的状况,间接放入老年代,那么几分钟就会被放满,就 full GC 了。
罕用 GC 调优策略
策略 1:尽可能将对象调配在新生代,因为 full gc 老本高。适当通过 -Xmn
命令调节 新生代大小,最大限度升高新对象间接进入老年代的状况。
策略 2:大对象如果在新生代可能会呈现空间有余,导致很多小对象被调配到老年代,毁坏新生代的对象构造,可能会呈现频繁的 GC。能够设置间接进入老年代的对象大小。-XX:PretenureSizeThreshold
策略 3:正当设置进入老年代对象的年龄,缩小老年代的内存占用。-XX:MaxTenuringThreshold
策略 4:设置稳固的堆大小。(-Xms
初始化堆大小,-Xmx
最大堆大小)
如果满足这些指标,个别 不须要 进行 GC 优化:
- MinorGC 执行工夫不到 50ms
- Minor 执行不频繁,约 10 秒一次
- FullGC 执行工夫不到 1s
- FullGC 执行不频繁,不低于 10 分钟一次
为什么新生代内存要两个 Survivor 区?
- Survivor 的意义就是缩小被送到老年代的对象,而缩小 Full GC 的产生。
设置两个 Survivor 区最大的益处就是 解决了内存的碎片化。
如果只有一个 Survivor 区,Eden 满了后触发 Minor GC,Eden 和 Survivor 都有存活对象,Eden 的存活对象就会挪动到 Survivor 区,这两局部的内存是不间断的。
内存碎片化会很影响程序的性能,堆空间被分布的对象占据不间断的内存,会导致堆中没有足够大的间断内存空间。
两个 Survivor 区的机制,在整个过程中,永远有一个是 survivor 空间是空的。但如果再细分上来,每一块的空间就会比拟小,容易导致 Survivor 区满,所以两个区是最好的。
年老代的特点
年老代的特点是产生大量的死亡对象,并且要产生间断可用的内存空间,所以应用 复制 - 革除算法 和并行收集器进行垃圾回收。
老年代的特点
每次回收都只回收大量对象,个别采纳 标记 - 整顿算法
另外,标记 - 革除算法收集垃圾的时候会 产生许多内存碎片 (即不间断的内存空间),尔后须要为较大的对象分配内存时, 若无奈找到足够的间断的内存空间,就会提前触发一次 GC 的收集动作。
确定对象是垃圾
- 可达性分析法
根搜索算法,从 GC Root 登程,对象没有援用,就断定为无用对象
- 援用计数法
为每个对象创立一个援用计数,有对象援用时计数器 +1,援用被开释时计数 -1,当计数器为 0 时就能够被回收。
毛病:不能解决循环援用的问题。
1、标记 - 革除(Mark-Sweep)算法
最根底的垃圾回收算法。标记阶段的工作是标记出所有须要被回收的对象,革除阶段就是回收被标记的对象所占用的空间。
问题:容易产生内存碎片
2、复制(Copying)算法
它将可用内存划分为相等大小的两块,每次只应用一块,当这一块的内存用完了,就将存活的对象复制到另外一块上,而后革除垃圾对象。这样就不容易呈现内存碎片的问题。
问题:对内存的应用做出了昂扬的代价,因为可用的内存空间减半。而且如果存活对象很多,Copying 算法的效率将大大降低。
3、标记 - 整顿(Mark-Compact)算法(又称:压缩法)
标记阶段标记出须要回收的对象,而后将存活对象都向一端挪动,而后革除端边界以外的内存。
既解决了内存碎片的问题,又充分利用了内存空间
4、分代收集(Generational Collection)算法
分代收集算法是目前大部分 JVM 的垃圾收集器采纳的算法。个别状况下将堆区划分为 老年代和新生代
目前大部分垃圾收集器对于新生代采取复制算法,老年代个别应用标记 - 整顿算法
CMS 收集器(Concurrent Mark Sweep)
CMS 收集器是一种以获取 最短回收进展工夫 (STW 的工夫)为指标的 并发 收集器。采纳 标记 - 革除 算法实现的。
步骤:
- 初始标记(暂停所有的其余线程,标记一下 GC root 相连的对象,速度很快)
- 并发标记 (开启 GC 和用户线程,用一个闭包去记录 可达对象)
- 从新标记 (为了修改并发标记期间因为程序的持续运行而导致 标记产生变动 的那一部分对象的标记记录)
- 并发革除(开启用户线程,同时 GC 线程对标记的区域革除)
长处:并发收集、低进展
毛病:应用的 标记 - 革除 算法会导致收集完结时容易产生内存碎片
G1 收集器(Garbage-First)
步骤(相似 CMS)
长处:
- 并发:利用多核处理器,缩短 stw 进展工夫。(甚至不必进展)
- 分代收集:保留了分代的概念
- 空间整合:整体采纳标记 - 整顿,部分采纳复制算法
- 可预测进展:建设预测模型,预测进展时长(是绝对于 CMS 的大劣势)
内存溢出(out of memory)
指程序在申请内存时,没有足够的内存空间供其应用。(就是内存不够用)
内存透露(memory leak)
指程序在申请内存后,无奈开释已申请的内存空间。(堆内存对象)不再应用的对象,GC 不能回收
一次的内存透露能够疏忽,如果内存透露重大,会导致内存溢出。
-
起因:长生命周期对象 持有 短生命周期对象援用
只管短生命周期对象曾经不须要了,然而因为长生命周期对象持有它的援用而导致不能回收
例子:
- 单例模式
Instance 可能早已不被应用,
然而类仍持有 Instance 的【援用】。
因而 Intance【生命周期】和利用雷同,造成内存透露。 - 容器
容器内的【键值对】不被应用时,
Map 仍持有 key 对象 & value 对象的【援用】,
则会造成内存透露。
- 单例模式
援用类型(强脆弱虚)
java 执行 GC 判断对象是否存活的其中一种形式是 援用计数法
援用计数:Java 堆中每一个对象都有一个援用计数属性,援用每新增 1 次计数加 1,援用每开释 1 次计数减 1。
从 jdk1.2 开始,对象的援用划分了 4 个级别,使程序更灵便的管制 对象的生命周期
- 强援用:在强援用生效前不会被 GC 回收
(应用最广泛的援用)
- 软援用:在内存不足的状况下,能够被 GC 回收
- 弱援用:只有 GC 线程扫描到该对象,就进行回收
- 虚援用(作用):用来跟踪 GC,回收时发现这个对象有虚援用,会把虚援用退出一个援用队列。用虚援用是否存在,来判断对象是否被回收了。
HotSpot 虚拟机对象探秘
Java 对象的创立过程(五步!!)
1、类加载查看
虚拟机遇到 new 指令时,首先会去查看这个指令的参数是否能在常量池中定位到 这个类的符号援用,并且查看这个符号援用代表的类是否曾经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2、分配内存
在类加载查看通过后,虚拟机将为新生对象 分配内存,所需的内存大小在类加载实现后便可确定。
内存调配的两种形式:
取决于堆内存是否规整,内存是否规整又取决于 GC 收集器的算法是”标记 - 革除“还是”标记 - 整顿“,复制算法内存也是规整的。
1、指针碰撞
实用场合:堆内存规整(没有内存碎片)的状况下
原理:用过的内存全副整合到一边,没有用过的内寄存另一边,两头有个分界值指针,只须要向着没用过的内存方向,将该指针挪动对象内存大小的地位即可
GC 收集器:Serial、ParNew
2、闲暇列表
实用场合:堆内存不规整的状况下
原理:虚构机会保护一个列表,该列表会记录哪些内存块是可用的,在调配的时候,找一块足够大的内存块来划分给对象实例,最初更新列表记录
GC 收集器:CMS
内存调配并发问题:
在创建对象时 线程平安 是很重要的问题,虚拟机采纳两种形式来保障线程平安。
- CAS+ 失败重试
- TLAB:为每一个事后在 Eden 区分配内存,首先在 TLAB 调配,当 TLAB 内存不够时,再采纳上述的 CAS 进行内存调配
3、初始化零值
内存调配实现后,虚拟机要将调配到的内存空间都初始化为零值(不包含对象头)
这一步操作保障了对象的实例字段在 java 代码中能够不赋初始值就能间接应用,拜访到这些字段数据类型所对应的零值。
4、设置对象头
初始化零值实现后,虚拟机要对对象进行必要的设置,这些信息寄存在对象头中。
例如对象是哪个类的实例,对象的哈希码,GC 分代年龄 ………
5、执行 init 办法
执行 new 指令之后会接着执行 <init>
办法,把对象依照程序员的志愿进行初始化,这样一个真正可用的对象才算齐全产生进去。
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局能够分为 3 块区域:对象头 、 实例数据 和对齐填充。
-
对象头
第一局部用于存储对象本身的运行时数据(哈希码、GC 分代年龄、锁状态标记等等)
另一部分是类型指针,即对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 示例数据
是对象真正存储的无效信息
- 对齐填充
不是必然存在的,仅仅起占位作用。Hotspit 要求对象起始地址必须是 8 字节的整数倍。因而,当对象实例数据局部没有对齐时,就须要通过对齐填充来补全。
对象的拜访定位
建设对象就是为了应用对象,咱们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的拜访形式由虚拟机实现而定,目前支流的拜访形式有 应用句柄 和间接指针 两种:
1、句柄:如果应用句柄的话,Java 堆中会划出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址。
2、间接指针:如果应用间接指针拜访,那么 java 堆对象的布局就必须思考如何搁置类型数据的相干信息,而 reference 中存储的间接就是对象的地址
两种对象拜访的形式各有劣势。
- 应用句柄的益处是 reference 中存储的是稳固的句柄地址,在对象被挪动时只会扭转句柄中的实例数据指针,而 reference 自身不必批改
- 应用间接指针的益处是速度快,节俭了一次指针定位的工夫开销
类加载过程
Class 文件须要加载到虚拟机中之后能力运行和应用,那么虚拟机是如何加载这些 Class 文件呢?
零碎加载 Class 类型的文件次要三步:加载 -> 连贯 -> 初始化 。连贯过程又可分为三步: 验证 -> 筹备 -> 解析
加载
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的动态存储构造 转换为 办法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为办法区数据的拜访入口
加载阶段和连贯阶段的局部内容是穿插进行的,加载阶段尚未完结,连贯阶段可能就曾经开始了。
其中,类加载器、双亲委派模型也是十分重要的知识点。
验证
筹备
筹备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在办法区中调配。
- 这时候进行内存调配的仅包含 类变量(static),而 不包含实例变量,实例变量会在对象实例化时随着对象一块调配在 Java 堆中。
- 初始值 ” 通常状况 ” 下是数据类型默认的零值(如 0、0L、null、false 等)
解析
解析阶段是虚拟机将常量池内的 符号援用替换为间接援用 的过程,也就是失去类、字段、办法在内存中的指针或者偏移量。
符号援用:一组符号来形容指标,能够是任何字面量。
间接援用:间接指向指标的指针、绝对偏移量或一个间接定位到指标的句柄。
初始化
初始化是类加载的最初一步,也是真正执行类中定义的 java 程序代码(字节码),初始化阶段是执行类加载器 clinit()
办法的过程。
- 对于初始化阶段,虚拟机严格标准了 5 种状况,必须对类进行初始化
- 当遇到 new、getstatic、putstatic 或 invokestatic 这 4 条间接码指令时
- 应用
java.lang.reflect
包的办法对类进行反射调用时,如果类没初始化,须要触发其初始化- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户须要定义一个要执行的主类 (蕴含 main 办法的那个类),虚构机会先初始化这个类。
- 当应用 JDK1.7 的动静动静语言时,如果一个 MethodHandle 实例的最初解析构造为 REF_getStatic、REF_putStatic、REF_invokeStatic、的办法句柄,并且这个句柄没有初始化,则须要先触发器初始化。