日常工作中接触到的jvm相关的知识,和jvm相关书籍中汇总总结一下jvm相关基础知识,作为对jvm的了解。
文章目录
jvm
jvm -Java virtual machine Java虚拟机。
jdk1.8之前的jvm的结构:
运行时数据区域
java源代码 编译成 二进制字节码 jvm指令 解释成一条机器码 交给cpu 执行。
程序计数器
程序计数器 是线程私有。
【作用】: 记住下一条jvm的执行地址
【特点】: 线程私有;jvm中 唯一不会内存溢出的
java虚拟机栈
java虚拟机栈是线程私有。线程运行时需要的内存空间。java虚拟机栈是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
每个线程运行时所需要的内存
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
栈的特点:先进后出
一个栈由多个栈帧组成
堆heap
通过new关键字创建的对象 都会存储在堆内存中。简单来说堆就是Java代码可及的内存,是留给开发人员使用的。
特点:
- 线程共享,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存分配:
-Xms 256m-Xmx 256m
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;
JVM最大分配的内存由-Xmx指 定,默认是物理内存的1/4。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。
java堆区内部结构图 :
对象在刚刚被创建之后是保存在Eden空间区域的,那些长期存活的对象会经由Survivor(幸存者)空间转存到老年代空间区域(Old generation)。当然对于一些比较大的对象(需要分配一块比较大的连续内存空间),则直接进入到老年代区域,这种情况一般在Survivor空间区域内存不足的时候下会发生。
内存分配流程:
堆内存诊断工具:
jps ——查看当前系统中有哪些java进程
jmap——查看堆内存占用情况
jconsole——图形界面,多功能的检测工具,可以连续监测 线程 cpu
非堆内存 nonheap
在JVM中堆之外的内存称为非堆内存(Non-heap memory)
非堆内存分配:
-XX:PermSize 256m-XX:MaxPermSize 256m
VM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;
由-XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
溢出的几种情况
- 堆溢出: java.lang.OutOfMemoryError:Java heap spcace
- 栈溢出:java.lang.StackOverflowError
- 方法区溢出:java.lang.OutOfMemoryError:PermGen space
方法区
方法区是线程共享。存储已被虚拟机加载的类信息、常量、静态变量…
直接内存 Direct Memory
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。
直接内存oom ——当各个内存区域总和大于物理内存限制,就会导致动态扩展时出现OutOf MemoryError
类加载机制
类的加载过程
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,
最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
在Java代码中,类型的加、连接与初始化过程都是在程序运行期间完成的。
JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤。
虚拟机规范规定的5种必须立即执行初始化的情况:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先出发初始化 :
(1) new —— 使用new关键字实力化对象
(2)getstatic、putstatic——读取或设置一个类的静态字段
(3) invokestatic——调用一个类的静态方法 - 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
类加载器加载过程的详细说明
类加载器分类
- 启动类加载器 bootstrap class loader : 最底层的加载器,由c/c++语言实现。负责加载JDK中的rt.jar文件中的所有java字节码文件。
- 扩展类加载器 Extension Class Loader ,负责java相关的
双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
垃圾回收
什么是垃圾回收?
运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
内存回收时会将已经不再使用的对象实例等从内存中移除掉,以释放出更多的内存空间,这个过程就是常说的JVM垃圾回收机制。
新生代的垃圾回收 —— Minor GC
老年代的垃圾回收 —— Major GC 或者Full GC
为什么需要垃圾回收 GC?
清理堆内存空间,堆内存空间碎片整理,主要对象: java堆 方法区
垃圾回收之所以如此重要,是因为发生垃圾回收时一般会伴随着应用程序的暂停运行。
一般发生垃圾回收时除GC所需的线程外,所有的其他线程都进入等待状态,直到GC执行完成。
GC调优最主要目标就是减少应用程序的暂停执行时间。
垃圾收集算法
垃圾回收:把垃圾找出来,把垃圾清掉。
垃圾收集算法:
垃圾回收算法 | 说明 |
---|---|
根搜索算法 | 采用根搜索算法的垃圾回收线程把应用程序的所有引用关系看作一张图,从一个节点GC ROOT,开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,然后对这些节点执行垃圾回收。 |
标记清除算法 | 首先标记,在完成标记后统一回收所有被标记过的对象【弱点】:效率低,清除后产生大量不连续的内存碎片 |
复制算法 | 把可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当把一块内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。 |
标记整理算法 | 标记-整理算法是在标记-清除算法之上,又进行了对象的移动排序整理,虽然性能成本更高了,但却解决了内存碎片的问题。如果不解决内存碎片的问题,一旦出现需要创建一个大的对象实例时,JVM可能无法给这个大的实例对象分配连续的大内存,从而导致发生Full GC。 |
增量回收算法 | 增量回收算法把JVM内存空间划分为多个区域,每次仅对其中某一个区域进行垃圾回收,这样做的好处就是减小应用程序的中断时间,使得用户一般不能觉察到垃圾回收器正在工作。 |
JVM为了优化垃圾回收的性能,使用了分代回收的方式。它对于新生代内存的回收(Minor GC)主要采用复制算法,而对于老年代的回收(Major GC/Full GC),大多采用标记-整理算法。在进行垃圾回收优化时,最重要的一点就是减少老年代垃圾回收的次数,因为老年代垃圾回收耗时长,性能成本非常高,对应用程序的运行影响非常大。
需要特别注意的是,每一种垃圾回收器都会存在用户线程(即用户程序)暂停的问题,只不过每种回收器用户线程暂停的时长优化程度不一样。在启动JVM时,可以通过“指定参数-xx:+垃圾回收器名称”来自定义JVM使用何种垃圾回收器进行垃圾回收。如果未指定的话,JVM将根据服务器的CPU核数和JDK版本自动选择对应的默认垃圾回收器。
时空停顿 stop-the-world
所有应用线程都停止运行所产生的停顿——调优垃圾收集时,要尽量减少这种停顿 是为最关键的因素
垃圾收集的步骤
- 1.查找不在使用的对象
- 2.释放这些对象所管理的内存 & 对堆内存布局进行压缩整理