参考javaguide(guide哥的原文链接)
JVM内存模型基础和常见面试题总结
说说运行是基本数据区域?
哪些是线程私有的?
- 程序计数器
- 虚拟机栈
- 本地方法栈
那些是线程共享的?
- 堆
- 方法区
- 直接内存
程序计数器的作用?
- 记录线程执行的位置
- 负责指向下一条线程需要执行的字节码
- 不会出现OutMemoryError
java虚拟机栈的作用?
- 负责存入线程处理的方法栈帧
- 各种局部变量
本地方法栈?
- 执行native方法
堆?
- 最大的内存区域,存放对象
- 划分为老年代、新生代(幸存区、伊甸区)
- jdk8之后消除了永久代,变成元空间
- 对象一开始存在于伊甸区,每次进入幸存区那么年龄就是+1,如果达到阈值那么就会进入老年区,阈值可以通过-XX:MaxTenuringThreshold设置,如果幸存区的某个年龄大小超过幸存区的一半,那么就会以这个年龄或者是MaxTenuringThreshold的最小值作为阈值
堆中出现不同的溢出问题?
- java.lang.OutOfMemoryError: GC Overhead Limit Exceeded其实就是jvm垃圾回收次数多,但是每次都没怎么回收到东西
- java.lang.OutOfMemoryError: Java heap space堆内存溢出,创建对象太多
方法区的作用?
- 存储各种类信息、常量和静态变量还有即时编译器的代码优化
- 非堆
- 方法区其实就是一种规范,永久代和元空间就是方法区的是实现方式
为什么后期使用元空间?
- 永久代是存在于java内存,那么这个时候是受jvm控制大小,容易发生溢出问题
- 但是元空间是系统内存,相对出现内存溢出的机会很小,元空间的系统内存大小可以装下更多的类元信息
运行时常量池?
- class文件不仅仅有类的元信息(接口,属性,版本,字段)还有常量池表
- 用于存字面量和符号引用
- 可以通过intern()把变量放入常量池
字符串常量池的位置?
- jdk1.7以前存放到方法区,也就是永久代里面(hotspot)
- jdk1.7拿到了堆,其它放到了方法区(hotspot)
- jdk1.8就是把方法区通过元空间来实现,字符串常量池在堆,运行时常量池在方法区
直接内存的作用?
- 引入了NIO类,实现了系统和java堆的一个通道,和缓存区的IO实现,native库能直接分配堆外内存,并且通过把信息传输到堆的DirectByteBuffer作为引用操作,相当于就是系统和java堆的中间缓存减少系统和java堆的来回复制
HotSpot如何创建对象?
对象的创建
1.类加载检查
- 虚拟机遇到一条new指令,首先检查指令能否定位到常量池的这个类的符号引用,并且检查这个符号引用代表的类是否引用过、加载过、初始化过。如果没有就要进行类加载
- 实际上就是检查new的这个类是否加载,没有加载就通过类加载器进行加载
2.分配内存
- 类加载检查之后就是分配内存
- 分配方式有指针碰撞和空闲列表
- 指针碰撞适用场景是堆内存规整,用过的内存和没用过的分开,中间一个分界值指针,指针移动到一个对象的空间就可以了。而且是使用算法标记整理。(SerialOld,ParNew)
- 空闲类表通过表来记录空闲内存,用于内存不规整的情况就是标记清除(CMS)
- 内存分配处理并发问题,通过CAS+重复尝试,如果TLAB(jvm给线程分配的内存)不够的话,那么就再使用CAS+重复尝试来分配新的内存。
为什么会产生并发问题?
- 因为程序多个线程运行可能会导致线程争夺空间。
3.初始化零值
- 分配内存之后初始化对象的属性赋0值
4.设置对象头
设置对象头,实际上就是计算对象hashCode、gc分代年龄、类信息
5.执行init方法
按照程序员的意愿初始化对象,调用构造函数
检查->分配内存->初始化零值->设置对象头->执行init方法
对象的内存布局?
- 对象头(基本信息,或者是指向monitor信息、锁情况)
- 如果是数组还需要记录数组的长度
- 实例数据(有效数据)
- 可以通过-XX:FieldsAllocationStyle来规定变量的分配顺序
- 对齐填充(8字节倍数)
对象的访问位置
- 句柄:java堆中划分一片区域来存句柄池,主要指向对象实例数据和对象类型数据地址。
- 好处就是能够有稳定的句柄池来指向对象实例和对象数据,保证reference不会被回收
- 直接指针:指向对象数据,对象里面又有指向对象类型数据地址
- 如果不访问类的信息,就能节省一次访问时间
- 速度更快
常量池中常考问题?
String 类型的变量和常量做“+”运算时发生了什么
- 实际上就是JIT把他们拼接到了一起。所以最后的str5和str3是相同的。因为他们最后都是放进了常量池
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
对于编译期中可以确定的String?
- jvm会把他们直接存入字符串常量池
常量池的作用?
避免字符串重复创建
什么是常量折叠?
- 就是jvm在编译期帮你把能够拼接和直接显示的字符或者常量存入常量池
final修饰
- Sring s4=str1+str2其实相当于就是new StringBuilder().append(str1).append(str2).toString();在堆中创建对象而不是引用常量池的对象
- final修饰可以看成就是一个常量。所以相加得到的还是常量池的对象
- 下面之所以会出现错误是因为方法必须是在编译之后才能够被解释,也就是说,d和c其实都是在编译之后才能够知道str2长什么样,那么这个时候就只能够在堆上面创建对象c和对象d,并且检查常量池是否存在string,如果不存在那么就再创建一个相等的字符串常量放入常量池,如果有那么就直接返回字符串对象在堆中的地址
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + str2;// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
new String做了什么事?
- 创建字符串对象在堆
- 接着就是检查字符串常量池是否存在对象
- 如果没有那么就创建一个在常量池,如果有就直接返回对象的实例
String str2 = new String("abcd");
String str3 = new String("abcd");
实践部分
堆溢出
- 堆溢出只需要不断给容器创建对象就可以
虚拟机栈溢出
- 最危险的是不断创建线程导致os假死问题(这种是申请内存不足,而且由于线程太多,os处理不过来)
- 还有一种可能就是变量太多或者执行方法导致栈帧太多,线程栈溢出
方法区常量池溢出
- 可以通过不断intern来放入常量去把它挤满
- 出现一个问题
下面代码在jdk6和7分别是什么答案?
- jdk6全部false原因是创建字符串对象先看看常量池有没有,如果没有复制对象过去,并且返回常量池对象的引用很明显就是和堆的不一样(永久代)
- jdk7没有复制,字符串常量池已经搬到堆中,那么只需要查看常量池有没有,有就返回实例对象的引用,没有就创建一个并返回常量池的引用。
public class MyTest {
public static void main(String[] args) {
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}
}