1.运行时数据区域
2.1程序计数器
当前线程所执行的字节码的行号指示器。
像我们常用的debug模式,为什么能定位到这一行,就是因为程序计数器的原因,jvm在栈桢中LineNumberTable就存储了程序计数器与实际代码行数的关联表(虚拟机栈有图介绍)。
个人期望后续debug可以依赖于程序计数器来实现跳过一行代码
特点:(1)线程私有。(2)java方法存计数器记录地址,native方法计算器为nul,因为已经超出jvm内存地址了。
异常:不会出现OOM
2.2java虚拟机栈
虚拟机栈描述的是java方法的内存模型:线程在每次方法调用执行时创建一个栈帧然后压栈,栈帧用于存放局部变量表、操作数栈、动态链接、方法返回地址等信息。方法调用到执行完成对应的栈帧的入栈与出栈。我们平时说的栈内存就是指这个栈。
局部变量表:局部变量表存储了编译期可知的各种java虚拟机基本数据类型,对象引用和rerurnAdress类型。这些数据在局部变量表中的存储空间以局部变量槽(Slot)表示,64位长度的long和double占据两个变量槽,其余数据类型占据一个变量槽。局部变量表所需的空间在编译期间内完成分配,进入一个方法时,这个方法需要在栈桢中分配多大的局部变量表是完全确定的,在方法运行期间不会改变局部变量表的大小。这里的大小是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如一个变量槽占用32个比特、64个比特或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。
1字节(b) = 8比特(bit)=xxx槽(Slot)
操作数栈:后入先出的操作栈,编译期确定其需要的最大深度。方法执行过程中,会有各种指令码(javap -c反编译查看指令码)往操作栈中写入与提取内容,也就是入栈/出栈操作。如算术运算指令码iadd会弹出操作数栈栈顶的两个元素,执行相加运算,并将结果入栈。
动态链接:符号引用转直接引用,每个栈帧包含一个指向运行时常量池中该栈帧所属方法的引用(运行期间完成),同类加载时解析阶段静态链接-符号引用转直接引用
静态链接:将一些符号引用(静态方法)替换为指向数据内存的指针或句柄(类加载解析期间完成)
方法返回地址:正常退出,返回地址可以为程序计数器的值,栈帧保存;异常退出,返回地址通过异常处理器确定,栈帧不保存。
特点:虚拟机栈线程私有,生命周期与线程相同。
异常:StackOverflowError& OutOfMemeryError
StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
OutOfMemeryError:如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。
说明:《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
栈内存溢出demo
/**
* 虚拟机栈和本地方法栈OOM
* jvm args: -Xss128k
* java.lang.StackOverflowError
*/
public class JvmStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JvmStackSOF jvmStackSOF = new JvmStackSOF();
try {
jvmStackSOF.stackLeak();
}catch (Throwable e){
System.out.println("stack length:" + jvmStackSOF.stackLength);
throw e;
}
}
}
结果
2.3本地方法栈
虚拟机栈为虚拟机执行的java方法服务,本地方法站为虚拟机使用的native方法服务,native方法指的是java调用其他语言编写的程序。本地方法栈一般不需要我们关注
异常:StackOverflowError& OutOfMemeryError 同虚拟机栈
2.4 java堆
JVM管理的最大一块内存,在JVM启动时创建。堆内存存放所有对象实例以及数组。
堆是java垃圾收集器管理的主要区域(所以很多时候会称它为GC堆)。
堆内存分配:新生代,老年代。新生代分为Eden,From Survivor,To Survivor。
实例对象存储在堆区时:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据class存在方法区或者元空间
实例对象在即时编译过程中没有线程逃逸行为,Object实例对象一定是存在堆区。
特点:线程共享
异常:OOM
逃逸分析:分析对象动态作用域,当一个对象在方法里被定义后,他可能被外部方法引用,如作为入参传递给其他方法,这种称为方法逃逸;甚至可能被外部线程访问到,如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸、线程逃逸,称为对象由低到高的不同逃逸程度。即时编译器优化技术JIT重要前进方向。
优化:
1.栈上分配
如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存是一个很不错的主意,对象所占用的内存可以随栈桢出栈而被摧毁。但是由于复杂度等原因,HotSpot目前没有做这项优化,其他虚拟机(如Excelsior JIT)使用了该优化。栈上分配可以支持方法逃逸,不支持线程逃逸。
2.标量替换-重点
标量:如果一个数据如法分解为更小的数据来表示了,如java虚拟机的原始数据类型(int,long等数值类型及reference类型)都不能进一步分解了,那么就可称为标量;如果一个数据可分解,则称为聚合量,如java对象。如果把一个java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,该过程称之为标量替换。
假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建他的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以将对象的成员变量在栈上分配和读写,还可以为后续进一步优化手段创造条件。标量替换可以视作栈上分配的一种特例,对逃逸程度要求更高,不允许对象逃逸出方法范围外。
demo
/**
* 逃逸分析demo
*/
public class EscapeAnalysisDemo {
/**
* 未优化的代码
*/
public static int test(int x) {
int xx = x + 2;
Point p = new Point(xx, 42);
return p.getX();
}
/**
* 逃逸分析发现test方法Point对象只作用在当前方法内,不会发生逃逸,对其进行标量替换
* 优化后的代码
*/
public static int test(int x) {
int xx = x + 2;
int px = xx;
int py = 42; //无效代码最终也会消除
return px;
}
}
@Data
@AllArgsConstructor
class Point {
int x;
int y;
}
3.同步消除
线程同步本身就是个相对耗时的过程,如果逃逸分析确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以安全的消除掉。
堆内存溢出demo
/**
* java堆内存溢出异常
* jvm args: -Xms20m -Xmx20m
* <p>
* java.lang.OutOfMemoryError: Java heap space
*/
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true){
//每次增加一个1M大小的数组对象
list.add(new byte[1024 * 1024]);
}
}
}
结果:
2.5 方法区
方法去存储被虚拟机加载的类信息(类的版本,字段,方法,接口),常量,静态变量,即时编译器编译后的代码缓存等数据。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
特点:内存共享
jdk7的HotSpot,已经把放在永久代的字符串常量池、静态变量等移出,到了JDK8,终于完全废弃了永久代的概念,将永久代剩余的内容(类型信息)转移到使用本地内存实现的元空间中。
采用深入理解jvm第三版描述:
从概念上讲,静态变量所使用的内存都应当在方法区进行分配,但必须注意方法区本身是一个逻辑上的区域,在JDK7之前,HotSpot使用永久代来实现方法区,是完全符合这种逻辑概念的;但是JDK8之后,静态变量则会存放在java堆中,这时候“静态变量在方法区”就完全是一种对逻辑概念的表述了。
从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区中,但方法区如何实现,《Java虚拟机规范》并未规定,JDK7及其以后版本的HotSpot虚拟机选择把静态变量存储在Java堆中,《深入理解java虚拟机》4.3.1有验证。
运行时常量池:
方法区的一部分。Class文件中除了类的版本,字段,方法,接口等描述信息,还有一项信息是常量池表(Constant Pool Table),用于(1)存放*编译期*生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。除了保存Class文件中描述的符号引用外,(2)还会将符号引用翻译出来的直接引用也存储在运行时常量池中。(3)运行期间也可以将新的常量放入池中,如String.intern()方法。
2.5.1永久代PermGen
方法区是JVM的规范,永久代则是JVM规范的一种实现。永久代物理内存是是堆的一部分。
Jdk8后永久代被移, 永久代移除,方法区不会消失,因为它只是个规范。永久代的数据,静态变量+常量池转移到堆中,类的元信息转移到元空间。
移除原因:
1.使用PermSize和MaxPermSize设置永久代的大小,但是类及方法的信息等比较难确定其大小,容易遇到java.lang.OutOfMemoryError: PermGen错误。
2.字符串存在永久代中,容易出现性能问题和内存溢出。
永久代异常eg:
验证字符串常量池存在堆中还是元空间demo:
/**
* 验证jdk8以上 字符串常量池存在堆中还是元空间中
*
* @author sizuoyi
* @version 1.0.0: demo, v 0.1 2019-07-11 10:50 sizuoyi Exp $
*/
public class StringOOM {
static String base = "string";
/**
* jvm args:-Xms20m -Xmx20m
* java.lang.OutOfMemoryError: Java heap space
* <p>
* jvm args:-Xms20m -Xmx20m -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
* java.lang.OutOfMemoryError: Java heap space
* <p>
* 结论:不会导致元空间溢出,字符串常量池存储在堆中
*
* @param args
*/
public static void main(String[] args) {
List<String> list = new ArrayList<>();
while (true) {
String str = base + base;
base = str;
//intern()是一个native方法,作用是:如果字符串常量池已经包含等于该string对象的字符串,
//则返回代表池中的该string对象,否则将此string对象包含的字符串添加到常量池中,并且返回该string对象的引用
list.add(str.intern());
}
}
}
结果:
验证静态变量存储在堆中是元空间中demo1:
public class StaticOOM {
static String[] s0 = new String[1024 * 1024];
static String[] s1 = new String[1024 * 1024];
static String[] s2 = new String[1024 * 1024];
static String[] s3 = new String[1024 * 1024];
static String[] s4 = new String[1024 * 1024];
static String[] s5 = new String[1024 * 1024];
static String[] s6 = new String[1024 * 1024];
static String[] s7 = new String[1024 * 1024];
static String[] s8 = new String[1024 * 1024];
static String[] s9 = new String[1024 * 1024];
static String[] s10 = new String[1024 * 1024];
/**
* -Xms10m -Xmx10m
* java.lang.OutOfMemoryError: Java
heap space
* <p>
* -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
* 正常输出
* <p>
* 结论:不会导致元空间溢出,即静态变量存储在堆中
*
* @param args
*/
public static void main(String[]
args) {
System.out.println("end");
}
}
结果:
验证静态变量存储在堆中是元空间中demo2:
/**
* 静态变量存放位置demo
*/
public class StaticVariableDemo {
public static void main(String[] args) {
//对象的大小不计算静态变量
long objectSize = ObjectSizeCalculator.getObjectSize(new A());
long objectSize2 = ObjectSizeCalculator.getObjectSize(new B());
System.out.println(objectSize);//40
System.out.println(objectSize2);//40
}
}
class A {
int m0;
long m1;
double m2;
String m3;
}
class B {
int m0;
long m1;
double m2;
String m3;
static int n0;
static long n1;
static double n2 = 1;
static String n3 = "1";
}
结果:
结论:
类加载完成后可确定对象的大小。--对象的内存大小是不包含静态变量的,因为《java虚拟机规范》逻辑上指明静态变量是在方法区的,虽然创建过大的静态变量抛出异常为堆OOM(即物理上静态变量存放在堆中),但是对象的大小却不计算静态变量,侧面验证jdk8之后,方法区在淘汰了永久代之后,静态变量逻辑上属于方法区,物理上属于堆。
2.5.2元空间Metaspace
Jdk8后永久代被移,使用元空间替换
元空间:不使用堆内存,使用本地内存,即系统内存大小决定元空间大小,所以不会出现内存溢出。
MetaspaceSize 表示的并非是元空间的大小,它的含义是:主要控制matesaceGC发生的初始阈值,也就是最小阈值。也就是说当使用的matespace空间到达了MetaspaceSize的时候,就会触发Metaspace的GC,也就是fullGC。
元空间内存溢出demo:
/**
* 借助CGLib使方法去内存溢出异常
* jvm args: -XX:PermSize=10M -XX:MaxPermSize=10M
*
* jdk8移除老年代,使用元空间替换
* -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy)
-> methodProxy.invokeSuper(objects,args));
enhancer.create();
}
}
static class OOMObject{
}
}
结果:
2、对象
(1)对象的创建
1.定位符号引用与加载
当Java虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。
2.分配内存
类加载检查通过后,虚拟机将会为新生对象分配内存。对象所需内存的大小在类加载完成后便可确定(详见下文),为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
分配空间方式
1.指针碰撞:堆空间规整,使用过的在一边,空闲的在另一边,中间放着一个指针作为分界点的指示器,通过位移指针分配内存
2.空闲列表:堆空间不规整,使用过的与空闲的混在一起,维护一个列表,记录哪些内存块可用,分配时从列表找到一块足够大的空间划分给实例对象,更新列表。
选择哪种分配方式取决于堆空间是否规整,堆空间是否规整又由垃圾收集器是否带有空间压缩整理能力决定。
因此,使用Serial、ParNew等带有压缩整理(Compact)的收集器,采用指针碰撞,使用CMS这种基于清除算法(Sweep)的收集器,理论上只能采用空间列表分配。(CMS内部设计了分配缓冲区,通过较大列表拿到一大块分配缓冲区后,在它里边使用指针碰撞方式分配)
通过修改指针修改指向的位置来对象创建,并发安全问题
解决方案
1.CAS+重试保证更新操作的原子性
2.内存分配的动作按照线程划分在不同的空间进行,即每个线程在java堆中于线分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区分配,只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。
3.对象头设置
如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的GC分代年龄信息等。
以上,虚拟机创建完毕,java程序创建开始-构造方法
(2)对象的内存布局
对象在对内存的存储布局分为三个部分:对象头(Header),实例数据(Instance Data),对齐填充(Padding)
对象头:
1).存储对象本身的运行时数据,如HashCode(25),GC分代年龄(4),锁状态标志(2),线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据长度32个比特或64个比特,官方称为“Mark Word”。
2).类型指针,即对象只想它的类型元数据的指针,java虚拟机通常使用这个指针来确定是哪个类的实例。如果对象是java数组,对象头还必须有一块用于记录数组长度的数据。
实例数据:
对象真正存储的有效信息,即我们在程序代码里定义的各种类型的字段内容都会记录下来。
对齐填充:
仅仅占位,任何对象的大小必须是8字节的整数倍,对象头被设计为8字节的倍数,因此使用对齐填充来补充对象示例数据部分。
java占用内存情况具体分析可参考:https://www.cnblogs.com/feng-gamer/p/10543004.html
(3)对象的访问定位
创建对象后,java程序通过“栈上本地变量表的reference”数据来操作堆上具体对象。
对象访问主流方式有句柄与直接指针两种
句柄:java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄包含了对象实例数据(实例池中对象实例数据)与类型数据各自具体的地址信息(指向方法区的对象类型数据)。
直接指针:reference中存储的就是对象地址,对象包含对象实例数据和到对象类型数据的指针(指向方法区的对象类型数据)。
句柄好处:存储稳定地址,在对象被移动时(垃圾收集移动频繁)只改变句柄中的实例数据指针。
指针好处:速度块,节省一次指针定位开销。
HotSpot选择指针,主要由于java对象访问非常频繁,节约开销。
网友评论