JVM以前觉得是个很高深的内容,觉得自己是个菜鸡不用学,只要在业务上会Ctrl C +V就行了。现在工作了几年了如果还是只会这,我不被淘汰谁被淘汰?其实JVM也可以解决工作中的问题,之前业务里面写递归,导致栈溢出,看着报错信息一脸懵逼,无从下手。我做了什么?会导致栈溢出?不仅如此,现在的面试造火箭也是必问的内容。之前招银的面试问了一个,一个对象的创建,它的对象头里面都包含什么内容????一脸懵逼???于是下定决心,JVM必须搞明白。找个PDF(深入理解java虚拟机)开始读起。写博客不为分享,只为自己纯手工的写一遍大神的解释,代码走一遍,加深印象。方便以后的笔记查看。
其实了解JVM的原因就是:了解代码运行的原理,写更好的代码;遇到问题可以知道为何出错,怎么解决;最后就是应对面试造火箭。一般了解的虚拟机都是Hotspot
1、java虚拟机主要分为五大模块,类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块(来自百度);
虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
主要类型有:程序计数器、虚拟机栈、本地方法栈、方法区、堆
程序计数器:分配的一块较小的内存空间,当前运行线程的字节码的行号指示器;各线程独立存储
虚拟机栈:线程私有,每个方法在执行的同时都会创建一个栈帧用户存储方法内的局部变量表,操作数栈,动态链接,方法出口灯信息。方法的执行对应着栈帧在虚拟机栈内的入栈和出栈过程。栈里面存放着基本数据类型和对象的引用,栈的大小由:-Xss控制,栈帧大小默认好像是为1M。一般不会对这个进行改动
本地方法栈:本地方法栈内保存着native方法的信息,当JVM创建的线程调用native方法后,JVM不会在虚拟机栈中创建栈帧,jvm会动态的链接并调用对应native方法。
堆:几乎的对象都被分配在堆中,同时也是垃圾回收的主要区域,堆的大小控制参数主要有:
-Xms:堆的最小值;-Xmx:堆的最大值;-Xmn:新生代的大小;-XX:NewSize:新生代的最小值;-XX:MaxNewSize:新生代最大值
方法区:存储已经被JVM加载的类信息、常量、静态变量等数据。内存大小通过-XX:MetaspaceSzie 和 -XX:MaxMetaspzceSize控制 ;JDK8,就没有方法去这个说法了,转为元空间(metaspace),运行时常量池在堆中。
上述的内存区域:线程间共享的内存区有:方法区和堆;线程私有内存区有:程序计数器、虚拟机栈、本地方法栈。
堆和栈的区别
栈:以栈帧的方式存储方法调用的过程,并且存储方法调用过程中基本数据类型的变量以及对象的引用,上述所占内存都是分配在栈中,变量在方法调用完之后会自动释放,基本不用考虑内存回收;每个线程都有一个栈内存,栈内存储的变量只能在对应的线程内可见,栈内存属于线程的私有内存;
堆:堆内存用于存储java中创建的对象。包括:成员变量,局部变量,类变量,他们指向的对象都是存储在堆内存中。(栈上分配除外)。堆内存的设置远大于栈内存
提及栈上分配:JVM提供的优化技术。解释:对于线程私有的对象,将他打散分配在栈上,而不用分配在堆上。私有对象跟着方法调用自动销毁,不需要进行垃圾回收,从而提供性能。依赖的技术是:逃逸分析,JDK8默认开启。逃逸分析的用于判定对象的作用于是否会逃逸出方法体。
栈上分配测试:
开启栈上分配VM参数设置:-server -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+PrintGC -Xms10m -Xms10m
关闭栈上分配VM参数设置:-server -XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:+PrintGC -Xms10m -Xms10m
-server选择jvm的运行模式,只有server模式才能开启逃逸分析,
DoEscapeAnalysis :+表示开启逃逸分析,-表示关闭。
EliminateAllocations: 标量替换:是否运行将对象打散分配在栈上面;开启了标量替换,person的2个字段有可能被视为独立的变量在栈上分配
PrintGC :打印GC信息
-Xms:堆的最小值
-Mms:堆的最大值
package org.example.notes1;
import lombok.Data;
/**
* @author: w
* @date: 2020/9/12
* @description:
*/
public class Stack {
public static final Integer size = 100000000;
@Data
private static class Person{
private String name;
private String age;
}
public static void testWithStackAlloc(){
Person person = new Person();
//todo 业务操作 person.setAge("1");
}
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
for (int i=0;i<size;i++){
testWithStackAlloc();
}
long time2 = System.currentTimeMillis() - time1;
System.out.println("调用过程耗时:"+time2);
}
}
根据执行日志显示:调用过程耗时,开启时,在几毫秒;关闭时在1300毫秒左右;
当执行一条new指令时,是如何创建对象?
1、首先检查这个指令的参数是否能在常量池中定位到类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过了。如果没有上述内容,则执行相应的类加载过程(文档表示是在后续详细解释)
2、在类加载检查通过后,接下来虚拟机将为新生对象分配内存。在内存分配这块存在2个需要探讨的问题:内存分配的方式和内存分配频繁的情况下的线程安全问题
在内存分配中有2种方案:指针碰撞(java堆内存是绝对规整的,所有用过的内存都放在一边,空闲的聂村放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离)、空闲列表(java堆内存是不规整的,已使用和空闲的内存相互交错,无法进行指针碰撞,虚拟机就需要维护一个列表,记录哪些内存块可以使用)。java堆内存是否规整由垃圾收集器是否带有压缩整理功能决定。在使用Serial/ParNew等待Compact过程的收集器,系统采用的分配算法是指针碰撞,而使用CMS基于Mark-Sweep算法的收集器时是使用的空闲列表。
内存分配频繁的情况下的线程安全问题:虚拟机提供了2种解决方案:CAS(compareAndSwap)和通过-XX:+UseTLAB开启预分配内存。
3、内存分配完成后,jvm需要将分配到的内存空间都初始化为零值(不包括对象头)。这个操作保证对象的实例字段在java中可以不用赋初始值就直接使用,程序能访问到这些字段的及数据类型所对应的零值。
4、对对象进行必要设置,设置对象属于哪个类的实例,如何才能找到类的元数据信息,对象的哈希吗、对象的GC分代年龄。将这些信息放在对象的对象头之中 。
5、经过上述操作,在虚拟机的视角一个新的对象已经产生,而java中对象创建才刚刚开始,需要调用init进行对象初始化。
对象的内存布局
hotspot虚拟机中,对象在内存中存储的布局分为3个区域:对象头、实例数据和对齐填充。
对象头:对象头包括了2部分信息,第一个部分用于存储对象自身的运行时数据,包括:哈希码,对象的GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳。对象头根据对象的不同状态存储不同的内容
图片引用于(深入理解java虚拟机);对象头的另外一个部分是类型指针,就是对象指向它的类元数据的指针,通过这个指针确定对象是哪个类的实例。如果是一个java数组,在对象头中海必须有一块用于记录数组长度的数据,由于虚拟机可以通过普通的java对象的元数据信息确定对象的大小,但是从数组的元数据中无法确定数组的大小。
实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
对齐填充:当对象实例数据部分没有对齐时,需要通过对齐填充来补全。
对象的访问定位
对象的访问方式主要有 使用句柄和直接指针2种。
句柄访问:java堆中可能将会划分出一块内存来作为句柄池(2版和3版描述不一样,2中没有可能2个字眼,以第三版描述为准),reference中存储的就是对象的句柄地址,句柄中包含对象实例数据和类型数据各自具体的地址信息
指针访问:java堆中的内存布局必须考虑如何防止访问类型数据的相关信息,reference中存的就是对象地址,如果只是访问对象本身的话,就不需要多一次通过句柄访问开销。(hotspot采用)。因为使用直接指针来访问的最大好处就是速度快,节省指针定位的时间开销。