心里种花,人生才不会荒芜,如果你也想一起成长,请点个关注吧。
JVM
class文件通过类加载器加载到运行时数据区,运行时数据区又分为线程私有和线程共享的内存;
运行时数据区的数据和方法,通过执行引擎,利用解释执行或者是JIT解释成0101的数据给操作系统
1、运行时数据区
- 线程共享区
- 方法区
- 堆
- 线程私有
- 虚拟机栈
- 本地方法栈:
- 程序计数器:
程序计数器
- 当前线程正在执行的字节码指令的地址
- 内存区域中不会有OOM
- 为什么会有?时间片轮转、多线程,需要记录
虚拟机栈
- 组成:栈帧--> 局部变量表,操作数栈、动态连接、完成出口(返回地址)
- 局部变量表:存储8大基本数据类型和引用
- 操作数栈:存放方法的执行和操作
- 动态连接:涉及多态 ,用于判断当前的方法应该执行的是哪个态的方法
- 完成出口(返回地址) :记录的是方法返回以后应该继续执行哪里。这是正常情况,里面是根据程序计数器的地址来的,但如果是异常情况的话,就跟完成出口无关,而是走异常处理表
本地方法栈
- 保存的是native方法的信息
- 执行native方法时,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单的动态链接并直接调用native方法。程序计数器不会记录,是个null
2、方法区
- 存放类信息、常量、静态变量和即时编译期编译后额代码
- JDK>=1.8称为元空间,使用机器内存,大小基本不受限制,方便拓展;但可能会挤压堆空间
- JDK<=1.7 是永久代,内存受制于堆,会进行垃圾回收
3、Java堆
- 对象实例(几乎所有)
- 数组
为什么要分方法区和Java堆?
堆里面放的是一些经常变动需要回收的对象,方法区是一些静态的,不容易回收的信息,这是一种动静分离的思想
直接内存
不是虚拟机内存运行时数据区的一部分,也不是JVM规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用idrectByteBuffer对象直接引用并操作;
不受Java堆大小限制,但受本机总内存限制,也会OOM,也会被垃圾回收
NDK用的是直接内存
Intellij IDEA 在跑的时候可以设置JVM虚拟机参数,比如-Xms等
4、从底层深入理解运行时数据区
- JVM向操作系统申请内存,设置堆、方法区和栈的内存大小
- 进行类加载,class进入方法区
- 常量、静态变量 进入方法区
- 虚拟机栈---入栈帧
- 栈帧的方法执行,局部变量的引用进入栈帧的局部变量表
HSDB 工具 查看JVM内存
深入辨析堆和栈
功能
- 以栈帧的方式存储方法调用的过程,并存储方法调用过程中的基本数据类型的变量(int、short、long、byte、float、double、boolean、char)和对象的引用变量,其内存分配咋栈上,变量出了作用域就会释放
- 堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
线程独享还是共享
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程中可见,即栈内存可以理解成线程的私有内存
- 堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问
空间大小
- 栈内存要远远小于堆内存,栈的深度有限,会抛出StarkOverFlowError问题
5、内存溢出
- 栈溢出、堆溢出、方法区溢出、本机直接内存(堆外)溢出(ByteBuffer allocateDirect方法)
虚拟机优化技术
- 编译优化 --> 方法内联,就是运行的时候判断方法是不是直接的表达式,可以提取出来,这样就减少入栈出栈操作
- 栈的优化技术 -->栈帧之间数据共享,方法相互调用时有传参数,也就是操作数栈和另一个栈帧的局部变量表之间的共享
虚拟机中对象的创建过程
类加载 --> 检查加载 -->分配内存 --> 内存空间初始化 --> 设置 --> 对象初始化
6、对象的分配策略
几乎所有的对象都在堆上分配,此外满足一定条件还可以在栈上分配,也就是虚拟机栈上
对象分配策略
- 在new关键字创建对象的时候,会首先判断是否能栈上分配,判断条件是逃逸分析,就是这个对象不会逃出方法作用域,以及逃出当前线程,也就是被其他线程引用,没有的话就是没有逃逸,可以在栈上分配,当然大小不能太大
- 栈上分配的优点:栈上没有内存回收,效率高;
- 对应JVM设置:-XX: DoEscapeAnanlysis,默认是开启的
- 不能栈上分配的话,接着判断是否本地线程分配缓冲(TLAB)(占Eden 1%),是的话直接在Eden区分配;
- 没有在本地线程缓冲区分配的话,看是否是大对象,不是的话在Eden区分配,是的话直接去老年代
- 是否是大对象的话,可以设置JVM的参数,比如设置为10M,大于10M就是大对象
为什么新生代分为Eden from和to,并且是8:1:1
Eden区里只存放新生成的对象,经过一次GC后,存活的就会复制到from,然后from和to采用的是复制算法GC,复制算法GC效率高;
止于比例,大数据分析过90%的对象都会被回收,复制算法空间利用率只有50%,所以最后比例是8:1:1
为什么新生代的年龄最大是15?
因为对象的GC年龄存放在对象头中,用的是一个4位的数据,4位最多就是15;这个值可以通过JVM参数修改:-XX:MaxTenuringThreshold,改成15以下的,默认是15。CMS垃圾收集器默认是6
进入老年代后,这个GC分代年龄信息就没用了
空间分配担保
就是在对象晋级到老年代时,有可能老年代内存会不够,但如果每次老年代都进行GC又影响性能,所以JVM会进行担保,让老年代不用每次都GC,直到内存真不够时再进行GC,这样提高性能
动态年龄判断
如果from和to区里面大对象比较多,然后年龄又还没到15,这时候也会直接到老年代
7、常量池
静态常量池
- 字面量,比如String i = "abc",abc这个字面量
- 符号引用,比如String 这个类:java.lang.String
- 类的、方法的信息
运行时常量池
类加载 -- 运行时数据区 --- 方法区(逻辑区域)
实体类,符号引用 --> 直接引用(hash值)
运行时字符串常量JDK1.8后也在堆
字符串相关
String str = "ab" "cd" "ef"
首先会生成ab对象,再生成abcd对象,最后生成abcdef对象
//会先在常量池中创建king,并在堆里面创建一个a的String对象,并引用常量池中的King
String a = new String("king").intern();
//调用intern方法之后,会去常量池里查找是否有等于该字符串对象的引用,有就直接返回引用
String b = new String("King").intern();
所以 a == b 为true;如果没有加intern方法,则不为true,因为是2个String对象
8、面试题
什么情况下内存栈溢出?
- 无限递归,StackOverflowError
- 不断建立线程,JVM申请栈内存,机器没有足够的内存,OOM
9、问题
- 常量池是在方法区还是在堆 JDK1.8 运行时常量池(字符串部分放入堆),静态的都在方法区
- 普通成员变量在哪里? 是在堆里面,比如一个class的int成员变量,在new出来的时候,会跟随对象创建在堆里面
- 方法区:A类,类会在哪个时候卸载?回收?要同时满足下面的条件
满足以上条件只是能被回收,但不一定会被回收,比如有可能虚拟机的类的垃圾回收器被禁用了,-Xnoclassgc
- 类---所有的实例都要回收掉
- 加载该类的classload已经被回收
- 该类,Java.lang.class对象,没有任何地方被引用,并且不能通过反射访问该类的方法
public class Location {
//这个地方不会在常量池中创建
private String city;
private String region;
public static void main(String[] args) {
//JVM首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。
//这种方式可以减少同一个值的字符串对象的重复创建,节约内存。
String str ="abc";
//首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;
//其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,
// 在堆内存中创建一个 String 对象;最后,str1 将引用 String 对象。
String str1 =new String("abc");
//这里就跟第一步类似。
Location location = new Location();
location.setCity("深圳");
location.setRegion("南山");
//首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象
String str2= "ab" "cd" "ef";
//new Sting() 会在堆内存中创建一个a的String对象,
// “king"将会在常量池中创建
// 在调用intern方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
String a =new String("king").intern();
//调用 new Sting() 会在堆内存中创建一个b的String 对象,。
//在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
String b = new String("king").intern();
//所以 a 和 b 引用的是同一个对象。
if(a==b) {
System.out.print("a==b");
}else{
System.out.print("a!=b");
}
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
}
END
点赞转发,让精彩不停歇!关注我们,评论区见,一起期待下期的深度好文!