Bootstrap

Java-JVM详解

结构图

在这里插入图片描述

1.类加载器ClassLoader

ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。

类加载器分为四种:前三种为虚拟机自带的加载器。
1.启动类加载器(Bootstrap)C++
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

扩展类加载器(Extension)Java
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包

应用程序类加载器(AppClassLoader)Java
也叫系统类加载器,负责加载classpath中指定的jar包及目录中class,即加载用户创建的类

用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

双亲委派模型

应用程序类加载器 把 扩展类加载器视为父加载器,
扩展类加载器 把 引导类加载器视为父加载器。
不是继承关系,是组合的方式实现的。

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

简单来说: 下一级的类加载器,如果接到任务时会先把任务往上传,一直到根加载器,如果根加载器在它负责的路径下没有找到对应的class,会往回传到扩展类加载器让他加载对应的class,如果没有,会传到应用程序类加载器,如果AppClassLoader也没有找到对应的class,那么会报ClassNotFoundException或NoClassDefError,如果在某一级找到了,就直接返回Class对象。

好处:防止内存中出现多份同样的字节码(安全性角度)

1.Method Area方法区

被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
所有定义的方法的信息都保存在该区域,此区属于共享区间。 *
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中

2.stack栈

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

主要保存3 类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
栈操作(Operand Stack):记录出栈、入栈的操作。
栈帧数据(Frame Data):包括类文件、方法等等。

栈运行原理:
当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……

每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕
后会自动将此栈帧出栈。

常见问题栈溢出:Exception in thread “main” java.lang.StackOverflowError

3.Native Method Stack(本地方法栈)

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

4.堆

在这里插入图片描述
堆的内存结构:
在这里插入图片描述

Java7之前
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行

堆内存逻辑上分为三部分:
对象在这里产生,应用,最后被垃圾回收器收集
Young Generation Space 新生区
Tenure generation space 养老区
Permanent Space 永久区
新生代(年轻代)、老年代、永久代(持久代)。

Young Generation Space 新生区

新生区分为伊甸区(Eden space)和幸存者区(Survivor pace)
所有的对象都是在伊甸区被new出来的
幸存区分为From区(Survivor From space)和To区(Survivor To space)

MinorGC垃圾回收的过程如下
1.当伊甸区满的时候会触发第一次GC,把还活着的对象拷贝到From区,伊甸区又满了再次触发GC时,会扫描伊甸区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,同时把这些对象的年龄+1,并清空伊甸区和From区中的对象,
2.To和 From区互换,原To成为下一次GC时的From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
4,大对象特殊情况: 如果分配的新对象比较大Eden区放不下,但Old区可以放下时,对象会被直接分配到Old区(即没有晋升这一过程,直接到老年代了)

MinorGC的过程:复制 -> 清空 -> 互换

Tenure generation space 养老区(老年代)

经历多次GC仍然存在的对象会放进老年代(默认是15次),老年代的对象比较稳定,不会频繁的GC
若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”

出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够
因有二:
1.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整
2.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

永久代

用于存放JDK自身所携带的 Class、Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

出现java.lang.OutOfMemoryError: PermGen space,**说明是Java虚拟机对永久代Perm内存设置不够,原因: 程序启动需要加载大量的第三方jar包

永久代是方法区(相当于是一个接口interface)的一个实现。

Jdk1.8及之后: 无永久代,常量池1.8在堆中
在这里插入图片描述

永久代与元空间的最大区别之处:
永久代使用的是jvm的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。因此,默认情况下,元空间的大小仅受本地内存限制

堆参数调优入门

在这里插入图片描述
常用JVM参数:

参数备注
-Xms初始堆大小。只要启动,就占用的堆大小,默认是内存的1/64
-Xmx最大堆大小。默认是内存的1/4
-Xmn新生区堆大小
-XX:+PrintGCDetails输出详细的GC处理日志
查看jvm堆的默认值大小:
Runtime.getRuntime().maxMemory()   // 堆的最大值,默认是内存的1/4
Runtime.getRuntime().totalMemory()  // 堆的当前总大小,默认是内存的1/64

设置JVM参数: java -Xmx50m -Xms10m

查看堆内存详情
执行前配置参数:-Xmx50m -Xms30m -XX:+PrintGCDetails
在这里插入图片描述

新生代和老年代的堆大小之和是Runtime.getRuntime().totalMemory()

idea分析dump文件

idea生成dump文件配置:
-Xmx50m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tmp
-XX:HeapDumpPath:生成dump文件路径。

jdk自带了该类型文件的解读工具:jvisualvm.exe
在这里插入图片描述
常用命令行(了解)
查看java进程:jps -l
查看某个java进程所有参数:jinfo 进程号
查看某个java进程总结性垃圾回收统计:jstat -gc 20292

5.progrom counter register 程序计数器

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码
用来存储指向下一条指令的地址,即将要执行的指令代码
由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

6.执行引擎Execution Engine

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

7.本地接口Native Interface

本地接口的作用是融合不同的编程语言为 Java 所用
内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

GC垃圾回收

在这里插入图片描述

GC的特点:
次数上频繁收集Young区
次数上较少收集Old区
基本不动Perm区

垃圾判定

1.引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的.
优点:
简单,高效,现在的objective-c、python等用的就是这种算法。
缺点:
引用和去引用伴随着加减算法,影响性能
很难处理循环引用,相互引用的两个对象则无法释放。

2.可达性分析算法
通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

在Java语言中,可以作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中的引用对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中JNI(Native方法)的引用对象

真正标记以为对象为可回收状态至少要标记两次。
第一次标记:不在 GC Roots 链中,标记为可回收对象。
第二次标记:判断当前对象是否实现了finalize() 方法,如果没有实现则直接判定这个对象可以回收,如果实现了就会先放入一个队列中。并由虚拟机建立一个低优先级的程序去执行它,随后就会进行第二次小规模标记,在这次被标记的对象就会真正被回收了

四种引用

强引用: 类似于 Object obj = new Object(); 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。(只要引用就不会回收)

软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,才会将这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存。(内存溢出之前回收)

弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。(在下一次垃圾回收前)

虚引用:PhantomReference 类实现虚引用。无法通过虚
引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。(为乐得到一个通知)

垃圾回收算法

1.复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

优点:
1.实现简单
2.不产生内存碎片
缺点:
1.将内存缩小为原来的一半,浪费了一半的内存空间,
2.在对象存活率达到一定程度时,将会变的不可忽视,最起码对象的存活率要非常低才行

年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
原因: HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,新创建的对象都会被分配到Eden区。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法在这里插入图片描述
2.标记清除(Mark-Sweep)
算法分为2个阶段:

  1. 标记出需要回收的对象,使用的标记算法均为可达性分析算法
  2. 回收被标记的对象。

缺点:
效率问题(两次遍历)
空间问题(标记清除后会产生大量不连续的碎片。JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。)

3.标记压缩:
标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。

优点:
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
缺点:
如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

4.分代收集算法
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
难道就没有一种最优算法吗?
回答:无,没有最好的算法,只有最合适的算法。=======>分代收集算法。
分代回收算法实际上是把复制算法和标记整理法的结合

年轻代(Young Gen): 区域相对老年代较小,对像存活率低。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenure Gen): 区域较大,对像存活率高。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

1.Serial/Serial Old收集器:
特点:
新生代,老年代使用串行回收,
新生代复制算法,老年代标记-压缩,垃圾收集的过程中会Stop The World(服务暂停)

它还有对应老年代的版本:Serial Old
参数控制:-XX:+UseSerialGC 串行收集器

2.ParNew 收集器
ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。
特短板
新生代并行,老年代串行;
新生代复制算法、老年代标记-压缩
参数控制:
-XX:+UseParNewGCParNew收集器
-XX:ParallelGCThreads 限制线程数量

3.Parallel / Parallel Old 收集器
特点:可以通过参数来打开自适应调节策略,通过参数控制GC时间
新生代复制算法、老年代标记-压缩

参数控制: -XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供

参数控制: -XX:+UseParallelOldGC使用Parallel收集器+ 老年代并行

4.CMS收集器
以获取最短回收停顿时间为目标的收集器,是一种预处理垃圾回收器,不能等到old内存用尽是回收

优点: 并发收集、低停顿
缺点: 产生大量空间碎片、并发阶段会降低吞吐量

5.G1收集器
并行与并发
分代收集
空间整合
可预测的停顿

每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
步骤:
1、初始标记(Initial Making)
2、并发标记(Concurrent Marking)
3、最终标记(Final Marking)
4、筛选回收(Live Data Counting and Evacuation)

总结:
垃圾回收器选择策略 :
客户端程序 : Serial + Serial Old;
吞吐率优先的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;
响应时间优先的服务端程序 :ParNew + CMS。
G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。

;