JVM探究

本文详细探讨了JVM(Java虚拟机)的各个方面,包括JVM的位置、体系结构、类加载器的层次结构,如启动类加载器、扩展类加载器和应用程序加载器。文章详细阐述了双亲委派机制和沙箱安全模型,以及其组件如字节码校验器、存取控制器、安全管理器和安全软件包。此外,还介绍了JVM内存区域,如PC寄存器、方法区、栈、堆和新生区,以及JVM的垃圾回收机制,包括各种垃圾收集算法。文章还提及了JDK8前后JVM内存的变化,以及JMM(Java内存模型)的概念及其在多线程中的作用。
摘要由CSDN通过智能技术生成

JVM探究

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

1、JVM的位置

请添加图片描述

2、JVM的体系结构

请添加图片描述

请添加图片描述

3、类加载器

作用:加载Class文件 ~ new Student():

请添加图片描述

4、虚拟机自带的加载器

5、启动类(根)加载器

6、扩展类加载器

7、应用程序加载器

8、双亲委派机制

9、沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚机机(VMM)特定的运行范围中 并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
在java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信,而远程代码则及看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型

请添加图片描述

但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型

请添加图片描述

当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问,虚拟机中不同的受保护域(Protected Domain), 对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(dk 1.6)

请添加图片描述

10、组成沙箱的基本组件

  1. 字节码校验器
    确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

  2. 类装载器

    • 类装载器在3个方面对Java沙箱起作用:

      • 它防止恶意代码去干涉善意的代码;

      • 它守护了被信任的类库边界;

      • 它将代码归入保护域,确定了代码可以进行哪些操作。

      • 类装载器采用的机制是 双亲委派模式。

  3. 存取控制器
    存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

  4. 安全管理器
    是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。

  5. 安全软件包
    java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者、消息摘要、数字签名、加密、鉴别等。

请添加图片描述

11、Native

凡是带了native关键字的,说明java的作用范围达不到, 去调用底层C语言的库!
JNI: Java Native Interface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈,其他的就是Java栈;

Native Interface本地接口
本地接口的作用是融合不同的编程语言为]ava所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是在Native Method Stack 中登记native方法,在(Execution Engine)执行引擎执行的时候加
载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过lava程序驱动打印机或者]ava系统管理生产用Web Service等等,不多做介绍!

Native Method Stack
之的具体做法层Nataive Method stack 中登记native方法.在(Fxecution Engine ) 执行引擎执行的时候加载Native Libraies。【本地库】

12、PC寄存器

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

13、方法区

Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是 实例变量存在堆内存中,和方法区无关

static,final,Class,常量池~

14、栈:数据结构

程序 = 数据结构 + 算法

栈:先进后出,后进先出

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

喝多了吐就是栈,吃多了拉就是队列

为什么main()先执行,最后结束~
栈:栈内存,主管程序的运行,生命周期和线程同步;

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

一旦线程结束,栈就Over!
栈: 8大基本类型 + 对象引用+实例的方法
栈运行原理:栈帧

栈满了:StackOverflowError

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

请添加图片描述

Java对象在内存中实例化的过程

在讲 Java 对象在内存中的实例化过程前,先来说下在类的实例化过程中,内存会使用到的三个区域:栈区、堆区、方法区。

  • 堆区:
    • 存储的全部都是对象,每个对象包含了一个与之对应的 class 类的信息。
    • jvm 只有一个堆区(steap),它会被所有线程共享,堆中不存放基本数据类型和对象引用,它只存放对象本身。
  • 栈区:
    • 每个线程都包含一个栈区,栈中只保存基本数据类型的值和对象以及基础数据的引用。
    • 每个栈中的数据(基本数据类型和对象的引用)都是私有的,其它栈是无法进行访问的。
    • 栈分为三个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
  • 方法区:
    • 又被称为静态区,它跟堆一样,被所有的线程共享,方法区包含所有的 class 信息 和 static修饰的变量。
    • 方法区中包含的都是整个程序中永远唯一的元素,如:class、static变量。
简谈 JDK8 前后 JVM内存变化

JDK8 之前对内存划分为:新生代(YOUNG)—老年代(Tenured)—永久代(PermGen)
新生代:
新生代又分为伊甸区(Eden) 存活区(Survivor),其中存活区又分为两个大小空间一样的s0、s1,而且s0 和 s1 可以互相转化,存活区保存的一定是在伊甸区保存了很久的,并且经过好几次小的GC还存活下来的对象,存活区一定会有两块大小相等的空间。目的是一块存活区未来的晋升,另一块存活区是为了对象的回收。需要注意的是:这两块存活区一定有一块是空的

新生代中的 GC:
新生代大小(PSYoungGen total 9216K)=eden大小(eden space 8192K)+1个survivor大小(from space 1024K)

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8(Eden):1(一个survivor),一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中。
  因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到老年代中。

为什么要设置两个Survivor区?
设置两个Survivor区最大的好处就是解决了碎片化;
假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

碎片化带来的风险是极大的,严重影响Java程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存。。。画面太美不敢看。。。这就好比我们上学时背包里所有东西紧挨着放,最后就可能省出一块完整的空间放饭盒。如果每件东西之间隔一点空隙乱放,很可能最后就要手提一路了。

14.1、Java 中的数据类型

Java 中的数据类型有两种:

1、基本类型(primitive types): 共有8种,即:int、short、long、byte、char、float、double、boolean(注意并没有 String 的基本类型),这8中类型的定义是通过诸如:int a = 5;long b = 22L;的形式来定义的,称为自动变量。注意:自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在;

如:int a = 5; 这里的 a 是一个指向 int 类型的引用,指向 5 这个字面值,这些字面值的数据由于大小可知,生存期可知( 这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了 ),出于追求速度的原因,这些字面值就存在于栈区中;

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义
   int a = 3;
   int b = 3;
   编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

特别注意的是:这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

2、 包装类数据:如:String、Integer、Double等将相应的基本数据类型包装起来的类,这些数据全部存放在 堆 中, Java 用 new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

14.2、类实例化时内存中发生的变化

首先我们先对下面的代码进行分析:

public 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.stuid = 180.0 ;   //赋值
        people.sing();              //调用方法sing
    }
}
12345678910111213141516171819202122

代码解析

这段代码首先定义三个成员变量: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 = 13; person.height= 180.0;

先在栈区找到 person,然后根据地址值找到 new Person() 进行赋值操作。

如图:
请添加图片描述

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

在方法体void speak()被调用完成后,就会立刻马上从栈内弹出(出站 )

最后,在main()函数完成后,main()函数也会出栈 如图:

请添加图片描述

以上就是Java对象在内存中实例化的全过程。

15、三种JVM

  • Sun公司 HotSpot Java Hotspot(TM) 64-Bit Server VM (build 25.181-b13,mixed mode)
  • BEA JRockit
  • IBM J9 VM

我们学习的都是:HotSpot

16、堆

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

类加载器读取了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量一,保存我们所有引用类型的真实对象;
堆内存中还要细分为三个区域::

  • 新生区(伊甸园区)
  • 养老区
  • 永久区

GC垃圾回收,主要是在伊甸园区和养老区~
假设内存满了,OOM,堆内存不够!java.lang.OutofMemoryError: ]ava heap space
在JDK8以后,永久存储区改了个名字 (元空间) ;

17、新生区(伊甸园区)

  • 类:诞生和成长的地方,甚至死亡;
  • 伊甸园,所有的对象都是在 伊甸园 区new出来的!
  • 幸存者区 (0,1)

养老区(老年区)

请添加图片描述

真理:经过研究,99%的对象都是临时对象!

18、永久区

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

一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;

  • jdk1.6之前:永久代,常量池是在方法区;
  • jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
  • jdk1.8之后:无永久代,常量池在元空间

请添加图片描述

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

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

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

MAT, Jprofiler 作用:

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

19、堆内存调优

20、GC: 垃圾回收

请添加图片描述

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

  • 新生代
  • 幸存区 (form , to )
  • 老年区

GC两种类:轻GC(普通的GC),重GC (全局GC)

GC题目:

  • JVM的内存模型和分区一详细到每个区放什么?
  • 堆里面的分区有哪些? Eden, form, to,老年区,说说他们的特点!
  • GC的算法有哪些? 标记清除法,标记整理,复制算法,引用计数器,怎么用的?
  • 轻GC和重GC分别在什么时候发生?
引用计数法:

请添加图片描述

复制算法

请添加图片描述
请添加图片描述

  • 好处:没有内存的碎片
  • 坏处:浪费了内存空间,多了一半空间永远是空的to区,假设对象100%存货(极端情况)。

复制算法最佳使用场景:对象存活度较低的时候:新生区

标记清除算法:

请添加图片描述

  • 优点:不需要额外的空间!
  • 缺点:两次扫描,严重浪费时间,会产生碎片
标记压缩

再优化

请添加图片描述

标记清除压缩

先标记清除,再清除压缩

总结

内存效率:复制算法>标记清除算法 > 标记压缩算法 (时间复杂度)
内存整齐度:复制算法=标记压缩算法 要 标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法

没有最好的算法,只有最合适的算法-----> GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法!

老年代:

  • 区域大:存活率高
  • 标记清除(内存碎片不是太多) + 标记压缩混合 实现

JMM

  1. 什么是JMM ?

    JMM:
    Memory Model(Java内存模型)

  2. 它是干嘛的?

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

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

请添加图片描述

解决共享对象可见性这个问题:volilate
  1. 它该怎么学?

    JMM:抽象的概念,理论
    JMM对这八种指令的使用,制定了如下规则:

    • 不允许read和load、store和write操作之一单独出现。即使用了read必须
      load,使用了store必须write
    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
    • 不允许一个线程将没有assign的数据从工作内存同步回主内存
    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock—个被其他线程锁住的变量
    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

    JMM对这八种操作规则和对volatile的—些特殊规则就能确定哪里操作是线程安全哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

    volatile:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值