浅谈Java 虚拟机与GC原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jjc120074203/article/details/78802839

再谈之前 先给大家强烈推荐一本书。《分布式Java应用》 作者 林昊 它里面很清楚写了相关 java 底层基础相关知识。

本人根据书里面的讲解的内容以及各大博客学习的心得 将其整理一下。如果有不足之处的请各位大神指出。

1.1 Java文件的编译机制

Java 代码 编辑*.class文件 命令:javac 类名
大概流程 如图:
这里写图片描述
一共分为三部:

  • 分析和输入到符号表
    • 就是将代码转换为一个字符串转变为token 序列,将token 序列转换成抽象生成语法树。
  • 注解处理
    • 处理用户自定义的annotation
  • 语义分析和生成class 文件
    • 基于抽象语法树进行语法分析,检查变量是否声明、语句是否全部执行到底、异常是否全部Throw出去等等,分析完毕之后 将实例成员的初始化收集到构造器中、将静态初始化器初始化为类与接口初始化方法,接个后序遍历抽象语法树,将符号表中转换成*.class 文件。

1.2类的装载机制

原理 :就是讲*.class 文件加载到JVM 生成Class 对象(或者说是字节码)的一个过程。
这里写图片描述
包括三个步骤:

  1. 装载
    类装载器负责找到二进制字节码,并且加载到JVM的过程 classload+类的全限定名进行查找。数组直接有JVM创建
  2. 链接(校验—》准备—-》解析)
    字节码校验器负责对二进制字节码进行格式校验、初始化状态类中的静态变量及解析类中的接口。
  3. 初始化(不一定是在类的装载中必须触发完成的,也可以是需要用之前触发就可以了)
    通常有4种情况会出现: 1.new Class 2.子类调用父类方法 3.JVM 指定初始 4.反射调用了类中的方法。

1.3类的执行机制

原理:当生成class文件之后,就可以对class对象的方法进行调用之后,在源码编译阶段将源码编译为JVM 字节码,字节码在运行时通过JVM做一次转换生成机器指令 然后写入JVM硬件 )

JVM转换为机器指令提供两种类型的执行方式:

  • 字节码解释执行
  • 编译执行(JIT编译器 just in time)

字节码解释执行
对于对执行不频繁的代码则继续采用解释的方式
我们知道JVM是基于栈体系进行 优点是代码紧凑 ,代码体积小。
每个线程创建的时候都会创建栈 ,每个栈里面包含多个栈帧,每个栈帧包括 局部变量区,操作数栈,方法地址等信息。
这里写图片描述

编译执行
将字节码编译为机器码的支持 JIT编译器
特点:对于执行频繁的代码采用编译执行
通常有两种:

  • client compiler
    • 主要优化有 方法内联、去虚拟化、冗余削除
  • server compiler
    • 针对逃逸分析【一个对象的指针是否被多个方法或者线程引用,如果是 该指针表明发生逃逸】做了一些优化。 主要方法:标量替换,消除同步等

3.1JVM 内存回收

JVM 内存回收主要回收的是堆中的和方法区中的内存。主要基本原理:
首先去内存中查找不需要的内存,然后对内存进行回收操作
通常采用的收集器的方式对内存进行回收。常用的收集器主要有

引用计数收集器和跟踪收集器

引用计数收集器
主要采用了分散式管理方式,通过计数的方式表示对象是否被引用,当计数值为0时,表示当前对象不再被引用。
比如: 当A释放了B的引用的时候 ,B的计数值归零,回收B内存。
如果出现循环引用 :A-B、A-C、B-C 此时这种方式就不太高效了。

跟踪收集器

跟踪计数器采用的是集中管理的方式对内存对象进行集中回收处理。全局记录引用状态,并且基于一定的条件触发。每次执行需要从根集合扫描存活对象的状态。
主要算法有 Copying 、Mark-Sweep 、Mark-Compact 三种方式。
Copying
从根节点扫描存活的对象,然后将这些对象复制到一个全新未使用的内存中。使用点:存活内存较少的时候比较适用。
Mark-Sweep
从根节点扫描存活的对象,标记所有存活的对象,然后扫描整个集合对未存活的对象进行清除。缺点会造成内存碎片。
Mark-Compact
从根节点扫描存活的对象,标记所有存活的对象,然后扫描整个集合对未存活的对象 ,对这些对象进行清除的同时在进行移动操作。
优点不会产生内存碎片。缺点 回收成本比较高

分代的垃圾回收策略

是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
打印JVM GC日志配置: -XX:+PrintGCDetails
新生代 GC–MinorGC
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。因此对新生代对象内存占用的对象进行GC为MinorGC.
主要原理:新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
常用的方式
Serial GC
新生代内存按照8:1:1的比例分配,在整个扫描环境和复制过程基本采用单线程进行GC 回收,更加实用单CPU,新生代的内存比较小以及对暂停时间要求不是非常高的应用上。通常可以采用 -XX:+UseSerialGC 进行配置指定方式。
并行回收GC ( Parallel Scavenge)
在启动时Eden、SO、S1 的比例按照上述方式进行分配,但是在运行一段时间之后,Parallel Scavenge 会根据当前Minor GC 的频率动态调整Eden、SO、S1 分配比例。手动配置-XX:-UseAdaptiveSizePolicy 设置Eden、SO、S1 值大小。扫描整个环境和复制的过程基本采用的是多线程进行的并对并行回收做了很多的优化。-XX:+UseParallelGC 配置
并行GC ( ParNew)
ParNew 对SurvivorRatio 分配方式与串行是一致的, 与Parallel Scavenge对大的区别就是并行GC需要配合CMS进行使用。-XX:+UseConcMarkSweepGC 配置

.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

旧生代(Old Generation)与持久代 (Permanet Generation)GC
旧生代: 经过多次垃圾回收没有被回收的对象或者大对象。
持久代 :用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响
SDK 采用了串行GC、并行GC、并发GC这三种方式完成对旧生代与持久代的GC回收。
串行GC
它是基于于Mark-Sweep-Compact 算法来实现的。
1。先扫描整个集合对,对存活的对象进行着色处理。
2.遍历整个旧生代(Old Generation)与持久代GC,清理不用的内存对象。
3.移动压缩,存活的对象进行压缩移动,直到有一串连续的内存了
它也是通过单线程的方式进行内存回收。通常情况耗时比较长。可以通过 -XX:+PrintGCApplicationStoppedTime 来查看当前GC回收时间。
并行GC
主要采用了Mark-Sweep-Compact 算法来实现。但是对其进行了相应的优化的动作。
1。先扫描整个集合对根据存活对象的代码密度区状态,选择扫描内存区域,对齐进行着色移动标记,此过程单线程
2.遍历整个旧生代(Old Generation)与持久代GC,并发的回收移动操作
通过-XX:+UseParallelGC 或-XX:+UseParallel01dGC 来强制指定。

并发GC(Concurrent Mark-Sweep)

CMS 采用的是Mark-Sweep 算法方式来实现。同时引用了free List 的方式去记录旧生代中空闲的内存空间,当旧生代进行分配的时候,会优先根据free list 中的位置去分配内存
需要注意的一点:CMS GC 回收的时候大部分都伴随着应用并发的进行的。分配内存的时候 就会伴随回收的情况。会导致freeList 竞争压力太大。因此引用了了Mutual exclusion locks 以JVM分配内存优先。
1.第一次标记( Initial Marking) * 暂停应用,先对旧生代扫描可访问的对象进行标记A*
2.并发标记(Concurrent Marking) 然后对第一次标记的A进行 扫描,,通过对象间 的引用关系进行标记,在之前可能出现并发引用标记的被修改 的情况,CMS 采用 Card Table 的方式,对引用改变的对象标识位dirty
3.由于并发标记可能导致引用错乱,因此采用 Final Marking ( remark )进行对引用进行修改。需要暂停应用,可能会修改对象的引用关系或创建新的对象,因此要对这些改变或新创建的对象也进行扫描重新扫描 再次着色。
4**.并发收集**(Concurrent Sweeping )恢复所有应用的线程,就进入到这步了,这步要负责的是将没有标记
的对象进行回收。由于内存碎片的原因,可能会造成每次回收的内存比之前分配出去的小。为避免这种现象,在进
行sweeping 的时候,CMS 会尽量将相邻的块重新组装为一个块,采用的方法为首先从free list 中删除块,组装完毕后再重新放入free list 中。

CMS GC 应用场景:

  • CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
  • CMS 并非没有暂停。而是用两次短暂停来替代串行标记整理算法的长暂停。
  • CMS回收器减少了回收的停顿时间,但是降低了堆空间的利用率。

Full GC
当旧生代和持久代 发生GC的时候,其实 新生代也发生着GC,新生代/旧生代 会按照自己的新生代/旧生代的配置方式进行GC,这一方式叫做Full GC。

Full GC 触发的条件:

  1. 当旧生代空间不足
  2. Permanet Generation 空间满
  3. CMS GC时出现promotion failed 和 concurrent mode failure
  4. 统计得到的Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间

JVM调优

通常情况下:MinorGC 会快于Full GC 。但是各个代大小设置直接决定了MinorGC 和Full GC
常用的关键配置:

  • -Xms JVM 初始堆内存
  • -Xmx JVM 初始堆最大堆内存
  • -XX:PermSize=64M JVM初始分配的非堆内存
  • -XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配
  • -Xmn 决定了新生代(New Generation ) 空间的大小 如果要设置Eden,S0,S1 他们之前的比例问题的话。-XX:SurvivorRatio 来控制。Sun官方推荐配置为整个堆的3/8
  • -XX:MaxTenuringThreshold 控制对象在经历多少次Minor GC 后才转入旧生代,通常又将此值称为新生代存活周期 PS:串行GC 有效–XX:UseSerialGC

配置时候常用的问题:
1. (SUN)-Xms 和-Xmx 配置一致好处:
JVM 通常情况下-Xms 默认的内存为物理内存的1/64,-Xmx通常情况为默认物理内存的1/4,如果内存小于初始堆内存的时候,JVM 会自动将内存扩大为最大堆内存的大小。相反如果内存大于 最大内存的时候,JVM 会自动减少至当前最小堆内存为止。依次往复。 如果将两个值设置为相等时,就可以避免运行时要不断地扩展JVM 内存空间这个问题。
2. 新生代内存设置合理性
JVM=新生代+旧生代+永久代
【新生代内存设置太小】 1.会导致MinorGC 频发GC 2.可能会导致对象提前进入旧生代。
【新生代内存设置太大】1.MinorGC耗时比较长 旧生代就小了,FullGC 就可以频繁增加,
通常推荐的比例是新生代占JVM Heap 区大小的33%左右。
3.新生代生命周期合理性
-XX:MaxTenuringThreshold = 16 表示对象经理16次GC之后就进入了旧生代。在增大了存活周期后,对象在Minor GC 阶段被回收的机会就增加了,但同时带来的是survivor 区被占用。

猜你喜欢

转载自blog.csdn.net/jjc120074203/article/details/78802839
今日推荐