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 }
View Code

以下示例引自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
View Code

首先说明一点,在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地址相同。
jvm常量池,堆,栈内存分布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兆

猜你喜欢

转载自www.cnblogs.com/April1995/p/9560848.html