写作时间:2020-01-30
实现目标:JVM解析与总结
涉及知识:JVM

JVM体系结构

类装载器ClassLoader

负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎(Execution Engine)决定

虚拟机自带的加载器
    启动类加载器(Bootstrap)C++
    扩展类加载器(Extension)Java
    应用程序类加载器(AppClassLoader)Java也叫系统类加载器,加载当前应用的classpath的所有类
    用户自定义加载器  Java.lang.ClassLoader的子类,用户可以定制类的加载方式

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时,子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

执行引擎Execution Engine

负责解释命令,提交操作系统执行。

本地接口Native Interface

本地接口的作用是融合不同的编程语言为 Java 所用,初衷是为了融合 C/C++程序,Java 诞生时是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

PC寄存器PC Register

每个线程启动的时候,都会创建一个PC(Program Counter ,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。

本地方法栈Native Method Stack

具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库

方法区Method Area

方法区也是所有线程共享区,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

存储内容
    类型信息 这个类型的全限定名 这个类型的直接超类的全限定名 这个类型是类类型还是接口类型 这个类型的访问修饰              符 任何直接超接口的全限定名的有序列表
    字段信息 字段名 字段类型 字段的修饰符
    方法信息 方法名 方法返回类型 方法参数的数量和类型(按照顺序) 方法的修饰符
    方法列表(Method Tables )
    常量池

栈Stack

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。

遵循“先进后出”/“后进先出”原则

每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

堆Heap

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存

堆内存逻辑上分为三部分:
    1) 新生代Young Generation:
        Eden:
            大部分新生的对象创建区
            超过一定内存空间会转移到 Survivor区
        form to:
            Space(S0) Space(S1)
    2) 年老代:  年轻代的对象如果能够挺过数次收集,就会进入年老代
    3) 永久代元空间

垃圾回收GC

简单的说就是内存中已经不再被使用到的空间就是垃圾

判断对象的存活

引用计数

给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。

可达性分析

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
作为GC Roots的对象包括下面几种:
    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2. 方法区中类静态属性引用的对象。
    3. 方法区中常量引用的对象。
    4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

引用

强引用
    一般的Object obj = new Object() ,就属于强引用。

软引用 SoftReference
    一些有用但是并非必需,用软引用关联的对象,系统将要发生OOM之前,这些对象就会被回收。

弱引用 WeakReference
    一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。

虚引用 PhantomReference
    幽灵引用,最弱,被垃圾回收的时候收到一个通知

GC算法

复制算法

复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉

年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认值是 15)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。

优点:运行效率高,没有内存碎片。 缺点:需要双倍空间,造成内存的利用率不高

标记清除

标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。缺点:此算法需要暂停整个应用,会产生内存碎片

优点:不需要额外空间。 缺点:此算法需要暂停整个应用,会产生内存碎片

标记整理

在标记阶段,确定所有要回收的对象,并做标记。存活对象往内存的一端移动,然后直接回收边界以外的内存。

优点:不需要额外空间。 缺点:实现复杂,开销大

分代回收算法

根据对象的存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-整理”算法进行回收。

GC垃圾回收器

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现

分类

串行垃圾回收器(Serial): 它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境

并行垃圾回收器(Parallel): 多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景

并发垃圾回收器(CMS): 用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程互联网公司多用它,适用于对响应时间有要求的场景

G1垃圾回收器: G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收

查看默认的垃圾回收器:

java -XX:+PrintCommandLineFlags -version

`垃圾收集器底层配置代码

G1垃圾回收器

以前收集器特点: 年轻代和老年代是各自独立且连续的内存块、年轻代收集使用单eden+S0 +S1进行复制算法、老年代收集必须扫描整个老年代区域、都是以尽可能少而快速地执行GC为设计原则

G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW(Stop The World)。

G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。

宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region), 可以近似理解为一个围棋的棋盘。

G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是-I部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。

G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的 survivor(tospace)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;

G1回收过程

JVM的参数

参数类型

标配参数:
    - version
    - help

X参数:
    -Xint    解释执行    
    -Xcomp    第一次使用就编译成本地代码
    -Xmixed    混合模式

XX参数:
    Boolean类型: 公式  ==> -XX: +或者- 某一个值(+表示开启 -表示关闭)
        -XX:+printGCDetails 是否打印GC收集细节
        -XX:-UseSerialGC   是否使用串行垃圾收集器
    KV设值类型:  公式   ==>  -XX:属性key=属性值value
        -XX:MetaspaceSize=128m
        -XX:MaxTenuringThreshold=15
    jinfo查看当前运行程序的配置:  jinfo -flag 配置项 进程编号

常用配置

-Xms: 初始大小内存,默认为物理内存1/64(等价于-XX:InitialHeapSize)

-Xmx: 最大分配内存,默认为物理内存1/4(等价于-XX:MaxHeapSize)

-Xss: 设置单个线程的大小,一般默认为512K~1024K(等价于-XX:ThreadStackSize)

-Xmn: 设置年轻代大小,一般都不用设置,一般都是用默认的即可

-XX:MetaspaceSize: 设置元空间大小

-XX:+PrintGCDetails: 输出详细GC收集日志信息

-XX:SurvivoRatio: 设置新生代中eden和s0/s1空间比

-XX:NewRatio: 配置年轻代与老年代在堆中的比例

-XX:MaxTenuringThreshold: 设置垃圾最大年龄

OOM

全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。 当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error

Java.lang.StackOverflowError: 栈溢出错误

Java.lang.OutOfMemoryError:Java heap space: 堆内存不够用

Java.lang.OutOfMemeoryError:GC overhead limit exceeded: 程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高

Java.lang.OutOfMemeoryError:Direct buffer memory: 导致原因:写NIO程序经常使那ByteBuffer来读取或者写入数据, 这是一 种基于通道(Channel)与缓冲区(Buffer)的I/0方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一 些场景中显著提高性能, 因为避免了在Java堆和Native堆中来回复制数据。

Java.lang.OutOfMemeoryError:unable to create new native thread: 导致原因:你的应用创建了太多线程了,一个应用进程创建多 个线程,超过系统承戴极限你的服务器并不允许你的应用程序创建这么多线程, linux.系统默认允许单个进程可l以创建的线程数是1024个,你的应用创建超过这个数量,就会报java. lang. OutOfMemoryError: unable to create new native thread

Java.lang.OutOfMemeoryError:Metaspace: 元空间溢出


施工中...

您的喜欢是作者写作最大的动力!❤️
  • PayPal
  • AliPay
  • WeChatPay
  • QQPay
YAN