浅谈JVM
一、JVM内存结构概述

Java源代码编译成Java Class文件后通过类加载器ClassLoader加载到JVM中
- 类存放在方法区中
- 类创建的对象存放在堆中
- 堆中对象的调用方法时会使用到虚拟机栈,本地方法栈,程序计数器
- 方法执行时每行代码由解释器逐行执行
- 热点代码由JIT编译器即时编译
- 垃圾回收机制回收堆中资源
- 和操作系统打交道需要调用本地方法接口
-
程序计数器(Program Counter Register)
每个线程都有一个独立的程序计数器,占用空间非常小,它记录了线程正在执行的字节码指令的地址,字节码解释器通过修改它的值来选取下一条要执行的指令,因此被称为程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。注意如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。因为native方法由C/C++实现 ,并没有被编译为字节码指令。 -
Java虚拟机栈(Java Virtual Machine Stack)
栈也是线程私有的,生命周期与线程相同,栈描述了Java方法执行时的内存模型。JVM会在每个方法执行的时候在栈中创建一个栈帧,用来存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了各种编译期可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
当异常线程请求的栈深度大于虚拟机所允许的深度时抛出 StackOverflowError,虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError(HotSpot虚拟机栈容量无法动态扩展)。
-
本地方法栈(Native Method Stacks)
和Java虚拟机栈的作用相同,不同的是里面存储的是native方法,也就是非java语言实现的方法。native方法会调用本地库接口(JNI),所以我们只能看到一个由native修饰的方法名。native方法实现纯java无法实现的功能,通常与操作系统及硬件有关。 -
Java堆(Java Heap)
Java堆是JVM管理内存中最大的一块,是被所有线程共享的一块区域,它的唯一作用就是存储对象的实例,几乎所有的对象是李都会在这里分配内存。因此。Java堆又被称作GC堆,因为垃圾回收器回收的就是对象实例,也就是说堆是由垃圾回收器管理的区域。
Java堆中也是有区域分配的,如新生代、老年代、永久代亦或元空间,每个区域中存储存活时间不同的实例。大部分实例创建后都是在新生代中,经研究发现,大部分实例生命周期短,活不过新生代,因此新生代是垃圾回收器主要的回收区域。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 错误。
- 方法区(Method Area)
方法区也是线程共享的区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError错误。
- 运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
二、双亲委派机制(类加载)
什么是双亲委派机制
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
为什么要使用双亲委派机制
防止重复加载同一个.class文件,通过委托去向上级问,加载过了就不用加载了。
保证核心.class文件不会被串改,即使篡改也不会加载,即使加载也不会是同一个对象,因为不同加载器加载同一个.class文件也不是同一个class对象,从而保证了class执行安全
三、GC垃圾回收机制
1、垃圾判别法
引用计数法
- 判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 优点:执行效率高,程序执行受影响小
- 缺点:无法检测出循环引用的情况,导致内存泄露
可达性分析算法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
可作为 GC Roots 的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
强、软、弱、虚引用
JDK1.2 以前,一个对象只有被引用和没有被引用两种状态。后来,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。
- 强引用就是指在程序代码之中普遍存在的,类似"Object obj=new Object()"这类的引用,垃圾收集器永远不会回收存活的强引用对象。
- 软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
- 弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
- 虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
2、垃圾回收算法
标记清除法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 优点:处理速度快
- 缺点:造成空间不连续,产生内存碎片(标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。)
标记整理法
- 标记没有被GC Root引用的对象
- 整理被引用的对象
- 优点:空间连续,没有内存碎片
- 缺点:整理导致效率较低
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半
- 分配同等大小的内存空间
- 标记被GC Root引用的对象
- 将引用的对象连续的复制到新的内存空间
- 清除原来的内存空间
- 交换FROM空间和TO空间
- 优点:空间连续,没有内存碎片
- 缺点:占用双倍的内存空间
分代垃圾回收机制
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代
每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代
老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用
标记 —— 清除或者标记 —— 整理算法回收。
3、内存分配与回收策略
对象优先在 Eden 分配
对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。
新生代 GC (Minor GC)
发生在新生代的垃圾回收动作,频繁,速度快。
老年代 GC (Major GC / Full GC)
发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。
-
大对象直接进入老年代
-
长期存活的对象将进入老年代
-
动态对象年龄判定
-
空间分配担保
四、Native关键字
凡是带了native关键字的,说明java的作用范围达不到了,就会去调用c语言的库
进入本地方法栈;调用本地方法接口JNI,JNI作用:扩展Java的使用,融合不同的编程语言为Java所有,最初是为了融合C C++
在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法
在最终执行的时候,在执行引擎中通过JNI加载本地方法库中的方法
目前该方法的使用越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机、Java系统管理生产设备等,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等。
pc寄存器
- 它是一块很小的的内存空间,几乎可以忽略不记。也是运行速度最快的区域
在jvm规范中,每个线程都有它自己的程序技术器。是线程私有的,声明周期与线程的生命周期保持一致。 - 在任何时间一个线程都要一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的jvm指令地址。
方法区
存放的是:常量,静态变量,类信息,常量池
五、栈
程序 = 数据结构 + 算法
先进后出,后进先出。栈内存,主管程序的运行,生命周期和线程同步;线程结束,栈内存就释放了。对栈来说,不会存在垃圾回收问题,因为一旦线程结束,栈就死了。
栈里存放:8大基本类型 + 对象引用 + 实例的方法
**运行原理:**栈帧
参考:https://blog.youkuaiyun.com/ATFWUS/article/details/104536028?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162072933516780269899425%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162072933516780269899425&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-104536028.first_rank_v2_pc_rank_v29&utm_term=%E6%A0%88%E5%B8%A7
栈满了:StackOVerFlowRrror
栈 + 堆 + 方法区的关系
java类实例化内存中流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5exZcBAg-1620835238845)(C:\Users\HP\Desktop\JVM.assets\image-20210511210254145.png)]
六、堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把类,方法,常量,变量放在堆中,保存我们所有引用类型的真实对象。
堆内存中还要细分伟三个区域:
-
新生区:其中又分为三个区:伊甸园区,幸存0区,幸存1区。
他是一个类诞生和成长的地方,甚至死亡
伊甸园区:所有的对象都是在这儿new出来的
假设伊甸园区里有10个空间,满了以后就会触发轻CG(普通GC)。有的对象还存在引用就活下来了,死了的就没了。活下来的就会到幸存区,如果幸存区也满了触发重GC(全局GC),活下来的就会到老年区。
正常情况下有99%的对象是临时对象,很少有活下来的到老年去
幸存区(0,1)
-
老年区:
-
永久区:
这个区域是常驻内存的,用来存放JDK自带的Class对象。存储的是java运行时的一些环境或类信息。这个区域不存在垃圾回收,关机虚拟机的时候会释放内存。
jdk1.6之前:永久代,常量池在方法区里
jdk1.7:永久代,慢慢的退化了,有个概念叫“去永久代”,常量池在堆中
jdk1.8之后:没有永久代了,常量池在元空间中
什么情况永久区会崩?
一个启动类加载了大量的第三方jar包,Tomcat部署了太多的应用或大量生成的反射类。 这些如果不断被加载,直到内存满,就会出现OOM。
GC垃圾回收主要是在新生区和老年区
假设内存满了,OOM,堆内存不够。
在JDK8以后,永久区改了个名字叫元空间
堆内存调优
1、尝试扩大堆内存
-Xms:设置初始堆内存分配大小,默认为物理内存的 1 / 64
-Xmx:设置最大内存分配大小,默认为物理内存的 1 / 4
-XX:+PrintGCDetails:输出相信的GC处理日志
-Xms1024m -Xmx1024m -XX;+PrintGCDetails
如果项目中,突然出现了OOM故障,应该如何排除?
- 使用内存快照分析工具,Jprofiler
- Debug,一行一行分析
JProfiler作用:
- 分析Dump内存文件,快速定位内存泄露
- 获得堆中的数据
- 获得大的对象
9万+

被折叠的 条评论
为什么被折叠?



