初识JVM

初识JVM

面试题

  • 谈谈你对JVM的理解?java8虚拟机和之前有什么变化?
  • 什么是OOM?什么是栈溢出?怎么分析?
  • JVM常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?知道吗?
  • 谈谈JVM中,类加载器你的认识?

1、JVM是什么?

JVM(Java Virtual Machine)是 Java 虚拟机,用于运行 Java 编译后的二进制字节码,最后生成机器指令。

JVM 是 Java 能够跨平台的核心

2、JDK,JRE,JVM三者关系

JDK :(Java Development Kit),Java 开发工具包。JDK 是整个 Java 开发的核心,集成了 JRE 和javac.exe,java.exe,jar.exe 等工具。

JRE :(Java Runtime Environment),Java 运行时环境。主要包含两个部分,JVM 的标准实现和 Java 的一些基本类库。它相对于 JVM 来说,多出来的是一部分的 Java 类库。

三者的关系是:一层层的嵌套关系。JDK>JRE>JVM
在这里插入图片描述

3、JVM的位置

在这里插入图片描述

JVM 与操作系统之间的关系:

JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。

4、JVM的体系结构

JVM体系结构图
在这里插入图片描述

栈、本地方法栈、程序计数器不会发生gc。

jvm调优主要在堆,方法区有一小部分。

5、类加载器

作用:加载Class文件
新建的对象放入堆里面,引用(地址)放到栈,其中引用指向堆里面对应的对象。

img

1)虚拟机自带的加载器
2)启动类(根)加载器 Bootstrap ClassLoader(无法使用Java程序获取到)
3)扩展类加载器 Extension ClassLoader
4)应用程序(系统类)加载器 Application ClassLoader

6、双亲委派机制

双亲委派机制执行过程:如果一个类加载器收到了类加载的请求,这个类加载器不会先尝试加载这个类,而是会先把这个请求委派给自己的父类加载器去完成,在每个层次的类加载器都是依此类推的。因此所有的类加载器请求其最后都应该被委派到顶层的启动类加载器中(Bootstrap),当父加载器无法完成这个请求时,子类加载器才会尝试自己去加载,如果自己管理范围内也找不到需要加载的,就会抛出:ClassNotFoundException 这个异常了。这就是为什么如果我们自己写java.lang.String类的时候,是不行的。

在这里插入图片描述

7、沙箱安全机制

沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离防止对本地系统造成破坏

  • 双亲委派机制-保证JVM不被加载的代码破坏
  • 安全权限机制-保证机器资源不被JVM里运行的程序破坏

组成Java沙箱的基本组件

  • 类加载体系结构(类的双亲委托机制)
  • class文件检验器
  • 内置于Java虚拟机(及语言)的安全特性
  • 安全管理器及Java API

8、*native

凡是带了native关键字的方法,说明java的作用范围达不到了,会去调用底层C语言的库!

会进入本地方法栈。

调用本地方法接口JNI。(Java Native Interface)

JNI作用:扩展java的使用,融合不同的编程语言为java所用。 最初:c、c++

它在内存区域中专门开辟了一块标记区域:native method stack,登记native方法

在最终执行的时候,通过JNI加载本地方法库中的方法

9、PC寄存器

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

10、方法区

方法区:Method Area

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

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

面试题:一张白纸,画出对象实例化过程的内存图。(主要是考你对JVM的理解)

在这里插入图片描述

11、栈

栈:数据结构(先进后出)

程序 = 数据结构 + 算法 持续学习~

队列:先进先出(FIFO:First Input First Output)

栈溢出:

public class Test {
    public static void main(String[] args) {
        //java.lang.StackOverflowError --》 栈溢出 main方法先进栈,
        //然后是test方法进栈,然后a方法进栈,再是test方法进栈,a方法进栈...
        new Test().test(); 
    }

    public void test(){
        a();
    }

    public void a(){
        test();
    }
}

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

线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收问题

一旦线程结束,栈就Over!

栈:8大基本数据类型、对象引用地址、实例方法

栈运行原理:栈帧

栈 + 堆 +方法区:交互关系

在这里插入图片描述

在内存中画出一个对象的实例化过程

先对下面代码进行分析:

class People{
    String name; // 定义一个成员变量 name
    int age; // 成员变量 age
    Double height; // 成员变量 height

    void sing(){
        System.out.println("人的姓名:"+name);
        System.out.println("人的年龄:"+age);
        System.out.println("人的身高:"+height);
    }

    public static void main(String[] args) {
        String name; // 定义一个局部变量 name
        int age; // 局部变量 age
        Double height; // 局部变量 height

        People people = new People() ; //实例化对象people
        people.name = "张三"; //赋值
        people.age = 18; //赋值
        people.sing(); //调用方法sing
    }
}

代码解析:

这段代码首先定义三个成员变量:String name、int age、Double height 这三个变量都是只是声明没有初始化,然后定义了一个成员方法 sing();

在 main()方法里同样定义了三个一样的变量,只不过这些是局部变量;

在main() 函数里实例化对象 people , 内存中在堆区内会给实例化对象 people 分配一片地址,紧接着我们对实例化对象 people 进行了赋值。people 调用成员方法 sing() 。mian()函数打印输入人的姓名,人的年龄和人的身高,程序执行结束。

下面通过图解法展示实例化对象的过程中内存的变化:
在这里插入图片描述
在程序的执行过程中,首先类中的成员变量和方法体会进入到方法区,如图:
在这里插入图片描述
程序执行到 main() 方法时,main()函数方法体会进入栈区,这一过程叫做进栈(压栈),定义了一个用于指向 Person 实例的变量 person。如图:
在这里插入图片描述
程序执行到 Person person = new Person(); 就会在堆内存开辟一块内存区间,用于存放 Person 实例对象,然后将成员变量和成员方法放在 new 实例中都是取成员变量&成员方法的地址值 如图:
在这里插入图片描述
接下来对 person 对象进行赋值, person.name = “小二” ; perison.age = 18; person.height= 180.0;

先在栈区找到 person,然后根据地址值找到 new Person() 进行赋值操作。
在这里插入图片描述
当程序走到 sing() 方法时,先到栈区找到 person这个引用变量,然后根据该地址值在堆内存中找到 new Person() 进行方法调用。

在方法体void sing()被调用完成后,就会立刻马上从栈内弹出(出栈),最后,在main()函数完成后,main()函数也会出栈 如图:
在这里插入图片描述
以上就是Java对象在内存中实例化的全过程。
在这里插入图片描述

12、三种JVM

  • Sun公司 HotSpot
  • BEA JRockit
  • IBM J9 VM

13、堆

Heap:一个JVM只有一个堆内存,堆的大小是可以调节的。

类加载器读取了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量~,保存所有引用类型的真实对象

堆内存细分3个区域:

  • 新生区(伊甸园区) Young/New
  • 养老区 old
  • 永久区 Perm
    在这里插入图片描述

GC垃圾回收,主要是在伊甸园区和养老区。

假设内存满了,OOM,堆内存不够!java.lang.OutOfMemoryError: Java heap space

无限在堆中开辟空间,导致堆空间不足!
在这里插入图片描述

在JDK8以后,永久存储区改名为元空间

14、新生区、老年区、永久区

1)新生区

类诞生、成长甚至死亡的地方

  • 伊甸园区:所有对象都是在 伊甸园区 new出来的!
  • 幸存者区(0区、1区):轻GC当伊甸园区满了之后,第一次只清理伊甸园,活下来的放入幸存者区,后面在当伊甸园区满时,清理伊甸园区和幸存者区(from),活下来的放入幸存者区(to),之后活下来的在两个幸存者区之间来回切换。当清理达到一定次数(默认15次)时,活下来的会进入老年区。等老年区满后,重GC会清理新生区和老年区。都满了就报OOM。

2)老年区

新生区剩下来的,轻GC杀不死了,进去老年区。

3)永久区

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

  • jdk1.6之前:永久代,常量池在方法区
  • jdk1.7:永久代,但是慢慢退化了(去永久代)常量池在堆中
  • jdk1.8之后:无永久代,常量池在元空间
堆空间
在这里插入图片描述

元空间:逻辑上存在,物理上不存在!

15、堆内存调优

在这里插入图片描述

如何解决OOM?
1.尝试扩大内存看结果,如果还报错,说明有死循环代码 或垃圾代码   --> 指令:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
2.分析内存,看一下哪个地方出了问题(专业工具)

扩大内存方法:
Edit Configration>add VM option>输入:-Xms1024m -Xmx1024m -XX:+PrintGCDetails

在这里插入图片描述

在一个项目中,如果出现了OOM故障,那么应该如何排除?研究为什么出错、哪里出错~

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

MAT, JProfiler作用

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

测试代码:

// -Xms 设置初始化内存分配大小   默认 1/64
// -Xmx 设置最大内存分配大小    默认 1/4
// -XX:+PrintGCDetails    打印GC垃圾回收信息
// -XX:+HeapDumpOnOutOfMemoryError   OOM DUmp

//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class Demo03 {

    byte[] array = new byte[1024]; //1m

    public static void main(String[] args) {
        ArrayList<Demo03> list = new ArrayList<>();
        int count = 0;

        try {
            while (true){
                list.add(new Demo03()); //问题所在
                count++;
            }
        } catch (Error e) {
            System.out.println("count = " + count);
            e.printStackTrace();
        }

       /*
       Throwable
          Exception
          Error
        */
    }
}

设置参数:
在这里插入图片描述
运行程序:
在这里插入图片描述
错误分析:
在这里插入图片描述

16、GC:垃圾回收

在这里插入图片描述

JVM在进行GC时,并不是对这两个区域统一回收。 大部分时候,回收都是伊甸园区~

1.新生区

  • 伊甸园区
  • 幸存者区(form,to)

2.老年区

GC两种类:轻GC (Minor GC), 重GC (Full GC)

GC常见面试题目:
  • JVM的内存模型和分区~详细到每个区放什么?
    在这里插入图片描述

  • 堆里面的分区有哪些?
    新生区(Eden, form, to)、 老年区,说说他们的特点!

    新生区:类诞生、成长以及死亡的地方

    ​ 伊甸园区:new 出来的对象都在 伊甸园区

    ​ 幸存者区:伊甸园区满之后,进行轻GC垃圾回收,没死亡的类会进入幸存者区(会在幸存者区的两个区来回交换位置)

    老年区

    进入年老区情况:

    1. 老对象进入老年区(一般是在新生代经历15次GC,也可以使用MaxTenuringThreshold)
    2. 大对象进入老年区(一次请求的对象,新生区没有这么大的空间,会将对象直接分配到老年区(前提是老年区有这么大的空间,如果没有报内存溢出错误))
  • GC的算法有哪些?
    引用计数法、标记清除法、标记压缩算法、复制算法

  • 轻GC和重GC分别在什么时候发生?

    当Eden区没有足够空间进行分配时,虚拟机就会进行一次Minor GC

    Full GC是发生在老年代的垃圾收集动作,采用的是标记清除标记整理算法

    (由于老年代的对象几乎都是Survivor区熬过来的,不会那么容易死掉,因此Full GC发生的次数不会有Minor GC那么频繁,并且time(Full GC) > time(Minor GC))

17、GC几种算法

1)引用计数法

所谓的引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;任何时刻计数器的值为0的对象就会被GC垃圾回收,不能再被引用。
在这里插入图片描述

2)复制算法

复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收。内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。

在这里插入图片描述
img

好处:没有内存的碎片~

坏处:浪费了内存空间~ :多了一半空间永远是空to。假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候;新生区~

3)标记-清除算法

优化复制算法。

分为“标记”和“清除”两个阶段:(1)首先扫描,标记出被引用的对象(2)再次扫描,对没有标记的对象进行清除。

好处:不需要额外的空间!

坏处:两次扫描,严重浪费时间,会产生内存碎片。

4)标记-压缩算法

优化标记清除算法。

压缩:防止内存碎片的产生,再次扫描,向一端移动存活的对象(多了一个移动成本)

总结:

时间复杂度:复制算法 > 标记-清除算法 > 标记-压缩算法

内存整齐度:复制算法 > 标记-清除算法 = 标记-压缩算法

内存利用率:标记-清除算法 = 标记-压缩算法 > 复制算法
在这里插入图片描述
没有最好的算法,只有最合适的算法! –> 分代收集算法

18、JMM

1.什么是JMM?

JMM:Java Memory Model(Java内存模型)

2.他是干嘛的?

作用:缓存一致性协议,用于定义数据读写的规则(遵守)。

JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

volatile

原理:

  1. 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
  2. 规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
  3. 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障防止指令重排序

volatile(得益于内存屏障)

  • 保证可见性
  • 不能保证原子性
  • 禁止指令重排

JVM性能调优

JVM基本组成部分:类加载器运行时数据区字节码执行引擎

JVM体系结构
在这里插入图片描述

程序计数器:记录当前线程代码执行位置,线程相互切换时,防止代码重复执行,因此每个线程会有自己单独的程序计数器。

字节码执行引擎执行字节码,程序计数器的值由字节码执行引擎修改。

代码执行时,会为每一个方法在栈空间中分配一个栈帧,栈帧里包含局部变量表、操作数栈、动态链接、方法出口

GC清理的是没有引用指向的对象。

JVM调优目的是减少GC次数,减少STW

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

面试题:

  • 为什么Java虚拟机会设计这个STW机制?没有这个机制不就不会导致全局卡顿了吗?

反证,假如不设计STW机制,用户线程就会一直执行直到代码结束,用户线程执行时GC也会同时执行,用户线程执行结束可能会有一些垃圾没有回收存在内存中,因此STW机制有其存在的意义。

  • 能否对JVM调优,让其几乎不发生Full GC?

Full GC 最根本的产生原因就是有对象不停的进入老年代,最后导致空间不足,引发 Full GC。解决思路就是直接破坏掉产生条件,直接减少运行时期间从新生代晋升到老年代的对象,或者没有对象晋升到老年代就行了。

1️⃣大对象频繁进行老年代,造成老年代空间快速被占满,造成 Full GC。

解决方案:合理配置-XX:PretenureSizeThreshold大小。避免过多非必要对象进入老年代。

2️⃣metaspace 空间不足

解决方案:一般这个里面存放的都是一些 Class 类信息,Class 本身也是一个对象,需要空间存放。那么程序代码中什么时候会产生对象进入呢,当使用 CGLIB 动态代理不停的生成代理类的时候,就会加载到元数据空间,当然一般 4 核 8G 内存的物理机分配个 512M 是完全没问题的。

3️⃣从年轻代晋升到老年代的对象

  1. 【长期存活对象】达到了设置的年龄限制,默认是 15 次。
  2. Minor GC 后,存活对象大于 survivor 区,存活对象全部进入老年代,注意动态年龄的区别。
  3. 【动态年龄判定】如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代对象动态年龄判断一般是在minor gc之后触发。

JVM垃圾收集器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不进大厂不改名二号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值