飞道的博客

深度解析JVM内存模型

307人阅读  评论(0)

下图总体概括了JVM的内存模型     

                   Java内存区域                                                                                                           

一:JVM类加载机制详解

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

    类的生命周期 

         类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程                    

  • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
  • 连接,连接又包含三块内容:验证、准备、初始化。 1、验证,文件格式、元数据、字节码、符号引用验证; 2、准备,为类的静态变量分配内存,并将其初始化为默认值; 3、解析,把类中的符号引用转换为直接引用
  • 初始化,为类的静态变量赋予正确的初始值
  • 使用,new出对象程序中使用
  • 卸载,执行垃圾回收

  二  类加载器

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的

          1、根类加载器(Bootstrap ClassLoader)

这个类将负责把<JAVA_HOME>\lib\目录中的,或者-Xbootclasspath参数指定的目录所指定的路径中的,并且是虚拟机识别的的类库加载到虚拟机内存中,如rt.jar,识别仅按照文件名识别,如果名字不符合,即使在这个目录下,也不会被加载。启动类加载器无法被java程序直接引,用户如果在编写自定义的类加载器时,如果需要把加载请求委托给引导类加载器,那么直接用null代替即可。

         2、扩展类加载器(Extension ClassLoader)

这个类加载器由sun.misc.Launcher $ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

        3、应用程序加载类(Application ClassLoader)

这个类加载器是由sun.misc.Launcher $App-ClassLoader实现。该加载器是由ClassLoader的getSystemClassLoader()方法返回,所以一般称它为系统类加载器。一般它加载用户类路径(ClassPath)所指定的类库,开发者一般直接使用这个类加载器,如果没有定义自己的类加载器,那么这个应用程序加载类就是程序中默认的类加载器。

案例:手写一个类加载器

1、编写一个java类  使用javac编译生成class文件

 

2、编写自定义类加载器


  
  1. public class MyClassLoader extends ClassLoader {
  2. private String path;
  3. public MyClassLoader(String path){
  4. this.path = path;
  5. }
  6. //用于寻找类文件
  7. @Override
  8. public Class findClass(String name){
  9. byte[] b =loadClassData(name);
  10. return defineClass(name,b,0,b.length);
  11. }
  12. public byte[] loadClassData(String name) {
  13. name = path + name + ".class";
  14. InputStream in = null;
  15. ByteArrayOutputStream out = null;
  16. try {
  17. in = new FileInputStream(new File(name));
  18. out = new ByteArrayOutputStream();
  19. int i = 0;
  20. while ((i = in.read()) != -1){
  21. out.write(i);
  22. }
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }finally {
  26. try {
  27. out.close();
  28. in.close();
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. return out.toByteArray();
  34. }
  35. public static void main(String[] args)
  36. throws IllegalAccessException, InstantiationException, ClassNotFoundException {
  37. MyClassLoader classloader = new MyClassLoader( "E:/project/");
  38. Class c = classloader.loadClass( "Test");
  39. System.out.println(c.getClassLoader());
  40. System.out.println(c.getClassLoader().getParent());
  41. System.out.println(c.getClassLoader().getParent().getParent());
  42. c.newInstance();
  43. }
  44. }

3、测试结果

 

三  双亲委派机制

 双亲委派机制工作过程:

如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类.而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。

 双亲委派模型的优点:

java类随着它的加载器一起具备了一种带有优先级的层次关系,例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱.

例如 当jvm要加载Test.class的时候,

  (1)首先会到自定义加载器中查找,看是否已经加载过,如果已经加载过,则返回字节码。

  (2)如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。

  (3)如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。

  (4)如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。

  (5)如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下("sun.boot.class.path")查看是否有Test.class字节码,有则返回,没有通

知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看。

  (6)依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException

 

ClassLoader的源码来看看双亲委派模式


  
  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name); //检查该类是否加载过了
  7. if (c == null) { //没加载过的情况
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. //如果自定义的类加载器的parent不为null,就调用parent的loadClass进行加载类
  12. c = parent.loadClass(name, false);
  13. } else {
  14. //否则就去找bootstrap ClassLoader
  15. c = findBootstrapClassOrNull(name);
  16. }
  17. } catch (ClassNotFoundException e) {
  18. // ClassNotFoundException thrown if class not found
  19. // from the non-null parent class loader
  20. }
  21. if (c == null) {
  22. // If still not found, then invoke findClass in order
  23. // to find the class.
  24. long t1 = System.nanoTime();
  25. c = findClass(name);
  26. // this is the defining class loader; record the stats
  27. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  28. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  29. sun.misc.PerfCounter.getFindClasses().increment();
  30. }
  31. }
  32. if (resolve) {
  33. resolveClass(c);
  34. }
  35. return c;
  36. }
  37. }

 

 

二:JVM 内存模型组成部分

          根据JVM规范共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分

 

1、虚拟机栈        

虚拟机栈是线程私有的内存空间,它和 Java 线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。可以这么理解,虚拟机栈针对当前 Java 应用中所有线程,都有一个其相应的线程栈,每一个线程栈都互相独立、互不影响,里面存储了该线程中独有的信息。           

2、方法区

方法区与java堆一样,是各个线程所共享的,它用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法区是jvm提出的规范,而永久代就是方法区的具体实现。java虚拟机对方法区的限制非常宽松,可以像堆一样不需要连续的内存可可选择的固定大小外,还可以选择不识闲垃圾收集,相对而言,垃圾收集行为在这边区域是比较少出现的。在方法区会报出 永久代内存溢出的错误。而java1.8为了解决这个问题,就提出了meta space(元空间)的概念,就是为了解决永久代内存溢出的情况,一般来说,在不指定 meta space大小的情况下,虚拟机方法区内存大小就是宿主主机的内存大小

3、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域

4、本地方法栈

与虚拟机栈发挥的作用非常类似,他们之间的区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError,OutOfMemorryError异常。

5、堆

堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 区和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。随着 Java 版本的更新,其内容又有了一些新的变化:在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了 Java8,永久代被 元空间 (处于本地内存)取代了。运行时常量池是位于元空间中,string的实例是放在堆内存中

 

三:元空间(metaspace

1、方法区与永久代,元空间之间的关系

方法区是一种规范,不同的虚拟机厂商可以基于规范做出不同的实现,永久代和元空间就是出于不同jdk版本的实现。说白了,方法区就像是一个接口,永久代与元空间分别是两个不同的实现类而已。只不过永久代是这个接口最初的实现类,后来这个接口一直进行变更,直到最后彻底废弃这个实现类,由新实现类——元空间进行替代。

2、永久代

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,说说为什么会内存益出:这一部分用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和和存放Instance的Heap区域不同,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。

    2.1、 持久代的大小

  • 它的上限是MaxPermSize,默认是64M
  • Java堆中的连续区域 : 如果存储在非连续的堆空间中的话,要定位出持久代到新对象的引用非常复杂并且耗时。卡表(card table),是一种记忆集(Remembered Set),它用来记录某个内存代中普通对象指针(oops)的修改。
  • 持久代用完后,会抛出OutOfMemoryError "PermGen space"异常。解决方案:应用程序清理引用来触发类卸载;增加MaxPermSize的大小。
  • 需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小

   2.2、为什么移除持久代

  • 它的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
  • HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
  • 简化Full GC:每一个回收器有专门的元数据迭代器。
  • 可以在GC不进行暂停的情况下并发地释放类数据。
  • 使得原来受限于持久代的一些改进未来有可能实现

    永久代最终被移除,运行时常量池存在于内存的元空间中字符串常量移至Java Heap。  PermSize 和 MaxPermSize 会被忽略并给出警告

3、metaspace元空间

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: 
  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 
  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: 
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

4、元空间替换永久代的原因

1、之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;
2、提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了;
3、隐患就是如果程序存在内存泄露,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。

 

四:队列和栈有什么区别

1、队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列采用的FIFO(first in first out),新元素(等待进入队列的元素)总是被插入到链表的尾部,而读取的时候总是从链表的头部开始读取。每次读取一个元素,释放一个元素。所谓的动态创建,动态释放。因而也不存在溢出等问题。由于链表由结构体间接而成,遍历也方便。(先进先出)

2、栈
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。栈就是一个桶,后放进去的先拿出来,它下面本来有的东西要等它出来之后才能出来(先进后出),栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。


               

兜兜转转大半辈子,才明白保持初心,无非就是自由自在地活着。

 至于其他种种纷扰,当我们想明白、肯放下的时候,自然能找到与自己、与他人和解的办法。

       


转载:https://blog.csdn.net/xiaolinzi176/article/details/116174161
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场