走进Java
一、什么是虚拟机?
Java虚拟机,是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件(.class)。
Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
但是,跨平台的是Java程序(包括字节码文件),而不是JVM。JVM使用C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同的JVM。
也就是说,JVM能够跨计算机体系结构来执行Java字节码,主要是由于JVM屏蔽不同计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由JVM提供者来实现。
JVM由哪些部分组成?
JVM的结构基本上由4部分组成:
- 类加载器,在JVM启动时或者类运行时将需要的class加载到JVM中。
- 内存区,将内存划分成若干个区,以模拟实际机器上的存储、记录和调度功能模块,譬如实际机器上的各种功能的寄存器或者PC指针的记录等。
- 执行引擎,执行引擎的任务是负责执行class文件中包含的字节码指令,相当于实际机器上的CPU。
- 本地方法调用,调用C或C++实现的本地方法的代码返回结果。
怎样通过Java程序来判断JVM是32位的还是64位?
Sun有一个Java System属性来确定JVM的位数:32or64
sun.arch.data.model=32 //32 bit JVM
sun.arch.data.model=64 //64 bit JVM
我们可以使用一下Java语句来确定JVM是32位还是64位:
System.getProperty("sun.arch.data.modal");
🦅32位JVM和64位JVM的最大堆内存分别是多少?
理论上说32位的JVM堆内存可以达到2^32,即4GB,但实际上会比这个小很多。不同的操作系统之间不同,如Windows系统大约1.5GB,Solaris大约3GB。
64位JVM允许指定最大的堆内存,理论上可以达到2^64,这是一个非常庞大的数字,实际上你可以指定堆内存大小到100GB。甚至有的JVM,如Azul,堆内存到1000GB都是可能的。
🦅64位的JVM中,int的长度是多少?
Java中,int类型变量的长度是一个固定值,与平台无关,都是32位。意思就是说,在32位和64位的Java虚拟机中,int类型的长度是相同的。
Java内存区域与内存溢出异常
JVM运行内存的分类?
JVM运行内存的分类如下图所示:
- 程序计数器: Java线程私有,类似于操作系统里的PC计数器,它可以看做是当前线程所执行的字节码的行号和指示器。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空(Undefined)。
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- 虚拟机栈(栈内存): Java线程私有,虚拟机栈描述的是Java方法执行的内存模型:
- 每个方法在执行的时候,都会创建一个栈帧用于存储局部变量、操作数、动态链接、方法出口等信息。
- 每个方法调用都意味着一个栈帧在虚拟机中从入栈到出栈的过程。
- 本地方法栈: 和Java虚拟机的作用类似,区别是该区域为JVM提供使用Native方法的服务。
- 堆内存(线程共享): 所有线程共享的一块区域,垃圾收集器管理的主要区域。
- 目前主要的垃圾回收算法都是分代收集算法,所以Java堆中还可以细分为:新生代和来年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等,默认情况下新生代按照
8:1:1
的比例来分配。 - 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘一样。
- 目前主要的垃圾回收算法都是分代收集算法,所以Java堆中还可以细分为:新生代和来年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等,默认情况下新生代按照
- 方法区(线程共享): 各个线程共享的一个区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该是与Java堆区分开来。
- 运行时常量池:是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。
🦅直接内存是不是虚拟机运行时数据区的一部分?
参见《JVM直接内存》文章
直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
- 本机直接内存的分配不会受到Java堆大小的限制,受到本机总内存大小限制。
- 配置虚拟机参数是,不要直接忽略内存,防止出现OutOfMemoryError异常。
🦅直接内存(堆外内存)与堆内内存的比较?
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
- 直接内存IO读写的性能要由于普通的堆内存,在多次读写操作的情况下差异明显。
因为,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的JVM上方法区的实现肯定时不同的了。
同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。
- 参考文章
🦅JDK8之后Perm Space有哪些变动?Meta Space 大小默认是无限的吗?还是你们会通过什么方式来指定大小?
- JDK8后用元空间替代了Perm Space;字符串常量存放到堆内存中。
- Meta Space 大小默认没有限制,一般根据系统内存的大小。JVM会动态改变此值。
- 可以通过JVM参数配置
-XX:MetaspaceSize
:分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the inital high-water-mark)。此值为估计值,MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元空间的大小可能会变大。-XX:MaxMetaspaceSize
:分配给类元数据空间的最大值,超过此值就会触发Full GC。此值默认没有限制,但应取绝于系统内存的大小,JVM会动态改变此值。
🦅为什么要废弃永久代?
- 显示使用中易出问题。
由于永久代内存经常不够用或发生内存泄漏,爆出异常java.lang.OutOfMemoryError: PermGen
。- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久内存溢出,太大则容易导致老年代溢出。
- 永久代会为GC带来不必要的复杂度,并且回收效率偏低。
- Oracle可能会将HotSpot与JRockit合二为一。
即:移除永久代是为融合HotSpot JVM与JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
Java 内存堆和栈的区别?
- 栈内存用来存储基本类型的变量和对象的引用变量;堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存;堆内存中的对象可以被所有线程访问。
- 如果栈内存没有可用的空间储存方法和调用局部变量,JVM会抛出
java.lang.StackOverFlowError
错误;如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError
错误。 - 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。
-Xss
选项设置栈内存的大小,-Xms
选项可以设置堆的开始时的大小。
速记:
JVM中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个JVM的所有线程共享。
Java对象创建的过程,如下图所示:
-
Java中对象的创建就是在堆上分配内存空间的过程,此处说的对象创建仅限于new关键字创建的普通Java对象,不包括数组对象的创建。
-
检测类是否被加载
当虚拟机遇到new指令时,首先先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就执行类加载过程。 -
为对象分配内存
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。具体的分配内存有两种情况:第一种情况是内存空间绝对规整,第二种情况是内存空间是不连续的。
- 对于内存绝对规整的情况相对简单一些,虚拟机只需要在被占用的内存和可用空间之间移动指针即可,这种方式被称为“指针碰撞”。
- 对于内存不规整的情况稍微复杂一点,这时候虚拟机需要维护一个列表,来记录哪些内存是可用的。分配内存的时候需要找到一个可用的内存空间,然后在列表上记录下已被分配,这种方式成为“空闲列表”。
-
- 未分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。 - 对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息。 - 执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于Java程序来说还需要执行init方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码部分分配初始值,调用了init方法之后,这个对象才真正能使用。
到此为止一个对象就产生了,这就是new关键字创建对象的过程。
过程如下:
面试官可能会将问题引申成“
A a=new A()
经历过什么过程”的问题。
对象的内存布局是怎样的?
对象的内存布局包括三个部分:
- 对象头:对象头包括两部分信息
- 第一部分,是存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁等。
- 第二部分,就类型指针,即对象指向类元数据的指针。
- 实例数据:就是数据。
- 对齐填充:不是必然存在,就是为了对齐。
对象是如何定位访问的?
对象的访问定位有两种:
- 句柄定位:Java堆会画出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针访问:Java堆对象的不居中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
🦅 对比两种方式?
这两种对象访问方式各有优势。
- 使用句柄来访问的最大好处,就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象时非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
- 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。