JVM的位置
JVM处于操作系统之上,为Java程序在不同的系统平台上的运行提供便利,与硬件没有直接的交互。
JVM体系结构
一个java文件,通过javac命令编译成一个ClassFile,通过类加载器ClassLoader加载到运行时数据区中,运行时数据区由方法区、java栈、程序计数器、堆、本地方法栈组成。
1、 方法区(Method Area)
方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2、 Java堆(Java Heap)
是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例(所有的对象实例以及数组都要在堆上分配。)
3、 程序计数器(Computed)
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
4、 java栈(Java Stack)
每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
5、本地方法
本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
类的加载
class -> loading -> linking(verification -> preparation -> resolution) -> initializing -> GC
class 文件转换为字节码加载到内存中,verification 加载中会校验字节,preparation 将 class 类中的静态变量赋默认值,resolution 将 class 类文件符号应用转换为直接访问地址,initializing 将 class 类中的静态变量赋值为初始值。
双亲委派机制
JVM中提供了三层的ClassLoader:启动类(根)加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)、应用程序加载器(AppClassLoader)
它是类在加载时的一个安全机制,可以避免类的重复加载。第一步类加载的时会将这个请求向上委托给父类加载器去完成,一直向上委托,知道启动类加载器。第二步启动加载器检查是否能够加载当前这个类,能加载就结束并且使用当前加载器。否则排除异常,通知加载器进行加载。
Native
java代码中凡是带有native关键字的,说明java的作用范围打不到了,会去调用低层c语言的代码,进入本地方法栈调用本地方法接口(JNI:拓展java的使用,融合不同的编程语言),它在内存区域中专门开辟了一块标记区域。Native Method Stcak,登记native方法。
PC 寄存器(Program Counter Register)
每个线程都有一个线程计数器,是线程私有的,就是一个指针。指向方法区中的方法字节码(用来存储指向一条指令的地址,即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
方法区(Method Area)
方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也再次定义。简单来说所有定义的方法信息都会保存在该区域,此区域属于共享区域。
静态变量、常量、类信息、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
栈、堆
栈(Stack)先进后出,后进先出
栈内存主管程序的运行,生命周期和线程同步。线程结束栈内存释放。对于栈来说不存在垃圾回收问题。(8 大基本类型+对象引用+实例的方法)
栈运行原理,栈帧
堆(Heap)
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取到类文件后,一般会把类,方法,常量,变量。保存我们所有引用类型的真是对象。
堆内存分为三个区域
新生区
新生区是所有类诞生、成长甚至死亡的地方。所有对象都是在伊甸园区new出来的。其中还有幸存者0,1区。
99%的对象都是临时对象
养老区
伊甸园new出来的对象超过内存时会被GC垃圾回收,还在使用的将转存到幸存者区,当幸存者区满时再次被GC垃圾回收,如果还有在使用的将放入到永久区
永久区
这个区域常驻在内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行是的一些环境,不存在垃圾回收,关闭虚拟机的时候就会释放这个区域的内存
jdk1.6之前 :永久代,常量池在方法区
jdk1.7 :永久代,退化去永久代,常量池在堆中
jdk1.8之后 :无永久代,常量池在元空间
三种 JVM
Sun公司 HoSpot
BEA公司 JRockit
IBM公司 JIT
堆内存调优
默认情况下分配的总内存是电脑内存的1/4,初始化的内存1/64
OOM(OutOfMenmoryError)表示堆内存满了
1. 尝试扩大堆内存
2. 分析内存,看一下那个地方出现了问题(专业工具 内存快照 MAT,Jprofiler)
3. 分析Dump内存文件,快速定位内存泄漏
-Xms64m -Xmx64m -XX:+PrintGCDetails
-Xms 初始化内存分配大小 默认情况下分配的总内存是初始化的内存1/64
-Xmx 设置最大分配内存 默认情况下分配的总内存是电脑内存的1/4
-Xss 扩大栈的大小,可以将不存在逃逸的对象放入栈中
-XX:+PrintGCDetails 打印GC详情
Heap
PSYoungGen total 18944K, used 2309K [0x00000000feb00000, 0x0000000100000000, 0x0000000100000000)
eden space 16384K, 14% used [0x00000000feb00000,0x00000000fed41580,0x00000000ffb00000)
from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
to space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
ParOldGen total 44032K, used 0K [0x00000000fc000000, 0x00000000feb00000, 0x00000000feb00000)
object space 44032K, 0% used [0x00000000fc000000,0x00000000fc000000,0x00000000feb00000)
Metaspace used 3153K, capacity 4564K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
GC(垃圾回收机制)
主要是在新生区和养老区
轻 GC:普通的 GC 只针对新生代,偶尔针对幸存区
重 GC:重 GC 进行全局回收,清空内存
面试题解答
1. 请解释一下对象创建过程?(半初始化)
申请一块内存,创建一个成员变量赋予初始默认值,构造方法调用将成员变量赋予初始值,最后进行关连。
2. 加问 DCL 与 volatile 问题?(指令重排)
volatile 主要作用是禁止指令重排序,创建对象时 CPU 底部内层执行时读写指令可能会进行重排序,当第二个线程进入时使用的是半初始化状态的对象
3. 对象在内存中的存储布局?(对象与数组的存储不同)
对象头(markword) 类型指针(classpointer)数组长度(length) 实例数据(instancedata) 对齐(padding)
4. 对象头具体包括什么?(markword classpointer) synchronized 锁信息
markword 1.锁信息 2.GC信息 3.identity hashcode信息
5. 对象怎么定位?(直接 间接)
直接指针指向对象,通过对象内的类型数据指针指向方法区的类。优点对象小,垃圾回收时不用平凡改动,缺点两次访问。
指针指向两个对象,一个指向实例数据,一个指向类型数据。有点直接访问,缺点 GC 需要移动对象的时候麻烦。
6. 对象怎么分配?(栈上-线程本地-Edon-Old)
开始创建对象,判断是否能存放在栈中,不能放在栈中且对象过大将直接放在老年代(OLD)中,最后通过 FullGC 回收。小的对象则放在线程本地分配缓冲区 TLAB(Thread Local Allocation Buffer),最后放入新生代(Edon)中。经过 YoungGC 回收,如果没有被回收则放入幸存者 0 区,如果还没有被回收则放入幸存者 1 区,往复到达一定年龄则进入老年代(OLD)中,通过 FullGC 回收。
7. Object o = new Object()在内存中占多少字节?
markword 8 字节
classpointer 4 字节
padding 4 字节