Java虚拟机内存区域划分以及垃圾回收机制了解一下
做为java的基础以及面试的高频题,当然要回顾一下
Java虚拟机内存划分
嗯,,等我去盗张图、、
图片摘自https://www.cnblogs.com/whgk/p/6138522.html
首先说一下啊程序计数器和本地方法栈
- 程序计数器呢是当前线程执行的字节码行号指示器,从这幅良心图中也可以看出,程序计数器是线程隔离的,生命周期和线程相同,其实也好理解,程序计数器是指示当前线程执行到哪一行的,所以当然不能共享。
- 本地方法栈呢,就是本地方法栈、、不要翻白眼,我再解释一下,某些方法,可能是c,c#等其他语言实现的,经过Java封装后就放到这里边作为本地方法,表现到程序中就是呢些被native修饰的方法。注意当执行native方法时,程序计数器为空,因为程序计数器中指示的是字节码信息,而本地方法不一定是Java程序,所以可能没有字节码信息,不知道这样理解对不对。
然后,另外三个剪不断理还乱的区域才是重头戏
先看一行代码
void test() { Car c=new Car(); }
- jvm栈:虚拟机栈为每个方法建立一个栈帧,放置方法中的局部变量,如上述代码在执行时会在jvm中为test建立栈帧,而实例变量指针c就放在栈中。
- 堆:堆最常用的就是存放各种new的对象,也就是c指向的实例对象其实是存储在堆中的,包括该实例对象的实例变量
- 方法区:需要注意的是JDK8之前,由永久代实现,主要存放类的信息(例如访问控制信息,版本信息)、类变量(静态方法)、常量池、类中的方法等;JDK8之后,取消了永久代,提出了元空间,并且常量池、静态成员变量等迁移到了堆中;元空间不在虚拟机内存中,而是放在本地内存中。那么,方法区是不是就不属于虚拟机内存的一部分了?还是元空间只是方法区的一部分,还有一部分东西存放在方法区中?待了解。
笔者总结,如有错误,欢迎指正。
栈中为各方法建立一个栈帧,存放方法的局部变量表,方法出口信息等,以及对类实例的引用。
堆中放置实例对象的信息,包括实例的变量
方法区中存放类的完整信息,包括类的字节码信息,以及类中的方法、类中final修饰的常量和字面量(放在类型常量池中),类的静态变量
以下示例引自https://blog.csdn.net/u013241673/article/details/78574770
1 public class PersonDemo 2 { 3 public static void main(String[] args) 4 { //局部变量p和形参args都在main方法的栈帧中 5 //new Person()对象在堆中分配空间 6 Person p = new Person(); 7 //sum在栈中,new int[10]在堆中分配空间 8 int[] sum = new int[10]; 9 } 10 } 11 12 13 class Person 14 { //实例变量name和age在堆(Heap)中分配空间 15 private String name; 16 private int age; 17 //类变量(引用类型)name1和"cn"都在方法区(Method Area) 18 private static String name1 = "cn"; 19 //类变量(引用类型)name2在方法区(Method Area) 20 //new String("cn")对象在堆(Heap)中分配空间 21 private static String name2 = new String("cn"); 22 //num在堆中,new int[10]也在堆中 23 private int[] num = new int[10]; 24 25 26 Person(String name,int age) 27 { 28 //this及形参name、age在构造方法被调用时 29 //会在构造方法的栈帧中开辟空间 30 this.name = name; 31 this.age = age; 32 } 33 34 //setName()方法在方法区中 35 public void setName(String name) 36 { 37 this.name = name; 38 } 39 40 //speak()方法在方法区中 41 public void speak() 42 { 43 System.out.println(this.name+"..."+this.age); 44 } 45 46 //showCountry()方法在方法区中 47 public static void showCountry() 48 { 49 System.out.println("country="+country); 50 } 51 }
以下示例引自https://www.cnblogs.com/dreamroute/p/5946272.html
1 String s1 = "Hello"; 2 String s2 = "Hello"; 3 String s3 = "Hel" + "lo"; 4 String s4 = "Hel" + new String("lo"); 5 String s5 = new String("Hello"); 6 String s6 = s5.intern(); 7 String s7 = "H"; 8 String s8 = "ello"; 9 String s9 = s7 + s8; 10 11 System.out.println(s1 == s2); // true 12 System.out.println(s1 == s3); // true 13 System.out.println(s1 == s4); // false 14 System.out.println(s1 == s9); // false 15 System.out.println(s4 == s5); // false 16 System.out.println(s1 == s6); // true
首先说明一点,在java 中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。
s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。
s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello";,所以s1 == s3成立。
s1 == s4当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同。
s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,所以不做优化,等到运行时,s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。
s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。
至此,我们可以得出三个非常重要的结论:
必须要关注编译期的行为,才能更好的理解常量池。
运行时常量池中的常量,基本来源于各个class文件中的常量池。
程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。
Java垃圾回收
Java把堆空间分为新生代(采用 复制算法)和老年代(采用标记整理算法),新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成,结构图如下所示:
1、系统在eden区中创建对象,Eden区满触发一次youngGC/minorGC,把还有用的对象复制到from区,并将整个Eden区处理干净
2、当Eden区再次被用完时,将Eden和from中的对象复制到to区,并将Eden和from清理干净
3、Eden再满时,将Eden和to区复制到from,并清理Eden和to区
4、经过若干次youngGC后,某些一直在from和to区游荡的对象被复制到老年代
5、当老年代也用完时,进行一次fullGC
注意Java8中取消了永久代,取而代之的是元空间,二者的区别是永久代物理上是堆的一部分,和新生代老年代地址是连续的,属于Java虚拟机中的空间。而元空间属于本地内存的一部分。
与堆内存相关的JVM参数有:
- -Xms:设置Java应用程序启动时的初始堆大小
- -Xmx:设置Java应用程序能获得的最大堆大小
- -XX:NewSize:设置新生代的大小
- -XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小
- -XX:SurviorRatio:新生代中eden区与survivior区的比例
- -XX:MaxPermSize:设置最大的持久区的大小
- -XX:PermSize:设置永久区的初始值
- -XX:MaxMetaspaceSize=128m 设置最大的元内存空间128兆