《深入分析JavaWeb技术内幕》之 8-深入分析ClassLoader工作机制

8.1物理内存和虚拟内存

所谓物理内存就是我们通常所说的RAM(随机存储器)。在计算机中,还有一个存储单元叫寄存器,它用于存储计算单元执行指令(如浮点、整数等运算时)的中间结果。寄存器的大小决定了一次计算可使用的最大数值。

连接处理器和RAM或者处理器和寄存器的是地址总线,这个地址总线的宽度影响了物理地址的索引范围,因为总线的宽度决定了处理器一次可以从寄存器或者内存中获取多少个bit。同时也决定了处理器最大可以寻址的地址空间,如32位地址总线可以寻址的范围为0x0000 0000-0xffff ffff。这个范围是2^32=4294967296个内存位置,每个地址会引用一个字节,所以32位总线宽度可以有4GB的内存空间。

除了在学校的编译原理的实践课或者要开发硬件程序的驱动程序时需要直接通过程序访问存储器外,我们大部分情况都调用操作系统提供的接口来访问内存,在java中甚至不需要写和内存相关的代码。

不管是在Windows系统还是Linxu系统下,我们要运行程序,都要向操作系统先申请内存地址。通常操作系统管理内存的申请空间是按照进程来管理的,每个进程拥有一段独立的地址空间,每个进程之间不会相互重合,操作系统也会保证每个进程只能访问自己的内存空间。这主要是从程序的安全性来考虑,也便于操作系统来管理物理内存。

8.3在Java中那些组件需要使用内存

Java堆

线程

JVM运行实际程序的实体是线程,当然线程需要内存空间来存储一些必要的数据。每个线程创建时JVM都会为它创建一个堆栈,堆栈的大小根据不同的JVM实现而不同,通常在256K~756K之间。

线程所占空间相比堆空间来说比较小。但是如果县城过多,线程堆栈的总内存使用量可能也非常大。当前有很多应用程序根据CPU的核数来分配创建的线程数,如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致比较差的性能和更高的内存占用率。

堆外内存

堆外内存会自动清理本机缓冲区,但这个过程只能作为Java堆GC的一部分来执行,因此它们不会自动相应施加在本机堆上的压力。GC仅在Java堆被填满,以至于无法为堆分配请求提供服务时发生,或者在Java应用程序中显示请求时发生。可以通过调用System.gc()来释放堆外内存。但是这种方式会影响程序的性能,因为会增加GC的次数,一般情况下通过设置-XX:+DisableExplicitGC来控制System.gc()的影响,但是又会导致堆外内存泄漏问题。

JNI

JNI技术使得本机代码(如C语言程序)可以调用Java方法,也就是通常所说的native memory。实际上Java运行时本身也依赖于JNI代码来实现类库功能,如文件操作、网络I/O操作或者其他系统调用。所以JNI也会增加Java运行时的本机内存占用。

8.5.2java中的内存分配详解

从前面的JVM内存结构的分析我们可知,JVM内存分配主要基于两种,分别是堆和栈。先来说说java栈是如何分配的。

java栈的分配是和线程绑定在一起的,当我们创建一个线程时,很显然,JVM就会为这个线程创建一个新的Java栈,一个线程的方法调用和返回对应于这个Java栈的压栈和出栈。当线程激活一个Java方法时,JVM就会在线程额Java堆栈里新压入一个帧,这个帧自然成了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其他数据。

栈中主要存放一些基本类型的变量数据(int、short、long、byte、float、double、boolean、char)和对象句柄(引用)。存取速度比堆要快,仅次于寄存器,栈数据可以共享。缺点是:存在栈中的数据大小与生存期必须是确定的,这也导致缺乏了其灵活性。缺点是:存在栈中的数据大小与生存期是确定的,这也导致缺乏了其灵活性。

如下这段代码:

    public void stack(String[] arg) {
        String str = "junshan";
        if (str.equals("junshan")) {
            int i = 3;
            while (i > 0) {
                long j = 1;
                i--;
            }
        } else {
            char b = 'a';
            System.out.println(b);
        }
    }

这段代码的stack方法中定义了多个变量,这些变量在运行时需要存储空间,同时在执行指令时JVM也需要知道操作栈的大小,这些数据都会在javac编译这段代码时就已经确定,下面是这个方法对应的class字节码:

在这个方法的attribute中就已经知道stack和local variable的大小,分别是2和6.还有一点不得不提,就是这里的大小指定的是最大值,为什么是最大值呢?因为JVM在真正执行时分配的stack和local variable的空间是可以共用的。举例来说,上面的6个localvariable除去变量0是this指针外,其他的5个都是在这个方法中定义的,这6个变量需要的Slot是1+1+1+1+2+1,但是实际用到的只有4个,这是因为不同的变量作用范围如果没有重合,Slot则可以重复使用。

下面这段代码描述对象是如何在堆上分配内存的:

    public static void main(String[] args) {
        new String("hello world");
    }

 上面的代码创建了一个String对象,这个String对象将会在堆上分配内存,JVM创建对象的字节码指令如下:

先执行new指令,这个new指令根据后面的16位的“#2”常量池索引创建指定类型的对象,而该#2索引所指向的入口类型首先必须是类类型,然后JVM会为这个类的新对象分配一个空间,这个新对象的属性值都设置为默认值,最后将执行这个新对象的objectref引用压入栈顶。

new指令执行完成后,得到的对象还没有初始化,所以这个新对象并没有创建完成。这个对象的引用在这时不应该复制给str变量,因为invokespecial会消耗掉操作数栈顶的引用作为传给构造器的“this”参数,所以如果我们希望在invokespecial调用后在操作数栈顶还维持有一个指向新建对象的引用,就得在invokespecial之前先“复制”一份引用——这就是这个dup的来源。

在新对象初始化完成后再将这个引用赋值给本地变量。调用构造函数是通过invokespecial指令完成的,构造函数如果有参数要传递,则先将参数压栈。构造函数执行完成后再objectref的对象引用赋值为本地变量1,这样一个新对象才创建完成。

在编程中,如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量、形式参数都是从栈中非配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带一样,栈指针会自动指引你到放东西的位置,你所要的就是把东西放下来就行。在退出函数时,修改栈指针就可以把栈中的内容销毁。这样的模式速度最快。、

堆在应用程序运行时请求操作系统给自己分配内存,由于操作系统管理内存分配,所以在分配和销毁时都要占用时间,因此用哦个堆的效率非常低。但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据在堆里停留多长时间,因此,用堆保存数据时会得到更大的灵活性。当然,为达到灵活性,必然也会付出一定代价——在堆里分配存储空间时会花掉更长的时间。

8.6垃圾回收

8.6.3如何检测垃圾

不能通过跟对象可达的就是垃圾对象,那么这个跟对象集合中都是些什么呢?虽然跟对象和JVM的具体实现也有关系,但是大都会包含如下一些元素。

  • 在方法中局部变量区的对象的引用:这些跟对象直接存储在栈帧的局部变量区中。
  • 在Java操作栈中的对象引用:有些对象是直接在操作栈中持有的,所以操作栈肯定也包含根对象集合。
  • 在metadata space元数据区中的对象引用。
  • 在本地方法中持有的对象引用:有些对象被传入本地方法中,但是这些对象还么有被释放。
  • 类的Class对象:当每个类被JVM加载时都会创建一个代表这个类的唯一数据类型的Class对象,而这个Class对象也同样存放在堆中,而这个类不再被使用时,metadata space中类数据和这个Class对象同样需要被回收。
  •  

寄存器

方法区

本地方法栈

常量池

分配策略

内存回收策略:

静态内存分配与回收:编译时确定

动态内存分配与回收:运行时确定

 

检测垃圾:

根可达,活动对象

 

分代垃圾收集:

在young区的survivor中进行from———— to的替换

 

收集算法:

一、serial collector

 

二、 parallel collector

1、ParNewGC

2、parallelGC

3、parallelOldGC

 

三、CMS collector

问题分析:

 

1、GC日志分析

2、堆快照文件分析

3、JVMCrash日志分析

猜你喜欢

转载自blog.csdn.net/weixin_41395565/article/details/82990250
今日推荐