JVM基础
1. 什么是虚拟机
Java 虚拟机,是一个可以执行 Java 字节码的虚拟机进程。
Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
但是,跨平台的是 Java 程序(包括字节码文件),而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM 。
2. 怎样通过 Java 程序来判断 JVM 是 32 位还是 64 位
- 查看 Java 版本命令
java -version
- 我可以使用以下语句来确定 JVM 是 32 位还是 64 位:
System.getProperty("sun.arch.data.model")
3. 32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB ,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。
64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64 ,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB 。甚至有的 JVM,如 Azul ,堆内存到 1000G 都是可能的。
4. 64 位 JVM 中,int 的长度是多少
Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位或者 4 个字节。
意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。
JVM内存管理
1. JVM由哪几部分组成
- 类加载器。
- 内存区。
- 执行引擎。
- 本地方法调用。
2. Java 内存区域
-
程序计数器: 它可以看做是当前线程所执行的字节码的行号指示器。
线程私有
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
如果正在执行的是 Native 方法,这个计数器值则为空。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
-
虚拟机栈:虚拟机栈描述的是 Java 方法执行的内存模型。
线程私有
每个方法在执行的时候,都会创建一个栈帧用于存储局部变量、操作数、动态链接、方法出口等信息。
每个方法调用都意味着一个栈帧在虚拟机栈中入栈到出栈的过程。
-
本地方法栈
和 Java 虚拟机栈的作用类似,区别是该区域是为 Native 方法的提供的。
-
堆内存:
线程共享
-
方法区:用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
线程共享
JDK8之后已废除。
-
元数据区:用于存储虚拟机加载的类信息、即时编译器编译后的代码等数据。
元空间并没有处于堆内存上,而是直接占用的本地内存。
3. 直接内存是不是虚拟机运行时数据区的一部分
直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中农定义的内存区域。在 JDK1.4 中新加入了 NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
本机直接内存的分配不会受到 Java 堆大小的限制,受到本机总内存大小限制。
配置虚拟机参数时,不要忽略直接内存,防止出现 OutOfMemoryError 异常。
4. 直接内存(堆外内存)与堆内存比较
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
直接内存 IO 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。
5. 堆和栈区别
- 栈内存用来存储基本类型的变量和对象的引用变量;堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存;堆内存中的对象对所有线程可见。
6. JDK7和JDK8的区别
- JDK7 的改变
- 存储在永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。比如:常量池和静态变量放到 Java 堆里。
- 但永久代仍存在于 JDK7 中,并没完全移除。
- JDK8 的改变
- 废弃 PermGen(永久代),新增 Metaspace(元数据区)。
- 那么方法区还在么?方法区在 Metaspace 中了,方法区都是一个概念的东西。
7.MetaSpace ⼤⼩默认是⽆限的么
MetaSpace 大小默认没有限制,一般根据系统内存的大小。JVM 会动态改变此值。
8. 怎么设置MetaSpace区域的大小
可以通过 JVM 参数配置
-
-XX:MetaspaceSize : 分配给类元数据空间(以字节计)的初始大小。
此值为估计值,MetaspaceSize 的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
-
-XX:MaxMetaspaceSize:分配给类元数据空间的最大值
超过此值就会触发Full GC 。
9.为什么要废弃永久代
-
字符串存在永久代中,容易出现性能问题和内存溢出。
TODO:为什么会出现性能问题
-
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。永久代太大还会导致GC过慢,并且回收效率偏低。
10. 怎么获取 Java 程序使用的内存
可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存:
- Runtime#freeMemory() 方法,返回剩余空间的字节数。
- Runtime#totalMemory() 方法,总内存的字节数。
- Runtime#maxMemory() 方法,返回最大内存的字节数。
11 说说内存分配
- 基础数据类型直接在栈空间分配;
- 方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
- 引用数据类型,需要用 new 来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
- 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
- 局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆
空间区域等待 GC 回收; - 方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;
- 字符串常量在 DATA 区域分配 ,this 在堆空间分配;
- 数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!
类加载
1. 什么是类加载器?
类加载器,用来加载 Java 类到 Java 虚拟机中。
一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序在经过 Java 编译器编译之后就被转换成 Java 字节代码。
类加载器,负责读取 Java 字节码,并转换成 java.lang.Class 类的一个实例。
每个这样的实例用来表示一个 Java 类。通过此实例的 Class#newInstance(…) 方法,就可以创建出该类的一个对象。
2. 有哪些类加载器?
- 根类加载器
- 扩展类加载器–加载位置 :jre\lib\ext 中
- 应用类加载器–加载位置 :classpath 中
- 自定义加载器(必须继承 ClassLoader)
3. 类加载发生的时机是什么时候
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类还没进行初始化,则需要先触发其初始化。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类还没进行初始化,则需要先触发其初始化。
- 当初始化了一个类的时候,如果发现其父类还没进行初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个执行的主类,即调用其 #main(String[] args) 方法,虚拟机则会先初始化该主类。
- 当使用 JDK7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
4. 类什么时候被初始化
- new 一个对象时。
- 初始化一个类的子类(会首先初始化子类的父类)。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 反射(Class.forName(“com.lyj.load”)) 。
- JVM 启动时标明的启动类。
5. 类加载器是如何加载 Class 文件的
- 加载,是找到.class 文件并把这个文件包含的字节码加载到内存中。
- 连接,又可以分为三个步骤:
- 验证:字节码验证、
- 准备:Class 类数据结构分析及相应的内存分配
- 解析:最后的符号表的解析。
- 初始化(类中静态属性和初始化赋值),使用(静态块的执行)。
加载
在加载阶段,虚拟机主要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。
连接
-
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范。例如:是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比 javac 编译阶段的语义分析),以保证其描述的信息符合 Java 语言规范的要求。例如:这个类是否有父类,除了 java.lang.Object 之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
-
准备
准备阶段,是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
-
这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
-
这里所设置的初始值通常情况下是数据类型默认的零值(如 0、0L、null、false 等),而不是被在 Java 代码中被显式地赋予的值。
假设一个类变量的定义为: public static int value = 3。那么静态变量 value 在准备阶段过后的初始值为 0,而不是 3。因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 3 的 public static 指令是在程序编译后,存放于类构造器 () 方法之中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
-
如果类字段的字段属性表中存在 ConstantValue 属性,即同时被 final 和 static 修饰,那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。
假设上面的类变量 value 被定义为: public static final int value = 3 。编译时, javac 将会为 value 生成 ConstantValue 属性。在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 3。我们可以理解为 static final 常量在编译期就将其结果放入了调用它的类的常量池中。
-
-
解析
把类中的符号引用转换为直接引用
初始化
初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值。
- 使用静态代码块为类变量指定初始值。
JVM 初始化步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
6. 什么是双亲委派模型(Parent Delegation Model)
每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成。)
- 在同一个命名空间中,不会出现类的完整名字相同的两个类。
- 在不同的命名空间中,有可能会出现类的完整名字相同的两个类。
- 类加载器负责加载所有的类,同一个类(一个类用其全限定类名(包名加类名)标志)只会被加载一次。
7. 双亲委派模型的工作过程
- 当前 ClassLoader 首先从自己已经加载的类中,查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
- 当前 ClassLoader 的缓存中没有找到被加载的类的时候
- 委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 Bootstrap ClassLoader。
- 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
8. 为什么优先使用父 ClassLoader 加载类
- 共享功能:可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加载。
- 隔离功能:主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String。
9. 什么是破坏双亲委托模型
破坏双亲委托模型,需要做的是,#loadClass(String name, boolean resolve) 方法中,不调用父ClassLoader 方法去加载类,那么就成功了。
10. 如何自定义 ClassLoader 类
- 继承ClassLoader
- 重写父类findClass方法,返回一个Class对象
11. JAVA 对象创建的过程
Java 中对象的创建就是在堆上分配内存空间的过程,此处说的对象创建仅限于 new 关键字创建的普通 Java 对象,不包括数组对象的创建。
-
检测类是否被加载
当虚拟机遇到 new 指令时,首先先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就执行类加载过程。
-
为对象分配内存
具体的分配内存有两种情况:
- 对于内存绝对规整的情况相对简单一些,虚拟机只需要在被占用的内存和可用空间之间移动指针即可,这种方式被称为“指针碰撞”。
- 对于内存不规整的情况稍微复杂一点,这时候虚拟机需要维护一个列表,来记录哪些内存是可用的。分配内存的时候需要找到一个可用的内存空间,然后在列表上记录下已被分配,这种方式成为“空闲列表”。
多线程并发时会出现正在给对象 A 分配内存,还没来得及修改指针,对象 B 又用这个指针分配内存,这样就出现问题了。解决这种问题有两种方案:
- 第一种,是采用同步的办法,使用 CAS 来保证操作的原子性。
- 另一种,是每个线程分配内存都在自己的空间内进行,即是每个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),分配内存的时候再TLAB上分配,互不干扰。可以通过 -XX:+/-UseTLAB 参数决定。
-
为分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
-
对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的 hashcode ,GC 分代年龄等信息。
-
执行 init 方法
执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于 Java 程序来说还需要执行 init 方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了 init 方法之后,这个对象才真正能使用。
12. 获得一个类对象有哪些方式
13.对象的内存布局是怎样的
对象的内存布局包括三个部分:
- 对象头:对象头包括两部分信息。
- 第一部分,是存储对象自身的运行时数据,如哈希码,GC 分代年龄,锁状态标志,线程持有的锁等等。
- 第二部分,是类型指针,即对象指向类元数据的指针。
- 实例数据:就是数据。
- 对齐填充:不是必然的存在,就是为了对齐。
14. 对象是如何定位访问的
对象的访问定位有两种:
-
句柄定位:
Java 堆会画出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
-
直接指针访问:
Java 堆对象的不居中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址。
15 不同对象访问的的区别
这两种对象访问方式各有优势:
- 使用句柄来访问的最大好处,就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
- 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
16. Java 虚拟机是如何判定两个 Java 类是相同的
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。