JVM笔记
1. JVM知识体系:
1.1 JVM的位置
运行在操作系统之上
1.2 JVM 体系结构
Java是运行在Java的虚拟机(JVM)中的,但是它是如何运行在JVM中了呢?
我们在IDE中编写的Java源代码被编译器编译成.class的字节码文件。然后由我们得ClassLoader负责将这些class文件给加载到JVM中去执行。
JVM = 类加载机制+内存空间结构+垃圾回收
1.3 三种JVM
- Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)
- BEA JRockit
- IBM 39 VM
我们学习都是:Hotspot
2. 虚拟机类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。这个过程被称作虚拟机的类加载机制。
与那些在编译时需要进行连接的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,一方面带来了性能开销,另一方面也给java提供了极高的扩展性和灵活性。
例如,编写一个面向接口的应用程序,可以等到运行时再指定其 实际的实现类。
jvm是什么时候去加载类的呢?
我们什么时候使用到了这个类,它就去class字节码文件中去加载这个类
注意:数组本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。
2.1 类加载器
通过一个类的全限定名来获取描述此类的二进制字节流
2.2 双亲委派模型
从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
加载过程:先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLo ader 逐层检查,只要某个 Classloader 已加载就视为已加载此类,保证此类只所有 ClassLoader加载一 次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
![img](https://i-blog.csdnimg.cn/blog_migrate/3d156081fd354092c3ead49ce056200b.webp?x-image-process=image/format,png)
![img](https://i-blog.csdnimg.cn/blog_migrate/50ae87678b2256ffeafe503d087332e0.png)
-
Bootstrap classLoader
:启动类加载器,它主要是加载java的核心类。jvm虚拟机环境目录中是有一个lib文件夹的,这个文件下就是java最核心的类库,支撑着java系统的运行。
所以一旦jvm启动,那么首先就会通过启动类加载器去加载lib文件夹下的核心类库。
-
ExtClassLoader
:扩展类加载器,主要负责加载jre/lib/ext目录下的一些扩展的jar。这里面的类就是java运行环境的一些扩展类,这些扩展类就是在jvm启动后,通过扩展类加载器进行加载的。 -
AppClassLoader
:应用程序类加载器,主要负责加载应用程序的主函数类,可以理解成是加载我们写好的java代码的就可以了 -
UserClassLoader
:自定义类加载器,根据一些特殊的需求来自己定义类加载器加载我们的类。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//-----??-----
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// Class Not Found异常就是这么来的
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
2.3 为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:
String.java
。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为JVM启动时就会通过bootstarp类加载器把rt.jar下面的核心类加载进来,所以自己重写的不会被加载),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
2.4 java类加载过程
2.4.1 加载
简单的说,类加载阶段就是由类加载器负责完成以下事情:
- 根据一个类的全限定名来读取此类的二进制字节流
- 将二进制流存储在运行时内存区的方法区
- 在内存中生成一个代表该类的java.lang.Class对象,这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
加载完成之后,原来的二进制字节流就会按照虚拟机所设定的格式存储在方法区中了。
2.4.2 连接
连接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备、解析三个阶段。
(1)验证阶段
校验你加载进来的class文件中的内容是否符合规范,如果不符合规范jvm是无法正常运行的。
验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等
2.准备阶段
给类A分配内存空间,然后给里面的静态变量(static修饰的变量)也分配内存空间,并赋初始值。
3.解析阶段
符号引用替换为直接引用,如果类A继承了类B,那么在初始化类A的时候,如果发现类B还没有初始化,会先初始化类B。
2.4.3 初始化
执行所有静态代码块
准备阶段我们只是给变量i分配了内存空间,并赋值了初始值,但是后边的System.getProperty(“i”)是不会执行的。这部分代码就是在初始化阶段执行的。
public class A {
private static String i=System.getProperty("i");
}
2.5 Java创建对象的过程
Java创建对象包括初始化和实例化两个过程,初始化就是类加载过程,下面是实例化的过程
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
对象的内存布局(结构):对象头、实例数据和对齐填充。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
Java中代码执行的顺序:
3. Java 内存区域
3.1 运行时数据区域
3.1.1 native——>本地方法栈
native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
它在内存区域中专门开辟了一块标记区域: Native Method Stack,本地方法栈,登记native方法
进入本地方法栈
调用本地方法本地接口 JNI (Java Native Interface)
**JNI作用:开拓展Java的使用,融合不同的编程语言为Java所用!**Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
在最终执行的时候,通过JNI加载本地方法库中的方法
例如:Java程序驱动打印机,管理系统,掌握即可,在企业级应用比较少
private native void start0();
3.1.2 程序计数器
每个线程都有一个程序计数器,是线程私有的**,就是一个指针**, 指向方法区中的方法字节码,也就是当前线程正在执行的字节码的地址。 是一个非常小的内存空间,几乎可以忽略不计
编译后的字节码在没有经过实时编译器编译前,是通过字节码解释器进行解释执行。其执行原理为:字节码解释器读取内存中的字节码,按照顺序读取字节码指令,读取一个指令就将其翻译成固定的操作,根据这些操作进行分支,循环,跳转等动作,字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,在执行引擎读取下一条指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
3.1.3 方法区
保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;此区域属于共享区间;静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。
3.1.4 虚拟机栈
栈:就是先进后出
栈,也叫栈内存,主管程序的运行,生命周期和线程同步;**线程结束,栈内存也就是释放。**每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出(return、异常)。
对于栈来说,不存在垃圾回收问题一旦线程结束,栈就Over!每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。类似于下图的结构
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:栈内存中:8大基本类型+对象引用+实例的方法,既存储局部变量表、操作数栈、动态链接、方法出口等信息。
栈帧:局部变量表+操作数栈
每执行一个方法,就会产生一个栈帧。程序正在运行的方法永远都会在栈的顶部
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b9433be0f97b80b6a53a32e08a36b5ce.png#pic_center)
方法区+堆+栈的关系
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/19ad1761f4f2a9cb01161fcf71bb4daa.png)
3.1.5 堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组(保存所有引用类型的真实对象)。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
堆分区:新生代+老年代+永久代(元空间)
GC两种类:轻GC(普通的GC),重GC(全局GC)
为什么分代?
1、给堆内存分代是为了提高对象内存分配和垃圾回收的效率,99%的对象都是临时的
2、新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中。
3、根据不同年代的特点可以采用合适的垃圾收集算法。
4、新生代和老年代是垃圾回收的主要区域。
当新生代、老年代、元空间内存都满了之后才会报OOM
在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错
- 内存快照分析工具,MAT,Jprofiler,快速定位内存泄露
- Dubug,一行行分析代码!
3.2 JVM各区会出现的问题
内存泄露和内存溢出的区别:
- 内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。
- 内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
4. 垃圾回收算法
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法:引用计数算法、可达性分析算法
引用类型
- 强引用:如
Object obj = new Object()
只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被 垃圾收集器回收。
- 弱引用:它也是用来描述非需对象的,被弱引用关联的对象只能生存到下一次 垃圾收集发生之前。
- 虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。
4.1 引用计数法:
堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加加减减,为0回收
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
4.2 复制算法:
每次GC都会将Eden活的对象移到幸存区中:一旦Eden区被GC后,就会是空的。当一个对象经历了15次GC还没死,就会被转移到老年代
- 好处:没有内存的碎片
- 坏处:浪费了内存空间(一个幸存区的空间永远是空:to)。假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候;新生区~
4.3 标记–清除算法
标记:遍历内存区域,对需要回收的对象打上标记。
清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
- 效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
- 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。
4.4 标记-整理算法
(标记-清除-压缩算法)
清除之后对内存进行整理
再改进:先标记清除几次之后,再压缩1次
4.5 分代回收算法
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代:对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。
5. JMM
5.1 什么是JMM?
JMM:(Java Memory Model的缩写)缓存一致性协议,用于定义数据读写的规则
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存
![img](https://i-blog.csdnimg.cn/blog_migrate/98e7eddff216339d79ab32d9749280e5.webp?x-image-process=image/format,png)
5.2 JMM的意义
解决共享对象可见性这个问题: volilate,一旦刷新了就会很快的同步到主内存中。
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
JVM 面试常见:
1 介绍java内存区域(内存模型和分区,运行时数据区)
jvm中内存大体分为两部分,方法区和堆是所有线程共享的内存区域;而虚拟机栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
本地方法栈,本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务,调用native接口底层调用其他语言的实现,扩展java功能。
程序计数器,每个线程都有一个程序计数器,是线程私有的**,就是一个指针**, 指向方法区中的方法字节码,也就是当前线程正在执行的字节码的地址。 是一个非常小的内存空间,几乎可以忽略不计
方法区,保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;此区域属于共享区间;静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
虚拟机栈,主管程序的运行,生命周期和线程同步**;线程结束,栈内存也就是释放。每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。栈帧=局部变量表+操作数栈,包括8大基本类型+对象引用+实例的方法**
Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例和数组(保存所有引用类型的真实对象),几乎所有的对象实例都在这里分配内存。垃圾回收的重灾区,99%的JVM调优都在堆内存
2 解释内存中的栈、堆和方法区的关系
通过
new
创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;程序中的字面量(literal)如直接书写的100、"hello"和常量都是放在常量池中,常量池是方法区的一部分.
虚拟机栈里面存放着线程私有的东西,会指向堆和方法区
3 什么是OOM,什么是栈溢出? 怎么分析?
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
OutOfMemoryError: GC Overhead Limit Exceeded
:当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space
错误。(和本机物理内存无关,和你配置的对内存大小有关!)
4 JVM的常用调优参数有哪些?
设定堆内存大小,-Xmx:堆内存最大限制。
设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小 -XX:NewRatio: 新生代和老生代占比 -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
5 新生代和老年代的区别 ?
所谓的新生代和老年代是针对于分代收集算法来定义的,便于回收
新生代又分为Eden和Survivor两个区。加上老年代就这三个区。数据会首先分配到Eden区 当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。),当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。
如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空 间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代 中了,当然晋升老年代的年龄是可以设置的。如果老年代满了就执行:Full GC
6 MinorGC和FullGC的区别,GC是什么时候触发的
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:MinorGC和Full GC。轻GC和重GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发轻GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC,对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比轻GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
a) 年老代(Tenured)被写满; b) 持久代(Perm)被写满; c) System.gc()被显示调用; d) 上一次GC之后Heap的各域分配策略动态变化;**“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。**
7 说⼀下堆内存中对象的分配的基本策略
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
8 String s1 = new String(“abc”);这句话创建了几个字符串对象?
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
9 请你谈谈你对JVM的理解?
JVM是Java虚拟机的缩写,Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中。
所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。当然只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。
10 深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
堆里面的分区有哪些? Eden,from,to,老年区,说说他们的特点!
GC的算法有哪些? 标记清除法,标记压缩,复制算法,引用计数器,怎么用的?
谈谈JVM中,类加载器你的认识