内存模型

线程独占:

虚拟机栈:先进后出的 又称方法栈,线程执行方法都是创建一个栈帧,用来存储局部变量表(存放局部变量的),操作数栈(后进先出的操作数栈。负责写入数据和提取数据),动态链接(执行常量池中的方法引用),方法出口等信息,JVM对栈的操作有2种,出栈和入栈。方法调用就是入栈。方法返回就是出栈

本地方法栈:区别是虚拟机栈为执行Java方法服务,而本地方法栈则为Native方法服务 什么是native方法呢 就是非Java方法。与Java环境外交互。因为JVM一些底层是C写的。比如Thread类中的setPrioruty方法

HotspotJVM中 将本地栈和虚拟机栈合二为一的 栈是运行时单位 堆是存储的单位

程序计数器:一块较小的内存空间,是当前线程所执行的字节码的行号指令器,每条线程都要有一个独立的程序计数器,这类内存也被称为线程私有的内存。用来记录程序执行到哪一个地方,下次可以在这个地方继续执行

线程共享:

堆:java虚拟机内存最大的一块,被所有线程共享,几乎所有线程实例都在这里分配内存。

1.7包含新生代、老年代、永久代

1.8只有新生代和老年代(永久代被删除,新增元空间并且直接存放在内存上)

方法区:存储已被虚拟机加载的类信息,常量,静态变量

堆内存详解

以Java8及之后为例,堆是内存模型内占用最大空间的一块地方。这里主要存放对象实例,同时堆又被分为2个区域:

新生代:存放新对象或者没达到一定年龄的对象。新生代内部又划分为3个区域,Eden区、以及2个幸存区(S0和S1),这三个区域默认比例为8:1:1。当Eden区满的时候会发生Minor GC 此时将Eden区和S0区的对象放入S1,然后清空无用对象,此时S0区是空的。下次Minor GC时 将Eden区和S1区存活的对象放入S0,这样做是为了优化垃圾收集的频率,因为我们创建的对象大部分都是无用的。每发生一次MinorGC,对象的年龄会+1,如果一个对象在新生代达到了一定次数(默认15次)则会被放入老年代。可以通过调整MaxTenuringThreshold参数来决定进入老年代的年龄

老年代:存放常用的对象和大对象(大对象直接放入老年代是为了避免在新生代内产生大量的内存拷贝)。老年代的GC为 Major GC,和新生代比起来,需要的时间更长

JVM的组成部分

JVM包含两个子系统和两个组件。

类加载子系统: 根据类的全限定名来装载class文件

执行引擎子系统: 执行class中的指令

本地接口: 与编程语言交互的接口(c函数)

运行时数据区: JVM的内存模型

方法区、永久代、元空间的关系

方法区实际上是一种规范,我们可以理解成一个接口。不同的虚拟机厂商针对这个规范做了不同的实现。以HotSpot虚拟机举例。永久代就是最初的一个实现。慢慢的,这个接口一直变更。最后放弃实现类。使用元空间进行替代。

深入理解Java虚拟机中。是这么说的。方法区和堆一样,是各个线程共享的内存区域。用于存储被虚拟机加载的类信息,常量,静态变量和一些即时编译后的代码数据

新生代和老年代默认比值是1:2

到了Java8,永久代被移除,元空间接替。

元空间不与堆存在一起,而是直接存在本地内存上。所以理论上,机器的内存有多大,元空间就有多大

为什么使用元空间替代永久代

永久代的版本下,字符串常量池存在永久代中。大量使用字符串的情况下,很容易发生OOM异常,此外,JVM加载的class总数,方法总大小有时也不稳定。所以永久代的大小很难确定。使用元空间可以解决这个问题。

对象内存布局

对象内存布局分为三个部分,对象头,实例部分,对齐填充

对象头(Header)包含三部分

  1. Mark Word 存放对象自身的运行时数据,如hashcode,gc分代年龄,锁状态标识,线程持有的锁,偏向线程ID

  2. Class Pointer 对象指向类型元数据的指针 虚拟机通过这个指针来确定这个对象是哪个类的实例。

  3. 如果这个对象是数组,还有数组长度的信息。因为虚拟机可以通过Java对象的元数据确定Java对象的大小。但是从数组的元数据无法确定数组的小。

实例部分(Instance Data): 对象真正存储的有效信息,也是程序代码定义的类型字段内容。

对齐填充(Padding): 没什么特别的含义,可以理解为占位符。因为VM的内存管理对象起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍,对象头部正好是8字节的整数倍,所以如果我们对象实例数据没有对齐时,就要通过对齐填充来补齐。

为什么CG分代年龄最大是15?

因为对象头header的mark word里面存的信息有对象年龄。使用的是4bit 4bit所能表达的最大值就是15

判断对象是否存活的2种算法

引用计数算法: 给对象种添加一个引用计数器,每当一个地方引用它时,计数器+1 当引用失效时,计数器-1 引用数量为0时,说明对象没有被任何引用,可以被认为时垃圾对象,效率高,但无法解决循环引用的问题 Python采用此算法

可达性算法(根搜索算法): 为了解决引用计数法的循环引用问题,可达性分析算法的基本思路就是一系列名为GC Roots的对象作为起点,从这些节点向下搜索,搜索走过的路径成为引用链 当一个对象到GC Rootes没有任何引用链相连时,证明此对象不可用(不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程,两次标记后仍然是可回收对象,将要面临回收)

可做为GCRoots的对象: 虚拟机栈中引用对象 本地方法引用对象 方法区常量引用对象 方法区静态对象

CMS垃圾回收器和G1垃圾回收器

CMS垃圾回收器: 老年代的回收器,以最短回收停顿时间为目标的收集器。标记清除算法运作过程: 初始标记,并发标记,重新标记,并发清除。完成后会产生大量空间碎片。

G1垃圾回收器: JDK7诞生,JDK8走向成熟,JDK9成为默认的垃圾回收器。主要作用于老年代和新生代,基于标记整理算法实现。回收的范围是整个Java堆,分为初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确控制停顿。

JVM调优参数

- Xmx:堆的最大值

- Xms:堆的最小值

- Xmn:新生代大小

- Xss:堆栈大小

- XX:NewRatio:新生代与老年代的比例,默认为2 也就是1:2

- XX:MaxTenuringThreshold:对象进入老年代的年龄 默认15

- -XX:SurvivorRatio:Eden区与幸存区的比例大小 默认为8 也就是幸存区和Eden区的比例为2:8

一般Xms和Xmx我们会设置为一样,这样可以避免动态调整带来的损耗,让程序启动就分配整个堆内存

类的生命周期

加载:通过类的权限定名获取对应的二进制字节流并转换为方法区运行时需要的数据结构,在堆中生成一个类对象,作为一个入口

验证:主要是确保加载的类符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证以及符号引用验证等

准备:为类的静态变量分配内存并设置初始值(这里的初始值指的是0,null,false等,并非程序代码写的初始值!)

解析:将类中的符号引用转为直接引用,即直接指向目标的指针

初始化:为类的静态变量赋予正确的初始值

双亲委派模型

双亲委派模型是JVM类加载的一种。其他的加载方式还有

全盘负责:当一个类加载器去负责加载某个类。那么这个类所引用的其他类也要有这个类去加载

父类委托:先让父类加载器加载这个类。只有父类加载器无法加载此类自己才加载。

缓存机制:缓存机制会保证所有加载过的class都会被缓存。当程序加载某个类的时候,会先去缓存区寻找,如果不存在,才会去加载对应的类。会造成修改Class后,必须重启

双亲委派加载过程:如果一个类加载器收到了类加载的请求。首先不会自己去加载这个类。而是把这个请求委派给父类加载器去完成。不断地向上抛,直到最顶层的启动类加载器。当父类无法完成加载才会让子类去加载。

加载过程 : 自定义类加载器 -》 应用程序加载器 -》 扩展类加载器 -》 启动类加载器

应用程序加载器: 负责加载用户类路径上的类

扩展类加载器: 加载lib里的类 比如java.*开头的

启动类加载器:加载javahome/lib库中的类

为什么要使用双亲委派模型?

如果不去最顶层这样一层层加载。假如用户自己定义一个Object类。或者是String又或者是Integer。编译的时候,出现了多份同样的字节码,直接破坏了一致性,程序的安全收到极大影响。

打破双亲委派的方法

继承classLoader类,重写loadClass和findClass方法