JVM虚拟机

一、内存模型:包括堆、栈、本地方法栈、方法区(元空间)、程序计数器

JVM虚拟机

1.栈

①、栈又称为线程栈,是每个线程独有的内存空间,存放线程中的局部变量。
(栈中存放的是对象的内存地址,对象是存放在堆中的)
②、栈帧,一个方法对应一个栈帧内存空间。在每个方法执行时,在栈的内存空间中,会分配一块独立的内存空间,称之为栈帧。用于存放方法内部的局部变量表、操作数栈、动态链接和方法出口。
例如一个方法中存在 int a = 1,在栈帧中,首先会将int类型的常量1压入操作数栈,之后在局部变量表中为a划分一小块内存区域,分配完之后,会将操作数栈中的1弹出放入代表a的内存区域中。
方法出口,在被调用方法的栈帧空间,存储调用方法时调用方的代码行数位置,被调用方执行完之后,根据记录的位置,从而返回到调用方法中某一行代码
③、jvm中的栈是利用栈的数据结构,先进后出,先压栈进入的方法后出栈销毁释放
④、线程栈的内存空间中会存在自己程序计算器和本地方法栈

2. 堆

①堆=年轻代+老年代,年轻代=Eden区+Survivor区


 1. 年轻代和老年代默认比例是1:2,也就是默认年轻代占堆内存的1/3,老年代占堆内存的2/3。
 2. 年轻代中存在Eden区(伊甸园区)和Survivor区(幸存者区),在Survivor区中有两块内存区域,	这里及下文可以称为S0区和S1区。Eden区和Survivor区默认比例为4:1,也就是Eden区占年轻代内存空间的8/10,Survivor区占年轻代内存空间的2/10。Survivor区中的S0区和S1区以1:1的比例存在。简而言之,Eden区、S0区、S1区在年轻代中的默认比例为8:1:1。
 3. 在程序中创建对象时,首先会将创建的对象放入Eden区。
 4.   Eden区经过不断的对象被创建放入,Eden区被放满,此时,字节码执行引擎会暂停用户线程(STW 注释①),在后台开启一个线程执行minor GC(可达性分析算法),minor GC会将垃圾对象回收,非垃圾对象移入Survivor区的S0分区(复制算法),并将对象头中的年代年龄加1。 
 5. 经过一次minorGC后,Eden区被清空,之后创建的新对象依旧放入Eden区,直到下次Eden区被放满。
 6. Eden区再次被放满时,暂停用户线程,再次开启minor GC,这一次 minorGC会对Eden区和Survivor区上一次放入对象的分区进行一垃圾回收,将垃圾对象回收,非垃圾对象放入Servivor区的另一个分区,同样对象头中的分代年龄加1。
 7. 如此反复,当对象的分代年龄达到15,也就是执行了15次minor GC依旧没有被回收的对象,会被移入老年代中。
 8. 随着老年代中的对象增多,老年代被放满,字节码执行引擎暂停用户线程,在后台开启一个线程执行full GC(可达性分析算法),当老年代被放满并且其中对象无法被回收时,出现OOM(out Of Memory 注释③)。

  • 其他进入老年代的方式
    • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象,比如字符串、数组。jvm参数-XX:PretenureSizeThreshold
      可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效。==为什么要这样做呢?==为了避免为大对象分配内存时的复制操作而降低效率。
    • 对象动态年龄判断,当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio
      可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor GC之后触发的
      -== Minor GC后存活的对象Survivor区存放不下==,这种情况会把存活对象部分挪到老年代,部分可能还会放到Survivor区。

JVM虚拟机
永久区:这个区域常驻内存的。用来存放JDK自身携带的class对象,interface元数据,储存的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭VM虚拟机就会释放这个区域的内存

Jdk1.6之前:永久代,常量池在方法区
Jdk1.7:永久代,慢慢退化,去永久代,常量池在堆中
Jdk1.8之后:无永久代,常量池在元空间

什么时候出现永久区满
一个启动类,加载了大量的第三方jar包,tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM

发生OOM内存溢出,解决方法

A、 尝试扩大堆内存看结果
B、 分析内存,看一下哪个地方出现问题(专业工具)

1、内存溢出:(Out Of Memory—-OOM)

系统已经不能再分配出你所需要的空间,比如系统现在只有1G的空间,但是你偏偏要2个G空间,这就叫内存溢出
例子:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那就报错。

2、内存泄漏: (Memory Leak)

强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象,意思就是你用资源的时候为他开辟了一段空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出

3、JProfiler工具分析OOM原因:

分析Dump内存文件,快速定位内存泄漏
获得堆中的数据
获得大的对象
JVM虚拟机
JVM虚拟机

4、jvm调优

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。

JVM性能调优方法和步骤:
1.监控GC的状态;
2.生成堆的dump文件;
3.分析dump文件;
4.分析结果,判断是否需要优化;
5.调整GC类型和内存分配;
6.不断的分析和调整

3.双亲委派机制

A、说明:
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
B、作用:
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
C、流程图(理解,向上委托,向下加载)
JVM虚拟机
D、类加载器类别:(由上至下)
BootstrapClassLoader(启动类加载器)-> ExtClassLoader (标准扩展类加载器)-> AppClassLoader(系统类加载器)-> CustomClassLoader(用户自定义类加载器)

4.native关键字(主要用于方法上)

1、一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。
2、在定义一个native方法时,并不提供实现体(比较像定义一个Java Interface),因为其实现体是由非Java语言在外面实现的
主要是因为JAVA无法对操作系统底层进行操作,但是可以通过JNI(java native interface java本地方法接口)调用其他语言来实现底层的访问。
举例:Thread类中的start() 方法中调用一个start0()的native方法。

5.方法区(元空间)

类装载子系统会将字节码文件(.class)的相关信息放到方法区里,方法区中包含:常量、静态变量、类信息、对象引用(内存地址)等

  • 注意:public static User user = new User();,静态变量user和new User()的内存地址存放在方法区,new User()产生的对象存放在堆中。

6.jvm参数

参考
https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

7、class文件加载到内存的过程

JVM虚拟机

①、类的加载过程
一个java文件从被加载到被卸载这个生命周期,一共需要经历5个阶段,jvm将类加载分为:
加载(loading)->链接(linking 验证+准备+解析)->初始化(initializing 使用前的准备)->使用->卸载回收(GC)

加载(loading)
首先通过一个类的全限定名来获取此类的二进制字节流;
其次将这个字节所代表的静态存储结构转化为方法区的运行时数据结构;
最后在java堆中生成一个代表这个类的class对象,作为方法区这些数据的访问入口。
总的来说就是class文件从硬盘,经过loading的过程(classloader),读到内存,进行linking(链接),之后进行initializing(类初始化),最后由GC回收。。

链接(linking)
链接分为 验证(verification)、准备(preparation)、解析(resolution)
什么方法可以进行解析:构造方法,private方法;带多态的方法是无法静态解析的
验证: 对格式进行校验,确保被加载类的正确性;
准备: 为类的静态变量分配内存,并将其初始化为默认值;
解析: 静态解析,把类中的符号引用转换为直接引用;

初始化(initializing)
此时静态变量赋值为初始值

②、类的初始化
1 类什么时候才被初始化?

创建类的实例,也就是new一个对象
访问某个类的或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(Class.forName())
初始化一个类的子类(会首先初始化子类的父类)
JVM启动时表明的启动类,既文件名和类名同名的那个类
2 类的初始化顺序
1)如果这个类还没有被加载和链接,那就先进行加载和链接
2)如果类存在父类并且这个类还没有被初始化,那就初始化直接的父类(不适用于接口)
3)static静态变量、静态块
4)总的来说,初始化顺序依次为: 静态变量、静态初始化块->变量 初始化块->构造器; 如果有父类,则:父类static方法->子类static方法->父类构造方法->子类构造方法。

③、类的加载
类的加载是指将类的.class文件中的二进制数据读入内存,将其放在运行时数据区的方法区内,然后在堆里创建一个这个类的java.lang.Class的对象,用来封装类在方法区类的对象。
类的加载的最终产品是位于堆中的Class对象。Class对象封装了类在方法区内的数据结构,并且提供了访问方法区内的数据接口。加载类的方式依次为:

* 从本地系统直接加载
* 通过网络下载.class文件
* 从zip,jar等归档文件中加载.class文件
* 从专有数据库中提取.class文件
* 将java源文件动态编译为.class文件(服务器)

④、类加载器

8、GC

①、什么是可回收垃圾对象

JVM虚拟机

如图分析: A为栈中的一个局部变量,B是堆中的一个对象,C是B的一个局部变量,D为C指向的一个对象。
此时,虽然D被C指向,但是B没有被任何对象引用,C是B的一个实例成员变量(无法算作GC Root),所以BCD都是垃圾对象。
如果A指向于B,那么此时BCD就不为垃圾对象。

②、如何找到垃圾对象(主要两种算法)

1)引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是可回收对象。
这个方法实现简单,效率高,但是目前主流的虚拟机并没有选择使用这个算法,其主要原因是它很难解决对象之间相互循环引用的问题。
所谓对象之间相互循环引用,如:objA和objB相互引用对方,除此之外无其他引用,但因为他们相互引用对方,导致他们引用计数器无法为0,于是引用计数器无法通知GC回收器回收它们。

2)可达性分析算法:
将GC Root做作为起点,从这些节点向下搜索引用的对象,找到的对象都标记为‘非垃圾对象’,其余未标记的对象都是垃圾对象。 GC Root,根节点,包括静态变量、本地方法栈的变量、线程栈的局部变量等

⑤、jvm垃圾回收算法:
1)Mark-Sweep 标记清除:
将内存区域进行标记,分为存活对象、未使用、可回收,对可回收区域对象进行回收,回收后,可回收区域变为未使用区域,标记的存活对象区域依然保留。
JVM虚拟机

弊端:-位置不连续,产生碎片。

2)Copying 复制算法:
将空间分为两份,用户使用一半空间,回收时,将可回收对象回收,未使用(包括回收后的空间此时也成为未使用区域)、存活对象复制到另一半空间,上一半空间全部清空成为未使用区域。
JVM虚拟机
弊端:-没有碎片,浪费空间。

3)Mark-Compact 标记整理:
在标记清除算法上做了一个改进,回收后对空间进行了整理。
JVM虚拟机
弊端:-没用碎片,效率偏低。

④、常用垃圾收集器解析

java中至今为止,有10种垃圾收集器。 下图1:
JVM虚拟机
在jdk1.8之前 主要以左侧的垃圾收集器为主,1.8默认的垃圾收集器是Parallel Scavenge(PS 收集年轻代)、Parallel Old(PO 收集老年代);分代模型;
1.8之后以右侧的垃圾收集器为主,像G1 是jdk1.9默认的垃圾收集器;分区模型

1)在jdk早期1.0/1.1/1.2的时代,默认的垃圾收集器是Serial和Serial Old,Serial(串行)
JVM虚拟机
使用-XX:UseSerialGC,年轻代使用SerialGC,老年代自动使用Serial Old GC

例如,用户线程进入,发现年轻代的Eden区满了,暂停用户线程,开启一个GC线程,SerialGC年轻代使用复制算法进行垃圾回收,当老年代满了,暂停用户线程,开启一个GC线程,Serial Old GC 使用标记-整理算法进行垃圾回收。

2)在jdk1.8,默认的垃圾收集器是Parallel Scavenge、Parallel Old,Parallel(并行)
JVM虚拟机
使用-XX:UseParallelGC/-XX:UseParallelOldGC,年轻代使用Parallel Scavenge GC,老年代使用Parallel Old GC

例如,用户线程进入,发现年轻代的Eden区满了,暂停用户线程,开启多个 GC线程,Parallel Scavenge GC年轻代使用复制算法进行垃圾回收,当老年代满了,暂停用户线程,开启 多个 GC线程,Parallel Old GC 使用标记-整理算法进行垃圾回收。

3)CMS(Concurrent Mark Sweep — 并发标记清除),由于无论是SerialGC、Serial Old GC、Parallel Scavenge GC、Parallel Old GC,它们在垃圾回收的时候都会暂停用户线程(STW stop the world),所用出现了CMS。但是由图1可见,Parallel Scavenge是无法和CMS一起使用的,所以出现了ParNew,进行年轻代的垃圾回收。ParNew和Parallel Scavenge十分相似,在Parallel Scavenge上做了改良,能够搭配CMS。
JVM虚拟机
JVM虚拟机
使用-XX:UseConcMarkSweepGC,年轻代使用ParNew GC,老年代使用CMS GC与Serial Old GC收集器的组合,Serial Old GC将作为CMS出错的后备收集器。

例如,用户线程进入,发现年轻代的Eden区满了,暂停用户线程,开启多个 GC线程,ParNew GC年轻代使用复制算法进行垃圾回收(这里和Parallel Scavenge没什么区别);当老年代满了,也会暂停用户线程(时间短,几乎可以忽略不计),进行初始标记(初始标记: 找到GC Root下的第一个引用)—-> 第二阶段开始并发标记(延着初始标记继续向下找引用对象),此时用户线程可以并行执行,不会产生STW,但此时可能产生浮动垃圾(并发标记为非垃圾的对象,在并行执行的用户线程中失去引用),还可能产生错标(在初始标记时,标记的垃圾对象,在并发标记时,并行的用户线程对这个对象添加了引用) 浮动垃圾影响不是很大,因为可以在下次垃圾回收时,再进行回收,但是错标,会导致非垃圾对象被回收掉,程序产生错误 —> 所以为防止错标,并发标记之后,会进行重新标记,此时也会暂停用户线程(时间短,几乎可以忽略不计) —-> 最后进行并发清理,此时用户线程可以并行执行,不会产生STW。

为CMS GC与Serial Old GC收集器的组合使用?
在并发标记、并发清理的时候,由于用户线程是可以执行的,所以有可能会产生新的对象,在空间不足又产生新的对象时,CMS会出错,此时便会停止CMS,使用Serial Old GC进行垃圾回收。

CMS,是为了解决STW时间过长的问题,CMS的STW据说不会超过100ms;G1是JDK9的默认垃圾收集器,据说STW时间不会超过10ms;
ZGC是JDK11的默认垃圾回收器,据说STW时间不会超过1ms

⑤、G1管理内存分块
JVM虚拟机
G1会把内存分为很多的Region(区域),Region之间不是连续的,每一块Region可能是老年代、Eden、Survivor、也可能是Humongous(大对象),大对象一块Region装不下,就会用连续的多块Region来装。
好处:可以多线程的进行垃圾回收。
注意:但G1的内存区域里不是固定的E或O等,假如一块Region一开始装的Eden区的对象,在这个对象回收掉之后,这块区域可能分配老年代的对象。所以G1对比以前的垃圾回收就不需要指定年轻代与老年代的占比了

上一篇:2020-11-29


下一篇:Java 进阶 07 —— JVM 垃圾回收相关概念