jvm 运行时内存区域?
线程私有的:
- 程序计数器
- 虚拟机 栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存(非运行时数据区域的一部分)
JDK8 将方法区异常了由元空间取代。
程序计数器
程序计数器简介?
- 可以看作是当前线程所执行的字节码的行号指示器。字节码解释器通过这个计数器来选取下一条乣执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
- 程序计数器是唯一不糊出现
OutOfMemoryError
异常的区域。
程序计数器的主要作用?
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,记录当前线程执行的位置,方便线程恢复执行后继续执行。
java 虚拟机栈
java虚拟机栈介绍?
java虚拟机栈是线程私有的,它的生命周期和线程相同。它描述的是java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
栈是由一个个栈帧组成,栈帧中拥有局部变量表、操作数栈、动态链接、方法出口信息。
局部变量表主要存放的数据类型?
- 各种基本数据类型的值。
- 对象引用(可能是指向对象的指正或者是指向一个代表对象的句柄)。
java 虚拟机栈会抛出哪两种异常?
StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError:Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
java 的方法是如何调用的?
java 虚拟机栈类似数据结构的栈,栈中主要保存的是栈帧,每一次的方法调用都是一个栈帧被压栈的过程,每个方法调用结束,都会有一个栈帧弹出。java 的返回方式只有 return 或者抛出异常,不管哪种,都会导致栈帧弹出。
本地方法栈
本地方法栈简介?
本地方法栈和虚拟机栈非常类似,只是虚拟机栈是为 java 方法调用服务,儿本地方法栈是为虚拟机使用 native 方法服务的。在 Hotspot 中本地方法栈和虚拟机栈是合二为一的。
本地方法执行时,也会在本地方法栈中创建一个栈帧,用以保存本地方法的局部变量表、操作数栈、动态链接、出口信息。
本地方法执行完成后也会出栈。
本地方法调用也会抛出 StackOverFlowError、OutOfMemoryError。
堆
堆内存简介?
堆是Java 虚拟机所管理的内存中最大的一块,堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
因为 JDK 1.7默认开启的逃逸分析,在方法中引用的对象如果没有其他引用,对象可以直接在栈中创建。
堆也是 java 垃圾收集器管理的主要区域。
逃逸分析?
从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
堆内存分区?
堆内存通常被分为:新生代、老年代、永久带。JDK8之后,方法区(Hotspot的永久代)被移除,由元空间取代。
永久代和方法区的关系?
方法区是 jvm 的规范,永久代是Hotspot对方法区的实现。所以说在Hotspot中,方法区(永久代)是堆内存中的。
堆内存中的新生代?
堆内存中的新生代又分为三块, Eden 区、两个 Survivor 区。两个 Survivor 区又叫 from 和 to,或者叫 s0 和s1。在 s0 和s1 中的对象,每经历一次新生代垃圾回收,年龄增加1,当年龄达到15岁,会晋升到老年代中。晋升老年代的年龄可以通过 -XX:MaxTenuringThreshold (最大年龄阈值)设置。
堆内存中会抛出的异常?
对内存中主要抛出 OutOfMemoryError 内存溢出错误。内存异常又分为两种,java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
,当gc时间长,且回收内存少时,报这个错误。
java.lang.OutOfMemoryError: Java heap space
,当堆内存不足以存放新创建的对象,就会报这个错。可以通过 -Xmx 控制最大堆内存大小,不过这个参数受制于物理内存的大小。
方法区
方法区简介?
方法区也是属于各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、运行时常量池、静态变量、以及编译器编译后的代码等数据。java 虚拟机规范把方法区描述为堆的一个逻辑部分,不过方法区却有一个非堆的别名。
JDK8 之前永久代没有被移除,可以用下面两个参数指定方法区的初始大小和最大大小。
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小
JDK8 之后可以用下面两个参数设置元空间的初始值和最大值。
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
为什么要用元空间替换永久代呢?
- 永久代会使用 JVM 的固定内存空间,容易发生溢出。而元空间直接使用直接内存,不受 JVM 固定内存空间控制。元空间溢出抛出
java.lang.OutOfMemoryError: MetaSpace
. - 在 JDK8中,合并 Hotspot 和 JRockit 时,JRockit 从来都没有叫永久代的地方,所以也就没有必要设置永久代了。
方法区的运行时常量池?
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)。
运行时常量池也会抛出 OutOfMemoryError 错误。
JDK7之前,运行时常量池包含字符串常量池,存放在方法区,此时 hotspot 虚拟机对方法区的实现为永久代。
JDK7 字符串常量池放到了堆中,运行时常量池还在方法区中。
JDK8 移除了方法区,则运行时常量池随着类文件元信息移到了元空间。
直接内存?
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO,使用 Native 函数库直接分配堆外内存,然后通过 DirectByteBuffer 对象作为这块内存的引用进行操作,避免在 Java 堆和 Native 堆之间来回复制数据,从而提高性能。
本机直接内存的分配不会受到 Java 堆的限制,但是会受到物理机总内存大小以及处理器寻址空间的限制。
java 对象创建
对象创建过程?
- 类加载检查
- 分配内存
- 初始化零值
- 设置对象头
- 执行init方法
类加载检查过程?
虚拟机遇到 new 指令时,首先检查常量池中是否有这类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析、初始化过,如果没有,先执行内加载过程。
分配内存?
在类加载检查通过后,便可以确定对象的所需要内存大小,接下来虚拟机将在堆内存中,为对象分配一块确定大小的内存空间。
内存分配的方式?
内存分配方式有指针碰撞和空闲列表两种,选择哪种分配方式由堆内存是否规整决定,堆内存是否规整又由于所采用的垃圾收集器是否带有压缩功能决定。收集器(CMS)。
当使用标记-清楚算法时,内存不规整,会使用空闲列表法,空闲列表记录可以使用的内存空间,进行分配。当使用标记-整理算法时,内存规整,会使用指针碰撞法,指针只需要沿着没有用过的内存区域移动对象大小的位置即可。收集器(Serial、ParNew)。
初始化零值?
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头?
初始化零值后,虚拟机会对对象头进行必要的设置,比如对象的类信息,哈希码,对象的分代年龄,以及是否启用偏向锁等。
执行init方法?
在完成设置对象头后,从虚拟机的视角看,对象已经创建完成。但是从java程序视角看,对象创建才开始,执行 init 方法,将所有的零值设置为初始值。对象才算初始化完成。
对象的访问定位?
java 程序通过栈上的引用来操作堆上的对象。对对象的访问方式由虚拟机实现而定,一般有使用句柄和直接指针两种:
使用句柄:需要在堆中开辟一块空间存储句柄,栈中的引用指向句柄的地址,句柄指向对象和对象在方法区中的类信息。
使用指针:栈中的引用直接指向堆中的对象,由堆类考虑如何分配内存地址,方便通过对象定位到对象在方法区中的类信息。
字符串常量池相关
- 直接拼接和引用拼接
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing"; //常量池中的对象(编译器优化)
String str4 = str1 + str2; //在堆上创建的新的对象 (实际上是StringBuilder调用append方法和toString方法实现的)
String str5 = "string"; //常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
- 在编译器有确切的值,可以被优化
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
String str2 = new String("abcd"); 创建步骤?会创建几个对象?
- 在堆中创建一个字符串对象。
- 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
- 如果没有的话,需要在字符串常量池中也创建一个值相等的字符串常量。如果有的话,就直接返回堆中的字符串实例对象地址。
String 的 intern 方法?
直接使用双引号声明出来的
String
对象会直接存储在常量池中。-
使用
String
提供的intern()
方法也有同样的效果,如果常量池中存这个对象,直接返回常量池中该对象的引用。如果不存在:- JDK7 之前,会在常量池创建相同的对象,并返回常量池的字符串的引用。
- JDK7 之后,是直接将堆中对象的引用地址放到常量池中,减少不必要的内存开销。
String s1 = "Javatpoint"; String s2 = s1.intern(); // 在常量池中存在,直接返回常量池的引用 String s3 = new String("Javatpoint"); // 新建堆上对象,返回堆上对象的引用 String s4 = s3.intern(); // 在常量池中存在,直接返回常量池的引用 System.out.println(s1==s2); // True System.out.println(s1==s3); // False System.out.println(s1==s4); // True System.out.println(s2==s3); // False System.out.println(s2==s4); // True System.out.println(s3==s4); // False
8中基本类型的包装类和常量池技术?
Byte, Short, Integer, Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True Or False。
两种浮点数类型的包装类 Float, Double 并没有实现常量池技术。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
自动装箱会创建新对象,导致 == 比较返回 false。
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
包装类型进行计算会自动拆箱,使用 == 比较返回 true。
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println(i4 == i5);// false
System.out.println(i4 == i5 + i6);// true
System.out.println(40 == i5 + i6);// true