JVM 的作 用是从软件层面屏蔽不同操作系统在底层硬件和指令的不同,同时JVM是一个 虚拟化的操作系统,类似于Linux或者Windows的操作系统,只是它架在操作系统上,接收字节码也就是把字节码翻译成操作系统上的机器码且 进行执行。
特性:跨平台、跨语言
jvm实现:不同公司有不同规范,java -version 可以看到是依据HotSpot
JVM是一个虚拟化的操作系统,所以它要虚拟处理器(执行引擎), 虚拟操作系统指令(基于操作数栈的指令集),虚拟操作系统内存(jvm内存区域),等等
.class文件指令到jvm执行引擎(面向jvm的指令),然后到cpu(面向cpu的指令)
JVM虚拟内存的结构划分:
分线程共享和线程私有两类:
每个方法都有代表其的栈帧进入虚拟机栈,先进来的后出去,会有程序计数器来记录先进来的(上一个方法)运行到哪儿了
栈帧包含四部分:局部变量表、操作数栈、动态连接、方法出口
每个线程都有对应的虚拟机栈,大小缺省为 1M,可用参数调整大小
堆栈溢出:栈帧深度压栈但又不出栈,导致栈空间不足,例如递归调用没有一个好的出口自己调自己
局部变量表:主要放的就是方法内的变量,一般为32位,若是64位就是用高低位占用两个来存放,若局部变量是对象就存放引用地址(4位)
操作数栈:主要用于保存计算过程的中间结果;本质上是JVM执行引擎的一个工作区;为了实现java的跨平台选择了面向操作数栈的指令集架构;
动态链接:java语言特性多态
完成出口:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)
1、本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点
2、不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机 栈服务的是JVM执行的java方法
3、虚拟机规范里对这块所用的语言、数据结构、没有强制规定,虚拟机可 以自由实现它
4、hotspot把它和虚拟机栈合并成了1个 5、和虚拟机栈一样,大小有限
较小的内存空间,存储当前线程执行的字节码的偏移量;各线程之间独立存储,互不影响
方法区(Method Area)是可供各线程共享的运行时内存区域,主要用来 存储已被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码缓存 等等,它有个别名叫做:非堆(non-heap),主要是为了和堆区分开。
方法区存储的信息分为两类:
1、类信息:主要指类相关的版本、字段、方法、接口描述、引用等
2、运行时常量池:编译阶段生成的常量与符号引用、运行时加入的动态变 量
在堆里划分了一 块来实现方法区的功能,叫永久代。因为这样可以借助堆的垃圾回收来管理方 法区的内存,而不用单独为方法区再去编写内存管理程序。
-XX:PermSize:初始大小;-XX:MaxPermSize:最大值
方法区中实现的永久代去掉了,用元空间( class matedata space)代替了之前的永久代,元空间的存储位置是:本地内存/直接内存,并且将方法区大部分迁移到了元空间
-XX:MatespaceSize:元空间初始大小;-XX:MaxMatespaceSize:可从本地内存为元空间分配出的最大值,默认是没有限制的
为什么要代替呢?
为了融合 HotSpot JVM 与 JRockit VM ,JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出
为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素;而元空间大小就只受本机总内存的限制因为使用的是本地内存
常量池可分两类:
1、Class常量池(静态常量池),在 .class 文件中有类的版本、字段、方法和接口 等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用,之所以说它是静态的常量池是因为这些 都只是躺在 .class 文件中的静态数据,此时还没被加载到内存
2、运行时常量池:每一个类或接口的常量池运行时表示形式
3、字符串常量池(没有明确的官方定义,其目的是为了更好的使用 String ):物理存储是在堆中;
string类分析:主要有 2 个成员变 量:char 数组,hash 值。 这点证明了String ,因为他们被priavte+final 修饰,java这样做有什么好处呢?
保证 String 对象的安全性
保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap容器才能实现相应的 key-value 缓存功能
可以实现字符串常量池
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象都是在这分配存储
堆结构:新生代和老年代
新生代和老年代比例为1:2;-XX:NewRatio来指定
新生代进一步划分为Eden和Survivor区,Survivor又分为From Survivor和To Sruvivor;eden,from,to的大小比例为:8:1:1;-XX:SurvivorRatio来指定
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所 以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此新生代实际可用内存空间为90%
JHSDB工具于服务性代理实现的进程外调试工具,可以看到jvm在运行时
1、JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间
2、JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小
3、完成类加载,收集所有类的初始化代码,包括 静态变量赋值语句、 静态代码块、静态方法;静态变量和常量放入方法区。
4、启动 main 线程执行 main 方法,开始执行,该创建对象创建对象,对象 引用放入栈,然后调用其他方法等等,如下图
JVM 在运行时,会从操作系统申请大块的堆内存进行数据的存储同 时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也可以申请过来一块,也就是堆外内存。
栈溢出:java.lang.StackOverflowError无限递归;OutOfMemoryError Stack的出现需要不断建立线程,机器没有足够内存
堆溢出:堆内存溢出:申请内存空间,超出最大堆内存空间;内存泄漏
方法区溢出:运行时常量池溢出;方法区中保存的 Class 信息占用的内存超过了我们配置或者Class对 象该回收时没有被及时回收
直接内存溢出:明显的特征是在 HeapDump 文件中 不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可 以考虑重点排查下直接内存方面的原因
字节码层面:两个方法一个是 main 一个是 init, new 一个对象其实对应着多条字节码指令,证明对象的创 建分好几个过程,其中 invokespecial 指令就是去执行 init 函数
检查加载:检查这个指令的参数是否能在常量池中定位到一个类的符号引用
分配内存:指针碰撞和空闲列表
指针碰撞:内存空间是规整的不能有内存碎片;所有用过的内存都放在一边,空闲的内 存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅 是把那个指针向空闲空间那边挪动一段与对象大小相等的距离
空闲列表:映射(可理解为Map),内存并不是规整的,虚拟机就必须维护一个列表,记录 上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给 对象实例,并更新列表上的记录
如何选择?跟垃圾回收有关,如果会清理内存碎片整理内存就选择指针碰撞;如果不会整理内存就选择空闲列表
并发安全:
cas加失败重试(性能相较差些);对分配内存空间的动作进行同步处理,虚拟机采用 CAS 配上失败重试的 方式保证更新操作的原子性
线程本地缓冲(TLAB,Thread Local Allocation Buffer)(默认)JVM 在线程初始化时,同时也会申请一 块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个buffer,如果需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争 的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块继续使用。
内存空间初始化:虚拟机将分配到的内存空间都初始化为默认值(如 int 值为 0, boolean 值为 false)
设置对象头:虚拟机要对对象头(object header)进行必要的设置,例如markword
对象初始化:构造方法,执行init函数
对象内存布局如图
new Object()在内存中占多少字节?
对象头markword8个字节+class pointer4个字节(开启了指针压缩)+没有实例数据需要填充4字节=一共是12字节
对象头8+如果class pointer没有开启指针压缩就是8+没有实例数据但无需填充=一共16字节
句柄访问
句柄方式:栈指针指向堆里的一个句柄的地址,这个句柄再定义俩指针分 别指向类型和实例。
好处是:垃圾回收时遇到对象内存地址的移动只需要修改句柄即可,不需 要修改栈指针
弊端是:寻址时多了一次操作。
直接指针(默认方式)
直接地址:栈指针指向的就是实例本身的地址,在实例里封装一个指针指 向它自己的类型。
很显然,垃圾回收要移动对象时要改栈里的地址值,但是它减少了一次寻址操作。
强引用:普通引用User user = new User();
软引用: SoftReference 构建软引用SoftReference
用软引用关联的对象,系统将要发生内存溢出( OuyOfMemory )之前,这些对象就会被回收,应用场景:缓存。
弱引用:通过 WeakReference 构建弱引用,正常情况下能通过 get 获取弱引用所引用的对象,但是当 gc 后被弱引用所 引用的对象如果再没有其他强引用的情况下就被回收了。
应用场景:ThreadLocal;用弱引用来解决内存泄漏的问题
虚引用:通过 PhantomReference 构建虚引用,应用场景:协助管理堆外内存
逃逸分析:当一个对象在方法中定义后,它可能被外部 所引用,称之为逃逸。
对象没有逃逸,如果能在栈分配,就在栈分配
开启逃逸分析需要配置以下参数:-XX:+DoEscapeAnalysis(默认开启)
开启逃逸分析编译器会在运行时对代码作如下优化:
(1)同步锁消除:如果确定一个对象不会逃逸出线程,即对象被发现只能 被一个线程访问到,无法被其它线程访问到,那该对象的读写就不会存在竞 争,对这个变量的同步锁就可以消除掉。
(2)分离对象或标量替换。java虚拟机中原始数据类型都不能再进一步分解可称为标量。相对的,一个数据可以继续分解就称为聚合量(例如:对象)。
如果逃逸分析证明一个对象不会被外部访问且可拆解,那么就会拆解创建成若干个成员变量,拆散后的变量便可以被单独分析与优 化, 可以各自分别在栈帧或寄存器上分配空间
(3)将堆分配转化为栈分配:栈上分配就是把方法中的变量和对象分配到 栈上,方法执行完后栈自动销毁,而不需要垃圾回收的介入,从而提高系统性 能。栈上分配基于逃逸分析和标量替换。
栈上分配的优点:1、可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有 效避免垃圾回收带来的负面影响;2、栈上分配速度快,提高系统性能。(减少堆内存的使用和减少GC)
栈上分配的局限性: 栈空间小,对于大对象无法实现栈上分配
大多数情况下,对象在新生代 Eden 区中分配。
当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC。
使用线程本地分配缓冲会加快在 Eden 中的分配效率
大对象直接进入老年代
典型的大对象是很长的字符串或是元素数量庞大的数组(4-5M),更坏的是“朝生夕灭”的大对象
HotSpot提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的:1.避免大量内存复制,2.避免 提前进行垃圾回收,明明内存有空间进行分配。
长期存活对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 区中每熬过一次 Minor GC(新生代垃圾回收) ,年龄就增加 1,当它的年龄增加 到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代。
通过-XX:MaxTenuringThreshold=threshold调整
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象 的年龄必须达到了MaxTenuringThreshold 才能晋升老年代,如果在Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到中MaxTenuringThreshold要求的年龄。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么可以确保Minor GC是安全的。
如果不成立,则虚拟机会查看 HandlePromotionFailure设置值是否允许担保失败。如果允许就会继续检查老年代最大可用连续空间是否大于历次晋升对象平均大小,如果大于会尝试进行Minor GC,如果担保失败会进行Full GC;如果小于或者 HandlePromotionFailure设置值不允许冒险,那就是进行Full GC。
c、c++手动回收问题:忘记回收,造成内存泄露;多次回收,造成运行出错。
引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1。问题在于:循环引用,无法回收影响效率。主流虚拟机未采用
可达性分析:“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
方法区中类静态属性引用的对象;java 类的引用类型静态变量。
方法区中常量引用的对象;比如:字符串常量池里的引用。
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
JVM 的内部引用(class 对象、异常对象 NullPointException、 OutofMemoryError ,系统类加载器)。(非重点)
所有被同步锁( synchronized )持有的对象。(非重点)
JVM 内部的JMXBean、JVMTI中注册的回调、本地代码缓存等(非重 点)
JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收时 只回收部分代的对象)(非重点)
finalize:对象的自我拯救。对象回收需要经过两次标记,经过可达性分析后再进行一次筛选,如果对象被finalize覆盖则不会被回收。finalize只会执行一次。
分代收集:根据对象存活时间长短分为老年代和新生代,新生代一般会有大量对象死去所以采用复制算法,老年代对象存活时间长采用标记整理或标记清除
标记清除:先标记出所有需要回收的对象,然后再次遍历内存将标记过的对象进行回收
会造成内存碎片;回收效率略低
标记复制:将内存分为两块,每次只使用一块当一块使用完触发GC时候将存活的对象复制到另一块上,再把使用过的那块一次性清除掉
实现简单,运行高效;分配完后比较规整;使用率比较低,只有一半;对应引用需要调整
标记整理:标记出需要回收的对象,然后让存活的对象移向内存一端,然后直接清除没有用的内存
需要扫描两遍;不会产生内存碎片但效率偏低
常见的垃圾回收器:
1、用户/工作线程:java程序运行后,用户不停请求操作jvm内存,这些称为用户线程
2、GC线程:jvm系统进行垃圾回收启动的线程
3、串行:GC采用单线程,收集时停掉用户线程
4、并行:GC采用多线程,收集时同样要停掉用户线程
5、并发:用户线程和GC线程同步进行,这意义就不一样了
6、 STW(Stop一the一World)指的是 GC 事件发生过程中,会暂停所有的工作线程,产生应用程序的停顿,没有任何响应,有点像卡死的感 觉,这个停顿称为 STW 。被 STW 中断的应用程序线程会在完成 GC 之后恢复。任何回收器都会发生,只是回收效率越来越高,尽可能地缩短了暂停时间。是jvm在后台自动发起和自动完成的,用户不可控。默认情况下没有最大暂停时间目标。 -XX:MaxGCPauseMillis=调整可能会导致垃圾收集器更频繁地发生,从而降低应用程序的整体吞吐量。
串行收集器,单线程、独占式,适用于小数据集(100M)
Minor GC采用的是复制算法,Major GC采用的是标记-整理算法。
并行收集器,关注吞吐量(吞吐量收集器)、多线程、减少整体垃圾回收时间
吞吐量指标的定义:根据收集垃圾所花费的时间和应用程序时间的时间 来衡量的,即吞吐量=应用程序执行时间/(应用程序执行时间+垃圾收集时间)
Minor GC采用的是复制算法,Major GC采用的是标记-整理算法。
适用于在多处理器或多线程硬件上运行的具有中型到大型数据集的应用程序。
并发收集器,适合回收堆空间在 几个G~ 20G 左右,CMS 收集器是基于标记-清除算法实现的
CMS的整体执行过程分成5个步骤,其中标记阶段包含了三步,具体细节如下:
1、初始标记:标记GC Roots直接关联的对象,会导致STW,但是这个没多少对象,时间短 。
2、并发标记:从 GC Roots 开始关联的所有对象开始遍历整个可达路径的 对象,这步耗时比较长,所以它允许用户线程和GC线程并发执行,并不会导致STW ,但面临的问题是可能会漏标,标记变动等问题。
3、重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生 变动的那一部分对象的标记记录,这个阶段会导致 STW ,但是停顿时间一般会 比初始标记阶段稍长一些,但远比并发标记的时间短。
4、并发清除;将被标记的对象清除掉,因为是标记-清除算法,不需要移 动存活对象,所以这一步与用户线程并发运行。
5、重置线程:重置GC线程状态,等待下次CMS的触发,与用户线程同时 运行。
CMS存在的问题:
CPU敏感;对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大
浮动垃圾,由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运 行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉空间碎片,因为采用的是标记-清除算法,但是如果他退化至使用SerialGC时可处理该问题
多线程垃圾回收器,可以与CMS配合(ParNew回收新生代,CMS回收老年代)
并发收集器,Garbage First,是一种服务器式垃圾收集器,针对具有大内存 的多处理器机器。它试图以高概率满足垃圾收集 (GC) 暂停时间目标,同时实现高吞吐量。
设计思想
将堆被划分为一组大小相等的堆区域(称 作:Region),每个Region都是一个连续的虚拟内存, Region 的大小可以 通过参数 -XX:G1HeapRegionSize=value 设定,取值范围为 1MB~32MB,且应 为 2 的 N 次幂。
通过可达性算法进行全局标记,有个集合统计各个区域中能够回收对象多少,他首先收集可以快速回收的(区域大部分空的)这样可以产生大量可用空间,得到的收益最高。使用暂停预测模型来满足用户定义的暂停时间目标(STW)。G1并没有抛弃之前对堆内存逻辑上的划分,它依然存在 eden 、 survivor 、 old ,同时多了一个 humongous (巨大的)区来存大对象。这些都是标签,可以在某时刻进行转变,例如之前S要升级至O时无需在物理地址上再改变,直接改变标签即可。
参数设置:启用G1收集器-XX:+UseG1GC;设置分区大小;设置最大GC暂停时间;设置堆最大内存;
运行过程:
1、初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快, STW,单线程执行。
2、并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活 对象,这个阶段耗时较长,但可以和用户线程并发执行。
3、重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记 录。STW,并发执行。
4、筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排 序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出后移动合并存活对象 到空Region,清除旧的,完工。因为这个阶段需要移动对象内存地址,所以必 须STW。
优点:并发性;分代GC;空间整理;可预测性
几点建议:
1、如果应用程序追求低停顿,可以尝试选择G1;
2、经验值上,小内存6G以内,CMS优于G1,超过8G,尽量选择G1
3、是否代替CMS只有需要实际场景测试才知道。(如果使用G1后发现性 能还不如CMS,那么还是选择CMS)
自定义加载器
启动类加载器:启动指定的jar包。jre
扩展类加载器:ext
应用程序类加载器:自己写的程序文件