JDK:Java开发工具
JRE:Java运行时环境
JVM:Java虚拟机
基本数据类型的存放位置:
- 在方法中声明的变量是局部变量,变量名和值都是存放在虚拟机栈中的局部变量表;
- 在类中声明的变量是全局变量,变量名和值都是存放在堆内存中(因为全局变量不会随着某个方法执行结束而销毁)。
引用数据类型的存放位置
- 在方法中,所声明的局部变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
- 在类中,引用变量名和对应的对象仍然存储在相应的堆中。
为什么不把基本类型放在堆中?(为什么不把引用数据类型放在栈中?)
堆比栈要大,但栈比堆的运算速度要快。
将复杂数据类型放在堆中的是为了不影响栈的效率,而是通过引用的方式去堆中查找。
八大基本数据类型的大小创建时就已经确立大小,三大引用类型创建时无法确定大小。
通过什么参数来设置栈的大小?
-Xss1M(设置为1M大小)
设置的栈空间值过大,会导致系统可以用于创建线程的数量减少。
一般一个进程中通常3000-5000个线程。
如何设置堆内存的大小
-Xms:用于表示堆的起始内存;
-Xmx:用于表示堆的最大内存;
超过 -Xmx 所指定的最大内存时,会抛出OOM异常;
通常会将 -Xms 和 -Xmx 两个参数配置相同的值,目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
如何理解栈管运行,堆管存储
在栈中,一个方法对应一个栈帧,一个栈帧中主要包括:操作数栈和局部变量表。
栈帧里的指令(操作数栈)影响着局部变量表里的变量,从而来操作或修改堆空间里的数据。
1.JVM内存区域(运行时数据区)
JVM将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区(元空间)。
程序计数器
线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
运行最快的存储区域,唯一一个没有 OOM 的区域。
虚拟机栈
线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法返回地址等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;栈不存在GC,但会有 OOM
(1)局部变量表:是一个数组,大小在编译成字节码时就确定(double和Long会占据两个slot),主要用于存储形参和方法体内的局部变量。this变量不在静态方法的局部变量表中,但是如果调用非静态的方法或者this.变量,会出现一个this来占据一个slot。基本数据类型直接存储;引用类型存放的是地址,地址指向堆空间中对象的实际存储位置。
(2)操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
栈顶缓存:将栈顶元素全部缓存在物理CPU的寄存器中,降低对内存的读写次数。
栈的深度在字节码时期就已经确定了:eg: // 宽化类型转换 long m = 12L; // 占两个slot int n = 800; // 占一个slot m = m*n; // 存在宽化类型转换,将n转换成long,也是占两个slot。 // 栈的深度为4
byte、short、char、boolean:都以int型来保存,占一个slot;long占两个slot。
eg:// 计算两数的和 int i = 15; int j = 8; int k = i + j; // 字节码为: 0: bipush 15 // 将15压入操作数栈 2: istore_1 // 从操作数栈中弹出15,装载到局部变量表 3: bipush 8 // 将8压入操作数栈 5: istore_2 // 从操作数栈中弹出8,装载到局部变量表 6: iload_1 // 将15放入操作数栈 7: iload_2 // 将8放入操作数栈 8: iadd // 从操作数栈中弹出15和8,进行加法运算,得到结果23存入到操作数栈中 9: istore_3 // 将操作数栈中的结果23存入到局部变量表 10: return // 结束 // 栈的深度为2
本地方法栈
线程私有的,保存的是native方法的信息,一个本地方法就是一个Java调用非Java代码的接口。当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
堆
java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
默认最大值:如果内存少于192M,堆为内存大小的一半;如果内存大于等于1G,堆为内存大小的1/4;
默认最小值:最少不得少于8M;如果内存大于等于1G,堆为内存大小1/64;
结构:
年轻代(占 1/3):Eden区(占 8/10) + S0(占 1/10) + S1(占 1/10)
老年代(占 2/3):方法区
存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元空间替代了,原方法区被分成两部分:
1.加载的类的信息
2.运行时常量池;加载的类信息被保存在元空间中,运行时常量池保存在堆中。
2.如何判断对象已经死亡?
可达性分析法
这个算法的基本思想就是通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,就证明此对象是不可用的。
可以作为GC Roots的对象包括:虚拟机栈中引用对象、本地方法中引用对象、方法区中类静态属性引用的对象、方法区中常量引用对象。引用计数法
一般不会用这个算法来管理内存,最主要的原因是它很难解决对象之间相互循环利用的问题。
每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
3.堆和栈有什么区别
四个角度:
①GC;OOM
②栈、堆执行效率
③内存大小;数据结构
④栈管运行;堆管存储
申请方式
栈:由系统自动分配。
堆:需要自己申请,并指明大小。对于Java需要手动new Object()的形式开辟。申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:操作系统中有个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。申请大小的限制
栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。意思就是说栈顶的地址和栈的最大容量是系统预先规定好的。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。所以,堆获得的空间比较灵活,也比较大。申请效率的比较
栈:由系统自动分配,速度较快。
堆:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址, 然后是函数的各个参数。静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序就在这个地方继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容可以自己安排。
4.简述强、软、弱、虚引用
强引用
如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止。软引用
如果内存空间足够,垃圾回收器就不会它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。弱引用
相对于软引用,弱引用关联的对象只能生存到下一次了垃圾回收之前。虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软、弱引用的区别:虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。这样就可以通过判断引用队列中是否已经加入了虚引用来了解被引用的对象是否将要被垃圾回收器回收,可以做一些在回收之前的必要行动。
5.垃圾收集算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,但它会带来两个明显的问题:效率问题和空间问题。(标记清除后会产生大量不连续的碎片)
复制算法(强行设置边界)
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。但是,为了收集垃圾,将内存使用量降低为一半,成本较高。
标记-整理算法(整理出边界)
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。
分代收集算法
一般将Java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,所以我们可以选择“标记-清理”或“标记-整理”算法进行垃圾收集。
6.常见的垃圾收集器有哪些?
Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1
Serial
单线程收集器,不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程,直到它收集结束。
Serial Old
Serial收集器的老年代版本,它同样是一个单线程收集器。
ParNew
其实就是Serial收集器的多线程版本,随着CPU增加,可以显示出优势。(并行)
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。Parallel Scavenge
使用复制算法,并行的多线程收集器。ParNew类似,但是侧重点是吞吐量(CPU运行用户代码时间/CPU消耗的总时间)。可以设置参数来调整最大垃圾收集停顿时间和吞吐量大小。
Parallel Old
是Parallel Scanvenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源配合的场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器。
CMS
老年代收集器,采用“标记-清除”算法,是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。整个过程有四个步骤:初始标记、并发标记、重新标记和并发清除。(Spark Streaming采用这个,最少停顿)
①初始标记:暂停所有的其他线程,并记录下直接与GC root相连的对象,速度很快;
②并发标记:同时开启GC和用户线程,从GC root继续向下进行标记,但是用户线程会继续更新对象的引用域;
③重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
④并发清除:开启用户线程,同时GC线程开始对为标记的区域做清扫。
优点:并发清除、低延迟。
缺点:1.采用标记-清除,会产生碎片;
2.无法处理浮动垃圾,在并发清除的过程中,用户线程还在继续执行,还会不断垃圾,这部分只能等到下次GC时处理;
3.对CPU特别敏感,由于并发标记和并发清除是和用户线程并发执行,所以会导致用户线程并发执行,所以会导致用户程序变慢,总的吞吐量减低。
G1
唯一一个同时可以用于年轻代和老年代的垃圾收集器,采用标记-整理算法,避免碎片。该收集器,将堆内存分为不同大小相等的region,并维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的region,把内存化整为零,但是由于引用关系的存在,仍然存在如何避免全局扫描问题。这里采用每一个region用一个remembered Set进行记录引用关系,避免可达性分析阶段的全区域垃圾扫描。
特点:并发性强、分代收集、标记整理进行空间整合,可以预测停顿时间。
①初始标记:
②并发标记:
③最终标记:将并发标记阶段对象变化记录在线程Remenbered Set Logs里面,最终把Remenbered Set Logs里面的数据合并到Remembered Set中。这一阶段需要停顿线程,但是可并行执行。
④筛选回收:对每一个region的价值和成本进行筛选,根据用户期望的GC停顿时间,得到最好的回收方案并回收。
7.吞吐量优先和响应优先得垃圾收集器如何选择?
8.内存分配与回收策略。(对象何时进行老年代?)
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,JVM 将发起一次 Minor GC。
(1)Minor GC 和 Major GC 有什么不同?
Minor GC:指发生在新生代的垃圾收集工作,Minor GC 非常频繁,回收速度一般也比较块。
Major GC:指发生在老年代的GC。
Full GC:清理整个空间包括年轻代和老年代。
(2)什么时候对象进行老年代?
大对象(需要大量内存连续的)直接进行老年代。
空间分配担保:
① 新生成的对象放在 Eden,当 Eden 被填满后,垃圾回收后存活的对象复制放入 From(其中一个 survivor);
② 当 From 满了,回收后存活的对象被复制到 To 区域(另一个 survivor),Eden 存活的也直接进入 To 区域,原 From 区域被清空;
③ 当 To 被填满后,如果里面存活的对象还活着,直接进行老年代(空间分配担保)。年龄判定:
① 年龄计数器会为对象记录年龄,每次经过一次 GC 仍然存活的,年龄 +1,当超过设定的值,直接进入老年代。
② 或者动态对象年龄判定,当如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。
(3)空间分配担保
- 安全的 Minor GC:老年代中最大可用的连续空间大于新生代所有对象的空间;
- 冒险的 Major GC:老年代中最大可用的连续空间大于历代晋升到老年代的平均水平且允许担保失败;如果小于平均值,则直接进行 Full GC,让老年代腾出空间。
9.虚拟机性能监控和故障处理工具
10.Class 类文件的结构
11.类加载机制
- 显式加载:
在代码中调用ClassLoader加载class对象,如:Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。 - 隐式加载:通过JVM自动加载到内存中,如:System.out.println(),自动加载标准输出流。
JVM 把描述类的数据从 class 文件加载到内存,主要有三个部分:
加载
(1)通过一个类的全限定名来获取定义此类的二进制字节流;
(2)将这个字节流所代表的静态存储结构转化为方法区(元空间)的运行时数据结构;
(3)类将.class文件加载到元空间后,在堆中创建一个Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
类的引用(栈)-> 类的实例(堆)-> 类的数据结构/类模板(方法区/元空间)
Class类的构造方法是私有的,只有 JVM 能够创建。
数据类的加载:数组类本身不是由类加载器负责创建的,而是由 JVM 在运行时根据需要而直接创建的(比如运行时才动态分配数组长度),但数组的元素类型仍然需要依靠类加载器去创建。
int[] A // 基本数据类型由虚拟机预先定义 String[] A // 引用数据类型则需要进行类的加载 Object[] A
- 创建数组类的过程:
(1)如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型;
(2)JVM 使用指定的元素类型和数组维度来创建新的数组类
(3)如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。
连接
验证
确保Class文件的字节流中包含的信息符合要求,不会危害虚拟机。
如果在这个阶段无法通过检查,JVM也不会正确装载这个类;
但是如果通过了这个阶段的检查,也不能说明这个类是完全没用问题的。准备
为类的静态变量分配内存,并将其初始化为默认值,这些变量所使用的内存都将在方法区中进行分配。
PS:
(1)这里不包含基本数据类型的字段用 static final 修饰的情况,因为 final 在编译的时候就会分配了,准备阶段会显式赋值。
(2)这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
(3)在这个阶段并不会像初始化阶段那样会有初始化或者代码被执行。解析
符号引用:用一组符号来表示所引用的目标,与虚拟机实现的内存布局无关。
直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,表示目标已经在内存里。
将常量池内的符号引用替换为直接引用的过程。
主要对类或接口、字段、类方法、接口方法的解析,主要是静态链接,方法主要是静态方法和私有方法。
初始化
开始真正执行定义的 Java 代码。执行 clinit() 方法,该方***收集所有类变量的赋值动作和静态语句合并产生,首先会执行父类。
clinit():只有在给类中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。
init():一定会出现在Class的method表中。
- 哪些类不会生成 clinit() 方法?
(1)一个类中并没用声明任何的类变量,也没有静态代码块时;
(2)一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时;
(3)一个类中包含 static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式。public class test { public int num = 1; // 对于非静态的字段。不管是否进行了显示赋值,都不会生成<clinit>()方法 public static int num1; // 静态的字段,没有显式的赋值,不会生成<clinit>()方法 public static final int num2 = 1; // 对于static final,不管是否进行了显式赋值,都不会生成<clinit>()方法。 }
全局常量:static + final 修饰的成员变量。
总结:
什么时候再链接阶段的准备环节:给此全局变量赋的值市字面量或常量,不涉及到方法或构造器的调用。
除此之外,都是在初始化环节赋值的。
public class test { public static int a = 1; // 在初始化阶段赋值 public static final int INT_CONSTANT = 10; // 在链接阶段的准备环节赋值 public static Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化阶段赋值 public static final Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); // 在初始化阶段赋值。因为Integer.valueOf()属于一个方法的调用 public static final String s0 = "helloworld0"; // 在链接阶段的准备环节赋值 public static final String s1 = new String("helloworld1"); // 在初始化阶段赋值 public static String s2 = "helloworld2"; // 在初始化阶段赋值 public static final int NUM1 = new Random().nextInt(10); // 在初始化阶段赋值 static int a = 9; // 在初始化阶段赋值,因为是显式赋值 static final b = a; // 在初始化阶段赋值,因为是显式赋值 }
12.Class的forName("Java.lang.String") 和 Class的getClassLoader()的loadClass("Java.lang.String") 有什么区别?
- forName("Java.lang.String"):会导致调用的类执行到初始化环节。
- loadClass("Java.lang.String"):只会执行加载环节。
13.哪些情况会触发类的加载(类的加载时期)?
- 创建类的实例,如:new、反射(Class.forName)、克隆、反序列化。
- 访问类的静态变量或静态方法。
- 初始化一个类的子类(会首先初始化子类的父类)。
- 虚拟机启动时,定义main()方法的那个类。
14.类加载器及双亲委派模型:
类加载器主要分两种:启动类加载器、其他类加载器。
启动类加载器(Bootstrap ClassLoader)
也叫“引导类加载器”,
将存放在 \lib 目录下,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。C++编写扩展类加载器(Extension ClassLoader)
加载 \lib\ext 目录中的,或者被 Java.ext.dirs 系统变量所指定的路径中的所有类库。应用程序类加载器(Application ClassLoader)
也叫“系统类加载器”,
因为这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值。
负责加载用户类路径(ClassPath)上所指定的类库。
自定义的类默认使用系统类加载器;
关于数组类型的加载:使用类的加载器与数组元素的类的加载器相同。
除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里的父子关系通常是子类通过组合关系(即在下层加载器中包含着上层加载器的引用)而不是继承关系来复用父加载器的代码。
// 自定义的类默认使用系统类加载器 ClassLoader classLoader1 = Class.forName("com.tiger.test").getClassLoader(); System.out.println(classLoader1); // sun.misc.Launcher$AppClassLoader@xxxx // 关于数组类型的加载:使用类的加载器与数组元素的类的加载器相同。 String[] arrStr = new String[10]; System.out.println(arrStr.getClass().getClassLoader()); // null:属于/lib下面的核心库,所以使用引导类加载器 int[] arr1 = new int[10]; System.out.println(arr1.getClass().getClassLoader()); // null:属于/lib下面的核心库,所以使用引导类加载器 ClassLoaderTest[] arr2 = new ClassLoaderTest[10]; System.out.println(arr2.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@xxxx
双亲委派模型的工作过程
向上委托、向下请求。
当一个类在加载的时候,会先委派它的父加载器去加载,这样一层层的向上委派,直到最顶层的启动类加载器。
如果顶层无法加载(即找不到对应的类),就会一层层的向下查找,直到找到为止。
// 获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@xxxx // 获取扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@xxxx // 获取引导类加载器,获取不到 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // null 因为启动类加载器是用C++写的,所以无法获取。
如果自己写一个 Java.lang.String 会被加载吗?
不能。因为针对 Java.* 开头的类,JVM 的实现中已经保证了必须由 bootstrap 来加载。