1. JVM的主要组成部分及其作用
-
Class loader(类装载)
根据给定的全限定名类名(如: java.lang.Object)来装载class文件到 Runtime data area中的method area。
-
Execution engine(执行引擎)
执行classes中的指令。
-
Native Interface(本地接口)
与native libraries交互,是其它编程语言交互的接口。
-
Runtime data area(运行时数据区域)
这就是我们常说的JVM的内存。
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
2. JVM运行时数据区/内存模型
-
程序计数器(Program Counter Register)【私有】
当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
-
Java 虚拟机栈(Java Virtual Machine Stacks)【私有】
每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
-
本地方法栈(Native Method Stack)【私有】
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
-
Java 堆(Java Heap)【共享】
Java 虚拟机中内存大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
-
方法区(Methed Area)【共享】
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
-
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Poll Table)用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
其中字符串常量池属于运行时常量池的一部分,不过在HotSpot虚拟机中,JDK1.7将字符串常量池移到了java堆中。
-
-
直接内存
直接内存不是JVM运行时的数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中引 入了NIO(New Input/Output)类,引入了一种基于通道(Chanel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java中的DirectByteBuffer对象作为对这块内存 的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和Native对中来回复制数据。
-
直接内存(堆外内存)与堆内存比较
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
- 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
-
使用场景
- 有很大的数据需要存储,它的生命周期很长
- 适合频繁的IO操作,例如网络并发场景
-
为什么要主动调用System.gc
通过触发一次gc操作来回收堆外内存
堆外内存的回收其实依赖于我们的gc机制,首先我们要知道在java层面和我们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能通过操作DirectByteBuffer对象来间接操作对应的堆外内存了。DirectByteBuffer对象在创建的时候关联了一个PhantomReference, 说到PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块
-
-
版本变化
-
Jdk1.6及之前:有永久代, 常量池在方法区
-
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池在堆
-
Jdk1.8及之后:无永久代,常量池在元空间
-
3. 堆栈的区别
-
物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
-
内存分配
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。
一般堆大小远远大于栈。 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
-
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
静态变量放在方法区,静态的对象还是放在堆
-
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
-
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;
堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
4. 对象创建的五种方法
- new关键字
- Class的newInstance方法
- Constructor类的newInstance方法
- clone方法
- 反序列化
5. new的过程
-
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。
-
类加载通过后,接下来分配内存。
-
若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;
即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
-
如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。
-
划分内存时还需要考虑一个问题-并发,也有两种方式:
- CAS同步处理,
- 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。
-
-
然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),后执行方法。
-
对象的访问定位
-
句柄
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
-
直接指针
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
-
6. Java 中都有哪些引用类型
-
强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的 对象来解决内存不足的问题。
-
软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用