Jvm

136 阅读10分钟

jvm

一:典型的垃圾回收器

1. Serial/Serial Old收集器 是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程(STW)。Serial收集器是针对新生代的收集器,采用的是Copying(复制)算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact(标记整理)算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

2. ParNew收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3. Parallel Scavenge收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4. Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5. CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

6. G1收集器 是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

2:方法区存放什么?

它存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。

3:jvm内存模型

图片.png

栈(Stack): 也叫方法栈,是线程私有的,线程在执行每个方法时,都会创建一个栈阵,用来存储局部变量表,操作栈、动态链接,方法出口等信息,调用方法时执行入栈,方法返回时执行出栈。

本地方法栈: 与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行JAVA方法时,使用栈,执行native方法时,使用本地方法栈。

程序计数栈: 保存当前线程所执行字节码的位置,每个线程工作时,都有一个独立的计数器,程序计数器只为执行Java程序服务,执行native方法时,程序计数器为空。

这三部分都是线程独占的。

堆: 是JVM管理中最大的一块。堆被所有的线程共享,目的是为了存放对象的实例。几乎所有的对象实例都会放在这里。当堆内存没有可用的空间时,会抛出OOM异常(out of memory的简称,称之为内存溢出)。根据对象的存活周期不同,JVM把内存进行分代管理,由垃圾回收器来进行对象的回收管理。

方法区: 也是各个内存共享的区域,又叫非堆区,用于存储已被虚拟机加载的类信息、常量、静态常量。jdk1.7的永久代就是方法区中的一种实现。

4:对象头

图片.png

5:字节大小

图片.png

6:jvm 地址映射

图片.png 采用指针压缩技术!!

4个字节,32位,可以表示232 个地址,如果这个地址是真实内存地址的话,那么由于CPU寻址的最小单位是byte,也就是 232 byte = 4GB。

如果内存地址是指向 bit的话,32位的最大寻址范围其实是 512MB,但是由于内存里,将8bit为一组划分,所以内存地址就其实是指向的8bit为一组的byte地址,所以32位可以表示的容量就扩充了8倍,就变成了4GB。

4字节,8位最大表示4GB内存。那么Java是怎么做到 4个字节表示32GB呢?怎有扩大了8倍???

这就要使用到之前提到的Java的对齐填充机制了。

Java的8字节对齐填充,就像是内存的8bit为一组,变为1byte一样。

这里的压缩指针,不是真实的操作系统内存地址,而是Java进行8byte映射之后的地址,所以也相对于操作系统的指针有进行的8倍的扩容。

JVM就将堆内存进行了块划分,以8字节为最小单位进行划分。

将java堆内存进行8字节划分

java对象的指针地址就可以不用存对象的真实的64位地址了,而是可以存一个映射地址编号。

7:指针压缩

哪些信息会被压缩? 1.对象的全局静态变量(即类属性) 2.对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节 3.对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节 4.对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

哪些信息不会被压缩? 1.指向非Heap的对象指针 2.局部变量、传参、返回值、NULL指针

8:压缩指针32g指针失效问题

因为寄存器中3的32次方只能寻址到32g左右(不是准确的32g,有可能在31g就发生指压缩失效),所以当你的内存超过32g时,jvm就默认停用压缩指针,用64位寻址来操作,这样可以保证能寻址到你的所有内存,但这样所有的对象都会变大,实际上未开启开启后的比较,40g的对象存储个数比不上30g的存储个数

Java内存模型(JMM)

调用栈和本地变量存放在线程栈上,对象存放在堆上。 (分为工作内存和主内存)(成员变量是线程不安全的)voilet

图片.png

图片.png

  • 一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。
  • 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  • 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  • 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  • 静态成员变量跟随着类定义一起也存放在堆上。
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

9:volatile关键字的作用

图片.png 如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据,例如:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故。

此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

图片.png 假如说当前有一个cpu去主内存拿到一个变量x的值初始为1,放到自己的工作内存中。此时它的状态就是独享状态E,然后此时另外一个cpu也拿到了这个x的值,放到自己的工作内存中。此时之前那个cpu会不断地监听内存总线,发现这个x有多个cpu在获取,那么这个时候这两个cpu所获得的x的值的状态就都是共享状态S。然后第一个cpu将自己工作内存中x的值带入到自己的ALU计算单元去进行计算,返回来x的值变为2,接着会告诉给内存总线,将此时自己的x的状态置为修改状态M。而另一个cpu此时也会去不断的监听内存总线,发现这个x已经有别的cpu将其置为了修改状态,所以自己内部的x的状态会被置为无效状态I,等待第一个cpu将修改后的值刷回到主内存后,重新去获取新的值。这个谁先改变x的值可能是同一时刻进行修改的,此时cpu就会通过底层硬件在同一个指令周期内进行裁决,裁决是谁进行修改的,就置为修改状态,而另一个就置为无效状态,被丢弃或者是被覆盖(有争论)。

当然,MESI也会有失效的时候,缓存的最小单元是缓存行,如果当前的共享数据的长度超过一个缓存行的长度的时候,就会使MESI协议失败,此时的话就会触发总线加锁的机制,第一个线程cpu拿到这个x的时候,其他的线程都不允许去获取这个x的值。

2 内存屏障

volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。具体的会分为读读、读写、写读、写写屏障这四种,同时它也会有一些插入屏障点的策略,下面是JMM基于保守策略的内存屏障点插入策略:

图片.png

3:单线程安全问题

class reorExample{

int a =0;

boolean flag =false;

}    

punlic void w(){

a = 1; //1 (此处涉及指令重排序) (需设置内存屏障)

***

flag = ture; //2

}

public void reader(){

if(flag){ //3

int i =a*a; //4

}

}