JVM知识

各元素存放的位置可看这篇

JVM探究

一个进程对应一个JVM实例

面试常见:
●请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新?
●什么是OOM(内存溢出),什么是栈溢出StackOverFlowError? 怎么分析?
●JVM的常用调优参数有哪些?
●内存快照如何抓取,怎么分析Dump文件?
●谈谈JVM中,类加载器你的认识
在这里插入图片描述
JRE:java开发环境,包含了JVM

2.JVM的体系结构

一个Java程序运行起来就是一个进程,一个进程对应一个JVM实例,一个JVM实例就有一个运行时数据区,一个进程只有一个方法区和堆,进程里的多个线程共享这两个区,每个线程有自己的程序计数器、本地方法栈,虚拟机栈。

在这里插入图片描述
百分之99的JVM调优都是在方法区和堆(99%是堆)中调优,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。

3. 类加载器

类是模板,是抽象的,类实例化得到的对象是具体的。所有的对象反射回去得到的是同一个类模板。
在这里插入图片描述
在这里插入图片描述
JVM中提供了三层的ClassLoader:

Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。

AppClassLoader:主要负责加载应用程序的主函数类

4.双亲委派机制

看这一篇
在这里插入图片描述
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?

类加载器收到类加载的请求 Application 将这个请求向上委托给父类加载器(ExtClassLoader)去完成,一 直向上委托,直到启动类加载器  Boot
启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器,否则, 抛出异常,通知子加载器进行加载
重复步骤3
举例:程序员自己写了一个String类,类加载器

Class Not Found异常就是这么来的
Null:Java调用不到。Java早期的名字:C+± -  Java = C++:去掉繁琐的东西,指针,内存管理~
Java语言保留了C的接口,这些方法就是用native(本地)修饰的,java通过native方法调用操作系统的方法

6.native

native:

  • 凡是带了native关键字的,说明java的作用范围达不到了(会去调用底层c语言的库)
  • 会进入本地方法栈
  • 调用本地方法本地接口 JNI (Java Native Interface)
  • JNI作用:开拓Java的使用,融合不同的编程语言为Java所用,最初: C、C++
  • Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
  • 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法
  • 在最终执行的时候,加载本地方法库中的方法通过JNI
  • 例如:Java程序驱动打印机,管理系统,掌握即可,在企业级应用比较少
    private native void start0();
    调用其他接口:Socket… WebService … http~

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
Native Method Stack
它的具体做法是Native Method Stack 中登记native方法,在 ( Execution Engine ) 执行引擎执行的时候加载Native Libraies。【本地库】

7.PC寄存器

程序计数器: Program Counter Register

  • 每个线程都有一个程序计数器,是线程私有的,生命周期与线程的生命周期保持一致,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计
  • 没有垃圾回收,不会有oom
    在这里插入图片描述
    在这里插入图片描述

8.方法区 Method Area

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

  • 方法区是不存在堆中的

    在这里插入图片描述
    方法区主要存放的是 Class,而堆中主要存放的是实例化的对象

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
也就是:
static、final、Class、常量池

在这里插入图片描述

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域

  • 多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。

  • 方法区在JVM启动的时候被创建,并且它的实际物理内存空间和Java堆区一样都可以是不连续的。

  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  • 方法区是接口,元空间或者永久代是方法区的实现

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:

    • java.lang.OutofMemoryError:PermGen space(JDK7之前)
      或者
    • java.lang.OutOfMemoryError:Metaspace(JDK8之后)
      举例说明方法区 OOM
  • 加载大量的第三方的jar包
    Tomcat部署的工程过多(30~50个)
    大量动态的生成反射类
    关闭JVM就会释放这个区域的内存。

9.栈

栈:数据结构
程序 = 数据结构+算法︰持续学习~
程序 = 框架+业务逻辑︰吃饭~
栈:先进后出、后进先出,桶
队列:先进先出( FIFO : First Input First Output )

为什么main()先执行,最后结束~

栈:栈内存

栈:栈内存,主管程序的运行,生命周期和线程同步

栈存放的内容:8大基本数据类型+ 对象引用+实例的方法

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)为基本单位存储的
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
    一个方法的执行对应一个栈帧的入栈,一个方法的执行结束对应一个栈帧的出栈
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

为什么main方法先执行,最后结束
线程结束,栈内存也就释放了,对于栈来说,不存在垃圾
StackOverFlowError:
在这里插入图片描述

栈、堆、方法区的交互关系

  • Person 类的 .class 信息存放在方法区中
  • person 变量存放在 Java 栈的局部变量表中
  • 真正的 person 对象存放在 Java 堆中
    在这里插入图片描述
    在这里插入图片描述

10、三种JVM

  • Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)
  • BEA JRockit
  • IBM 39 VM
    我们学习都是:Hotspot

11.堆Heap

一个JVM只有一个堆内存,堆内存的大小是可以调节的。
主要用于存放Java类的实例对象

堆内存分区(重要)

堆内存中还要细分为三个区域:

  • 新生区(伊甸园区) 又被划分为Eden区和Survivor区
  • 养老区 Old/Tenure generation space
  • 永久区 Permanent Space

为什么要把Java堆分代?不分代就不能正常工作了吗?

  • 经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
    • 新生代:有Eden、两块大小相同的Survivor(又称为from/to,s0/s1)构成,to总为空。
    • 老年代:存放新生代中经历多次GC之后仍然存活的对象。
  • 其实不分代完全可以,分代的唯一理由就是优化GC性能。
    • 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
    • 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

GC:Garbage recycling
轻GC:轻量级垃圾回收,主要是在新生区
重GC(Full GC):重量级垃圾回收,主要是养老区,重GC就说明内存都要爆了
在这里插入图片描述
在这里插入图片描述

GC垃圾回收,主要在伊甸园区,和养老区
如果内存满了,OOM,堆内存不够了,

**元空间:逻辑上存在,物理上不存在 (**因为存储在本地磁盘内) 而并不算在JVM虚拟机内存中,也就是并没有占堆内存。

12.堆内存调优

//-Xms8m -Xmx8m -XX:+PrintGCDetails
public class Hello{
	public static void main( String[ ] args) i
	string str = "kuangshensayjava";
	while (true){
	str += str + new Random( ) .nextInt( bound: 888888888)+new Random( ) .nextInt( bound: 99999999);
	}
}

在这里插入图片描述
可以通过调整这个参数(Edit Configuration—>VM options)控制Java虚拟机初始内存和分配的总内存的大小。

默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存: 1/64

OOM:

1、尝试扩大堆内存看结果
2、分析内存,看一下哪个地方出现了问题(专业工具)
//-Xms1024m -Xmx1024m -XX :+PrintGCDetails

当新生代、老年代、元空间内存都满了之后才会报OOM

在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错:

  • 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
  • Dubug,一行行分析代码!

MAT,Jprofiler作用

  • 分析Dump内存文件,快速定位内存泄露;
  • 获得堆中的数据
  • 获得大的对象
    ……

MAT最早集成于Eclipse中,IDEA中可以使用Jprofiles插件,在Settings—>Plugins中搜索Jprofiles,安装改插件即可使用

13.GC

JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代~

在这里插入图片描述
在这里插入图片描述

Minor GC

年轻代 GC(Minor GC)触发机制

  • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden区满Survivor区满不会触发- GC。(每次Minor GC会清理年轻代的内存)
  • 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,即会暂停其它用户的线程,等待垃圾回收线程结束,用户线程才恢复运行,GC调优就是希望GC能少一些。

Major GC

老年代 GC(MajorGC/Full GC)触发机制

  • 指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
  • 出现了MajorGc,经常会伴随至少一次的Minor GC
    但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程
    • 也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC
  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
  • 如果Major GC后,内存还不足,就报OOM了

Full GC

Full GC 触发机制(后面细讲)

触发Full GC执行的情况有如下五种:(归根结底就是老年代空间不足)

  • 调用System.gc( )时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小 大于 老年代的可用内存
  • 由Eden区、survivor space0(From Space)区 向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存 小于 该对象大小
    说明:Full GC 是开发或调优中尽量要避免的。这样STW时间会短一些

内存分配策略

内存分配策略或对象提升(Promotion)规则

  • 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
  • 对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
  • 对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置

针对不同年龄段的对象分配原则如下所示:
在这里插入图片描述

在这里插入图片描述

14判断对象可以回收的方法

JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

14.1引用计数法:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

它的优点是简单、高效,
但是缺点也是异常明显:1.这个方法无法解决对象循环引用的问题2.需要单独的字段存储计数器,增加存储空间开销3.每次赋值都需要更新计数器,增加时间开销。
== Java垃圾回收器中没有使用这类算法==。

程序启动后,objectA和objectB两个对象被创建并在堆中分配内存,它们都相互持有对方的引用,但是除了它们相互持有的引用之外,再无别的引用。而实际上,引用已经被置空,这两个对象不可能再被访问了,但是因为它们相互引用着对方,导致它们的引用计数都不为0,因此引用计数算法无法通知GC回收它们,造成了内存的浪费。如下图:对象之间的引用形成一个有环图。
在这里插入图片描述

14.2可达分析算法

基于引用计数法无法回收循环应用,我们就有了一种新的方法。

可达分析算法,或叫根搜索算法,在主流的JVM中,都是使用的这种方法来判断对象是否存活的

这个算法的思路很简单,它把内存中的每一个对象都看作一个结点,然后定义了一些可以作为根结点的对象,我们称之为GC Roots。如果一个对象中有另一个对象的引用,那么就认这个对象有一条指向另一个对象的边。
在这里插入图片描述
像上面这张图,JVM会起一个线程从所有的GC Roots开始往下遍历,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收。

14.3 什么对象可以当作GC Roots?

共有四种对象可以作为GC Roots

  • 虚拟机栈中的引用对象
    我们在程序中正常创建一个对象时,对象会在堆上开辟一块内存空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种对象可以作为GC Roots。

  • 全局的静态的对象
    也就是使用了static关键字定义了的对象,这种对象的引用保存在共有的方法区中,因为虚拟机栈是线程私有的,如果保存在栈里,就不叫全局了,很显然,这种对象是要作为GC Roots的。

  • 常量引用
    就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也作为GC Roots。

  • 本地方法栈中JNI引用的对象
    有时候单纯的java代码不能满足我们的需求,就可能需要调用C或C++代码(java本身就是用C和C++写的嘛),因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

15.GC常用算法

15.1 标记清除算法

在这里插入图片描述

  • 标记-清除算法 采用从根集合进行扫描,对被引用的(也就是不是垃圾的对象)对象进行标记,标记完毕后,再对堆内存从头到尾进行线性遍历,未被标记的对象进行直接回收,如上图。
  • 标记-清除算法优缺点:
    不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。
    在这里插入图片描述

15.2复制算法 :

新生区主要是用的复制算法
在这里插入图片描述

  • 复制算法将内存分为两个空间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空闲区间)则是空闲的。
  • 复制算法采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。
  • 复制算法优缺点
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

15.3标记压缩算法

  • 复制算法不适用于老年代的垃圾回收,因为老年代的大部分对象都是存活对象,复制成本高,使用复制算法效率低。

  • 标记清除算法可以用于老年代,但是不仅执行效率低,而且执行完之后会产生内存碎片。
    在这里插入图片描述

  • 标记-压缩算法采用和 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。

  • 标记-清除 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

15.4 三种GC算法对比

在这里插入图片描述

15.5 分代回收算法.

在这里插入图片描述
在这里插入图片描述

年轻代复制算法的具体应用:

生成空间好比就是eden区,survivor分别是From幸存区、To幸存区,eden区会标记一些存活的对象拷贝到From区,然后清空eden区。

如果From区和eden区都有垃圾,就把eden区和From区都存活的对象全部拷贝到To区,然后清空eden区和From区

如果To区和eden区都有垃圾,就把eden区和To区都存活的对象全部拷贝到From区,然后清空eden区和To区

反复循环,默认来回循环15次,如果活动的对象还是没有被垃圾回收器回收了,就存放到老年代
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值