一、JVM运行时数据区域?
1.程序计数器
字节码解释器
通过改变程序计数器来依次读取指令,从而实现代码的流程控制。- 在多线程的情况下,程序计数器用于记录当前线程执行的位置。
程序计数器是唯一一个不会出现
OutOfMemoryError
的内存区域
2.虚拟机栈(-Xss设置大小)
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 虚拟机栈描述的是 java 方法执行的内存模型,每一个方法的调用到完成,对应着一个栈帧入栈到出栈的过程。
每一个栈帧包括:局部变量表
、操作数栈
、动态链接
、方法返回地址
- 局部变量表:主要存放了编译期可知的各种
数据类型(boolean、byte、char、short、int、float、long、double)
、对象引用(不是对象本身)
。局部变量不同于静态变量,不会系统初始化
。 - 操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据。主要作为计算过程中变量临时的存储空间。
- 动态链接:在 java 源文件被编译为字节码文件时,所有的变量和方法引用都作为符号引用保存在 class 文件的
常量池
里。描述一个方法调用其他方法时,就是通过常量池
中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
栈中可能出现的异常?
- 固定大小栈,线程请求分配的栈容量超过虚拟机栈允许的最大容量,StackOverflowError
- 可动态扩展大小栈,未申请到足够内存,OutOfMemoryError
方法中的局部变量是否安全?
如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
3.本地方法栈
类似虚拟机栈,不过对应的是虚拟机使用到的本地native方法
。
4.堆(-Xmx/-Xms设置大小)
一个 JVM 实例只存在一个堆内存,java 堆区在 JVM 启动的时候即被创建,用来**存放对象实例,以及为数组分配内存。**堆是垃圾回收的主要区域,也称GC堆。
在 JDK 7 之前,堆内存被分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
在JDK7之后,永久代
被替换为元空间(位于内存)
。
堆是对象分配的唯一选择吗?
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束就会被移除,无需进行垃圾回收。
栈是运行时的单位,而堆是存储的单位
5.方法区
用于存储已被虚拟机加载的类信息
、常量
、静态变量
、即时编译器编译后的代码
等数据。
方法区是一种规范,永久代是对方法区的实现。
- 类型信息:对每个加载的类型(类 class、接口 interface、枚举、注解 ),存储包括以下信息:
-
运行时常量池:位于方法区。其中的
常量池表
用于存放编译期生成的各种字面量
和符号引用
。 -
域信息:域名称、域类型、域修饰符(public,private)。
-
方法信息:JVM 必须保存所有方法的以下信息,同域信息一样包括声明顺序。
-
静态变量:静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。
-
全局常量(static final):每个全局常量在编译的时候就会被分配了。
常量池和运行时常量池
常量池位于字节码文件中,指class常量池,其中存放了编译器生成的字面量
和符号引用
。
- 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
- 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
运行时常量池是指加载到虚拟机后的常量池,其中的常量池表
存放字面量
和符号引用
。
字符串常量池
字符串常量池是常量池中的一种类型,在jdk6.0之前位于方法区
中,在jkd7.0之后和静态变量
位于堆中。
字符串常量池中存放了字符串常量
和堆内字符串对象的引用
。
方法区的垃圾回收
主要回收两部分内容:常量池中废弃的常量和不再使用的类。
废弃的常量:
- 只要常量没有被任何地方引用,就可以被回收。
不再使用的类:
- 该类的所有实例已经被回收
- 加载该类的类加载器已经被回收
- 该类对应的.Class对象没有被引用,无法在任何地方通过反射访问该类的方法
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。
如加载大量的第三方的 jar 包;
Tomcat 部署的工程过多(30~50 个);
大量动态的生成反射类。
二、对象的创建过程?
对象的组成
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不是必然存在)。
1.类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2.分配内存
分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的并发问题
由于创建对象很频繁,所以要保证线程安全。
- CAS+失败重试:虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
TLAB是JVM 为每个线程分配的一个私有缓存区域,位于Eden区。
3.初始化零值
虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用。
4.设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 另外根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.执行init方法
将对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
三、关于对象的访问定位
我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有① 使用句柄和② 直接指针两种:
1.句柄
如果使用句柄的话,那么 java 堆中将会划分出一块内存来作为句柄池
,reference 中存储的就是对象的句柄地址
,而句柄中包含了对象实例数据
与类型数据
各自的具体地址信息
。
2.直接指针
如果使用直接指针访问,那么 java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址
。
二者对比
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。