目录
Java内存区域与内存溢出异常
为什么要了解JVM?
Java和C、C++语言不同,有JVM帮助程序员管理内存,避免了花费精力去写Delete和free的同时,也使得内存溢出和泄露的问题的修正变得困难。
了解JVM有助于解决上面的问题
运行时数据区域
JVM负责管理的内存包括
- 程序计数器(Program Counter Register)
- Java虚拟机栈(Java Virtual Machine Stack)
- 本地方法栈(Native Method Stacks)
- Java堆
- 方法区
- 运行时常量池
- 直接内存
程序计数器
Q1:在单线程下,JVM如何知道下一条该执行哪个指令,在多线程下,线程切换后,JVM是如何知道执行当前线程的哪部分
无论是多线程环境还是单线程环境,JVM都需要一个指示器来标注当前程序执行的位置,也就是程序计数器
这个计数器实际上存储的是下一条需要执行的字节码指令
在单线程环境下,计数器主要负责对分支、循环、跳转、异常处理的操作提供支持
在多线程环境下,计数器可以用来记录不同线程的执行情况,因此不难看出每个线程的程序计数器是独立的,不允许其他线程修改。
而对于这类线程之间独立存储、互不影响的内存区域,通常被称为“线程私有”的内存。
ps:一个地址又能占用多大空间呢?显然这里很难OOM。
Java虚拟机栈
Q2:JVM通过程序计数器知道了下一条指令的位置,但现在有一种新的情况,当某个方法A调用了方法B,方法B执行结束后是如何回到方法A的?
单纯思考栈的FILO(First In Last Out)的特性我们不难发现,他很擅长做这种事。
Java虚拟机栈是线程私有的,他的生命周期与线程相同,每个方法被执行的时候,Java虚拟机都会创建一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法被调用直至完毕的过程,就对应一个栈帧在虚拟机栈中从入到出的过程。--《深入理解Java虚拟机JVM-高级特性与最佳实践》(3th,43p,2.2.2c)
从书中原话,或者栈的特性,我们都应该能想到,每个栈只有一个活动栈帧,也就是处在栈顶的这个栈帧。
再进一步思考不难发现的是,这里似乎都存放着的都是Java中的方法相关的信息,因此Java虚拟机栈又被称为方法栈。
Q3:说到方法,不妨思考下局部变量为什么是线程安全的?
简单的来说就是,局部变量存在Java虚拟机栈中,而虚拟机栈又是线程私有的,所以是线程安全的。
当然,如果他逃逸出了他的范围,比如作为返回值返回,那么就未必仍旧是线程安全的了。
Q4:虚拟机栈异常
StackOverflowError、OOM
ps:HotSpot虚拟机栈不会扩容,所以对于他来说,他的OOM的问题仅仅只是线程申请栈空间失败了的报错,而StackOverflowError是说一个线程申请的栈的深度大于允许深度。二者的区别就是,OOM栈空间没申请到,StackOverflowError申请到了栈空间,但是不能再放下新的栈帧了。
本地方法栈
Q5:众所周知Java底层是C和C++语言实现的,最终底层都会调用一些其他语言的语法,这些方法怎么处理?
答案就是本地方法栈,这里主要是为JVM用到的Native方法服务。
Q6:回顾之前的内容我们会发现,程序计数器记录的是Java的字节码指令地址,那C和C++也没有字节码指令地址,程序计数器此时存放的是什么?
对于这里程序计数器存放的值是null,我们可以通过程序计数器里存放的是什么很快的发现当前执行到的是Java的方法还是Native方法。当本地方法执行结束的时候会回到Java方法继续去执行。
Q7:承接上面,JVM怎么知道Native方法执行完了该执行Java方法了呢?
首先,JVM在执行本地方法的时候,将控制权完全的交给了本地方法,不再跟踪本地方法的执行状态而是等待返回
当本地方法执行结束后会通过特定的接口(JNI)告知JVM本地方法执行完毕
最后JVM恢复Java方法的执行,程序计数器指向下一个待执行的地址
Java堆
Java堆是JVM管理内存中的最大的一块区域,主要是存放对象实例,是被所有线程共享的一块区域。同时Java堆满足逻辑连续物理可不连续的特性。
Q8:new出来的对象放在哪里?
Java的new操作是创建一个实例,因此自然是放在Java堆中。
既然new出来的对象都放在堆中,而Java开发又对new的次数没有限制,所以很容易想到这里是GC回收器的主战场,因此Java堆又称为GC堆。
Q9:Java堆异常
当无法再new对象的时候报OOM。
方法区
Q10:new出来的对象放在Java堆,但是Java中你通常可以通过访问静态变量来获取没有被new的类中的数据,此时这个类存放在哪里?
方法区用于存储已经被虚拟机加载的类的信息,包括类型信息、常量、静态变量,也就是说,方法区存放加载了但是还没有被new出来的类。
整个流程大致是:被类加载器加载的类会被放在方法区等待被new,new出来的实例放在堆,当方法要被执行的时候则创建一个栈帧并推入栈中。
Q11:说一下这行代码JVM是对其进行了怎样存储的。MyClass myC= new MyClass()
MyClass的类信息放在方法区中,new MyClass()存放在堆中,而myC存放的是new出来的MyClass的引用,因此是放在栈中的局部变量表中。
运行时常量池
运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量与符号引用。
String的intern用来将堆中的String放到字符串池中
Q12:String str = “a”存放在哪里?String str2 = new String(“a”)存放在哪里
str被直接放在字符串池中,而str2是被new出来的所以放在堆中。
Q13:String str1= “a”,str2 = “b”;str1+str2 == “a”+ “b” ?
str1+str2的过程实际上是由StringBuilder进行的并在最后进行了一次toString,
“a”+“b”则由编译器优化直接变成了“ab”放入字符串池中,所以二者不相等
现在是回顾时间:
Q1:在单线程下,JVM如何知道下一条该执行哪个指令,在多线程下,线程切换后,JVM是如何知道执行当前线程的哪部分
Q2:JVM通过程序计数器知道了下一条指令的位置,但现在有一种新的情况,当某个方法A调用了方法B,方法B执行结束后是如何回到方法A的?
Q3:说到方法,不妨思考下局部变量为什么是线程安全的?
Q4:虚拟机栈异常
Q5:众所周知Java底层是C和C++语言实现的,最终底层都会调用一些其他语言的语法,这些方法怎么处理?
Q6:回顾之前的内容我们会发现,程序计数器记录的是Java的字节码指令地址,那C和C++也没有字节码指令地址,程序计数器此时存放的是什么?
Q7:承接上面,JVM怎么知道Native方法执行完了该执行Java方法了呢?
Q8:new出来的对象放在哪里?
Q9:Java堆异常
Q10:new出来的对象放在Java堆,但是Java中你通常可以通过访问静态变量来获取没有被new的类中的数据,此时这个类存放在哪里?
Q11:说一下这行代码JVM是对其进行了怎样存储的。MyClass myC= new MyClass()
Q12:String str = “a”存放在哪里?String str2 = new String(“a”)存放在哪里
Q13:String str1= “a”,str2 = “b”;str1+str2 == “a”+ “b” ?