精华内容
下载资源
问答
  • JAVA内存管理

    2021-02-28 13:02:04
    被问到有关Java内存管理的知识,所以要搜集整理一下了。开始之前,我们要明白一点,我们所使用的变量就是一块一块的内存空间!!一、内存管理原理:在java中,有java程序、虚拟机、操作系统三个层次,其中java程序与...

    被问到有关Java内存管理的知识,所以要搜集整理一下了。开始之前,我们要明白一点,我们所使用的变量就是一块一块的内存空间!!

    一、内存管理原理:

    在java中,有java程序、虚拟机、操作系统三个层次,其中java程序与虚拟机交互,而虚拟机与操作系统间交互!这就保证了java程序的平台无关性!下面我们从程序运行前,程序运行中、程序运行内存溢出三个阶段来说一下内存管理原理!

    1、程序运行前:JVM向操作系统请求一定的内存空间,称为初始内存空间!程序执行过程中所需的内存都是由java虚拟机从这片内存空间中划分的。

    2、程序运行中:java程序一直向java虚拟机申请内存,当程序所需要的内存空间超出初始内存空间时,java虚拟机会再次向操作系统申请更多的内存供程序使用!

    3、内存溢出:程序接着运行,当java虚拟机已申请的内存达到了规定的最大内存空间,但程序还需要更多的内存,这时会出现内存溢出的错误!

    至此可以看出,Java 程序所使用的内存是由 Java 虚拟机进行管理、分配的。Java 虚拟机规定了 Java 程序的初始内存空间和最大内存空间,开发者只需要关心 Java 虚拟机是如何管理内存空间的,而不用关心某一种操作系统是如何管理内存的。

    二、 RUNTIME 类的使用:

    Java 给我们提供了Runtime 类得到JVM 内存的信息

    方法名称

    参数

    作用

    返回值

    getRuntime

    获取 Runtime 对象

    Runtime 对象

    totalMemory

    获取 JVM 分配给程序的内存数量

    long:内存数量

    freeMemory

    获取 当前可用的内存数量

    long:内存数量

    maxMemory

    获取 JVM 可以申请到的最大内存数量

    long:内存数量

    三、内存空间逻辑划分:

    JVM 会把申请的内存从逻辑上划分为三个区域,即:方法区、堆与栈。

    方法区:方法区默认最大容量为64M,Java虚拟机会将加载的java类存入方法区,保存类的结构(属性与方法),类静态成员等内容。

    堆:默认最大容量为64M,堆存放对象持有的数据,同时保持对原类的引用。可以简单的理解为对象属性的值保存在堆中,对象调用的方法保存在方法区。

    栈:栈默认最大容量为1M,在程序运行时,每当遇到方法调用时,Java虚拟机就会在栈中划分一块内存称为栈帧(Stack frame),栈帧中的内存供局部变量(包括基本类型与引用类型)使用,当方法调用结束后,Java虚拟机会收回此栈帧占用的内存。

    四、java数据类型

    33db0069bafe588ddf0f29811cdab071.png

    1、基本数据类型:没封装指针的变量。

    声明此类型变量,只会在栈中分配一块内存空间。

    2、引用类型:就是底层封装指针的数据类型。

    他们在内存中分配两块空间,第一块内存分配在栈中,只存放别的内存地址,不存放具体数值,我们也把它叫指针类型的变量,第二块内存分配在堆中,存放的是具体数值,如对象属性值等。

    3、下面我们从一个例子来看一看:

    public classStudent {

    String stuId;

    String stuName;intstuAge;

    }public classTestStudent {public static voidmain(String[] args) {

    Student s= newStudent();

    String name= new String("云鹤");int a = 10;char b = 'm';

    s.stuId= "6363";

    s.stuName= "刘德华";

    s.stuAge= 25;

    }

    }

    (1)类当然是存放在方法区里面的。

    (2)Student s = new Student();

    这行代码就创建了两块内存空间,第一个在栈中,名字叫s,它就相当于指针类型的变量,我们看到它并不存放学生的姓名、年龄等具体的数值,而是存放堆中第二块内存的地址,第二块才存放具体的数值,如学生的编号、姓名、年龄等信息。

    (3)int a = 10;

    这是 基本数据类型 变量,具体的值就存放在栈中,并没有只指针的概念!

    下图就是本例的内存布置图:

    a9e8f00c27e1cc57d86282b7fc450ad5.png

    此外我们还要知道Student s = new Student(); 包括了声明和创建,即:

    声明:Student s;

    创建:s = new Student();

    其中声明只是在栈中声明一个空间,但还没有具体的值,声明后的情况如下图所示:

    6901b9b9f9d3784222b5e398fa21f1b6.png

    创建后的情况如下图所示:

    4b6274aa13484c3fc331cc51efc19301.png

    (4)引用类型中的数组也封装了指针,即便是基本数据类型的数组也封装了指针,数组也是引用类型。比如代码int[] arr = new int[]{3, 6, 12, 9, 66, 31};如下图所示:

    b738370d9b742ad88bb867d8c55fcee3.png

    五、java值传参与引用传参

    (1)参数根据调用后的效果不同,即是否改变参数的原始数值,又可以分为两种:按值传递的参数与按引用传递的参数。

    按值传递的参数原始数值不改变,按引用传递的参数原始数值改变!这是为什么呢?其实相当简单:

    我们知道基本数据类型的变量存放在栈里面,变量名处存放的就是变量的值,那么当基本数据类型的变量作为参数时,传递的就是这个值,只是把变量的值传递了过去,不管对这个值如何操作,都不会改变变量的原始值。而对引用数据类型的变量来说,变量名处存放的地址,所以引用数据类型的变量作为传参时,传递的实际上是地址,对地址处的内容进行操作,当然会改变变量的值了!

    正常情况下,我们用数组测试TestArray类如下:

    public classTestArray {void change(int[] arr) {for(int i=0;i

    arr[i]=1000;

    System.out.println("方法体内修改值后:");for(int i=0;i

    System.out.println(arr[i]);

    }public static voidmain(String[] args) {int[] a = {1,2,3,4};

    TestArray testString= newTestArray();

    System.out.println("方法调用前:");for(int i=0;i

    System.out.println(a[i]);

    testString.change(a);

    System.out.println("方法调用后:");for(int i=0;i

    System.out.println(a[i]);

    }

    }

    输出结果如下:

    方法调用前:1

    2

    3

    4方法体内修改值后:1000

    2

    1000

    4方法调用后:1000

    2

    1000

    4

    数组实际上也是引用类型,在调用函数的过程中改变了其值。

    (2)特例:String

    public classTestString {voidchange(String str) {

    str= "吴奇隆";

    System.out.println("方法体内修改值后:" +str);

    }public static voidmain(String[] args) {

    String name= "歌星";

    TestString testString= newTestString();

    System.out.println("方法调用前:" +name);

    testString.change(name);

    System.out.println("方法调用后:" +name);

    }

    结果:

    方法调用前:歌星

    方法体内修改值后:吴奇隆

    方法调用后:歌星

    分析:

    上例中,虽然参数String 是引用数据类型,但其值没有发生改变,这是因为String 类是final 的,它是定长,不允许对其进行改变,而StringBuffer(多线程下使用性能优)和StringBuilder(单线程下面使用性能优)是可以改变的。如果这里用StringBuffer和SringBuiler替代,结果和Array的使用一样,中间结果会被改变。

    我们看初始情况,即String name = "歌星";这行代码运行

    完,如下图:

    e87c4ea4cde995e28b52fe41a3314a33.png

    当调用方法时testString.change(name),内存变化为:

    c2e7f0be8819b7e3a45a44119e556459.png

    在方法体内,参数str赋予一个新值,str = "吴奇隆"。因为"吴奇隆"这个String是定长,系统就会在堆中分配一块新的内存空间37DF,这样str指向了新的内存空间37DF,而name还是指向36DF, 37DF的改变对它已没影响:

    ae16827b526d6b93c02a9784730c1b03.png

    最后,方法调用结束,str与37DF的内存空间消亡。Name的值依然为歌星,并没有改变。

    所以String虽然是引用类型参数,但值依然不变:

    673511d38f5a54fa1f1eae22239f3132.png

    (3)无法交换的例子:

    public classTestChange {voidchange(Student stu1, Student stu2) {

    stu1.stuAge++;

    stu2.stuAge++;

    Student stu=stu1;

    stu1=stu2;

    stu2=stu;

    }public static voidmain(String[] args) {

    Student z= newStudent();

    z.stuName= "张信哲";

    z.stuAge= 40;

    Student r= newStudent();

    r.stuName= "任贤齐";

    r.stuAge= 30;

    System.out.println("交换前z:\t"+z.stuName+"\t"+z.stuAge);

    System.out.println("交换前r:\t"+r.stuName+"\t"+r.stuAge);

    TestChange testChange= newTestChange();

    testChange.change(z, r);

    System.out.println("交换后z:\t"+z.stuName+"\t"+z.stuAge);

    System.out.println("交换后r:\t"+r.stuName+"\t"+r.stuAge);

    }

    }

    运行结果:

    交换前z: 张信哲 40交换前r: 任贤齐30交换后z: 张信哲41交换后r: 任贤齐31

    93c1592bb02995e4c71af5fe8c9de7be.png

    806839f7cd5c3e4c322c97b0445b70cb.png

    展开全文
  • Java内存管理

    2021-03-29 10:53:54
    Java内存管理 1. Java程序的内存分配 要了解java内存分配,首先要了解进行内存分配时,java程序,java虚拟机,操作系统之间的关系。 Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而...

    1. Java程序的内存分配

    要了解java内存分配,首先要了解进行内存分配时,java程序,java虚拟机,操作系统之间的关系。

    Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关。

    java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。根据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。

    1. 有些语言在执行的过程当中会直接的向操作系统申请内存空间,而java语言为了保证其平台的无关性,而不是直接向操作系统申请内存空间,而是由java虚拟机向操作系统请求一定的内存空间,开始请求的内存空间的大小称之为初始内存空间。
    2. 当程序所需的内存空间超过了初始内存空间,java虚拟机会再次向操作系统申请内存。
    3. 如果java虚拟机已经申请的内存达到了规定的最大内存,但程序还需要更多的内存,这时会造成内存溢出(outOfMemery)的错误。

    如下图:
    在这里插入图片描述在这里插入图片描述在这里插入图片描述

    2. Java虚拟机对内存空间的管理

    java程序中的哪些内容会占用内存空间:类结构、对象中的数据、变量(包括基本类型和引用类型)等都会占用内存。JVM为了方便管理,java程序将内存分为五块,寄存器本地方法区方法区栈内存堆内存

    • 程序计数器(Program Counter Register)
      用来指示执行哪条指令。
      程序计数器的作用可以看做是当前线程所执行的字节码的行号指示。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空。
    • 本地方法区(Native Method Stack)
      Java栈是为执行java方法服务的,而本地方法栈是为执行本地方法服务的。
    • 方法区(Method Area)
      方法区也叫共享数据区、静态区,默认大小为64M,存放类的结构,即类名、全局变量、静态变量、字符串常量等,还有方法,不释放。
    • 栈内存(VM Stack)
      Java栈,为线程私有,每个线程对应一个java栈(并为其分配一个程序计数器、虚拟机栈和本地方法栈),当线程终止时,三者所占有的内存空间也被释放掉。
      栈内存的默认大小为1M,存储的都是局部变量(基本数据类型的变量,引用数据类型的地址…),变量所属的作用域一旦结束,该变量就自动释放。
      程序在执行的过程中,每遇到一个方法,会为该方法分配一个栈帧,该栈帧主要负责存储该方法的局部变量。
    • 堆内存(Heap)
      Java堆,是被所有线程共享的一块内存区域,默认大小是64M,主要存放new的对象和数组(数组也是对象),JVM不定时查看这个对象,如果没有引用指向这个对象就回收。
      (1)每一个实体都有首地址值。
      (2)堆内存中的每一个变量都有默认初始化值,根据类型的不同而不同。整数是0,小数0.0或者0.0f,boolean false, char ‘\u0000’。
      (3)垃圾回收机制。

    了解值传递与引用传递
    方法参数的传递方式分为值传递和引用传递。
    (1)值传递:如果方法的参数是基本数据类型,该参数的传递方式为值传递,java虚拟机会将参数的值拷贝到新的栈帧中。
    (2)引用传递:如果方法的参数是一个引用类型的变量,该参数是引用传递,java虚拟机为了减少内存浪费,提高运行效率,不会将整个对象拷贝到新的栈帧中,而是将对象在堆中的地址拷贝到新的栈帧。

    JVM内存情况示例

    package user;
    
    public class User {
    
    	public String name;
    	public String password;
    	public static int i;
    	public int j;
    	
    	public void changeName(User user) {
    		user.name="小明";
    		i++;
    	}
    	public void change(int a ,User user) {
    		user = new User();
    		user.name = "小黑";
    		user.password = "123456";
    		j++;
    		a--;
    	}
    	public void display() {
    		System.out.println(name);
    		System.out.println(password);
    		System.out.println(i);
    		System.out.println(j);
    	}
    }
    
    package user;
    
    public class UserTest {
    
    	public static void main(String[] args) {
    		int i = 10;
    		User u = new User();
    		u.change(i, u);
    		u.changeName(u);
    		u.display();
    	}
    }
    

    当我们运行程序时,JVM会把User类以及UserTest类编译完然后加载到JVM中的方法区,类的成员变量以及成员方法也被加载到方法区,此时内存模型图如下:
    在这里插入图片描述接着JVM会自动寻找main方法,在栈中为main方法分配一个栈帧用来存储该方法的局部变量,将局部变量i存入栈中,然后执行UserTest类中的代码:

    User u = new User();
    

    此时,由于new了对象,JVM在堆空间中划分一块内存给User类对象u,并为其分配一个内存地址,(这里若对象的成员变量没有赋初值,则JVM会为其赋初值),在栈中分配一块内存空间用于指向堆空间中对象的内存地址。此时内存模型图如下:
    在这里插入图片描述接下来,User类对象调用了change方法,并且参数为i,u。此时栈会为change方法分配一个栈帧来存储该方法的局部变量。

    public void change(int a ,User user) {
    		user = new User();
    		user.name = "小黑";
    		user.password = "123456";
    		j++;
    		a--;
    	}
    

    该方法内创建了对象,则在堆内存中划分空间存储该对象属性,属性只有两个,j++属于U对象的变量,传参时int a 变为i,这里的i是main方法中的局部变量,由于change方法结束后会弹栈,释放栈空间,对象的引用没有指向后,堆中的属性会被垃圾回收机制处理掉,故此时i的值减一,j的值加1。此时内存模型图如下:
    在这里插入图片描述接下来调用了changeName方法,

    public void changeName(User user) {
    		user.name="小明";
    		i++;
    	}
    

    此时U对象的name属性被重新赋值,从NULL变为“小明”,这里的i即为User类中的静态变量i,对其进行加1操作。此时内存模型图如下:
    在这里插入图片描述最后调用display方法

    public void display() {
    		System.out.println(name);
    		System.out.println(password);
    		System.out.println(i);
    		System.out.println(j);
    	}
    

    此时内存模型图如下:
    在这里插入图片描述Main方法中所有代码执行完毕,main方法所占用的栈空间也被收回,而堆空间等待GC回收。代码执行结果如下:
    在这里插入图片描述

    3. GC机制

    有关java垃圾回收机制的详细内容也可查看以下博客:
    Java垃圾回收机制详解
    垃圾回收器(Garbage Collector)
    作用:垃圾收集器的工作目标是回收已经无用的对象的内存空间,从而避免内存渗漏体的产生,节省内存资源,避免程序代码的崩溃。垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。

    3.1 GC算法

    (1)标记-清除(Mark-Sweep)
    标记-清除(Mark-Sweep)算法,分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

    标记-清除算法主要问题是:
    ① 效率问题,标记和清除过程的效率很低;
    ② 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

    (2)复制(Copying)算法

    复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

    复制算法的主要问题是:
    ① 复制算法将内存缩小为原来的一半,过于浪费
    ② 对象存活率较高时就要执行较多的复制操作,造成频繁GC,效率将会变低

    (3)标记-整理(Mark-Compact)

    标记-整理算法的标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这样连续的内存空间就比较多了。

    (4)分代回收(generational collection)

    程序创建的大部分对象的生命周期都很短,只有一小部分对象的生命周期比较长,根据这样的规律,一般把Java堆分为Young Generation(新生代),Old Generation(老年代)和Permanent Generation(持久代),上面几种算法是通过分代回收混合在一起的,这样就可以根据各个年代的特点采用最适当的回收算法。

    ① 新生代

    在新生代中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to), 这两个区域大小相等,相当于copying算法中的两个区域,它们用来存放每次垃圾回收后存活下来的对象。在新生代中,垃圾回收一般用Copying的算法,速度快。
    当新建对象无法放入eden区时,将触发minor collection(minorGC 是清理新生代的GC线程,eden的清理,from、to的清理都由MinorGC完成),将eden区与from区的存活对象复制到to区,经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象;当eden区再次满时,minor collection将eden区和to区的存活对象复制到from区,eden区和to区被清空,from区存放eden区和to区的存活对象,就这样from区和to区来回切换。如果进行minor collection的时候,发现to区放不下,则将eden区和from区的部分对象放入成熟代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象到成熟代。

    ② 老年代

    在成熟代中主要存放应用程序中生命周期长的内存对象,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。如果成熟代放满对象,无法从新生代移入新的对象,那么将触发major collection(major GC清理整合OldGen的内存空间)。

    ③ 永久代

    在永久代中,主要用来放JVM自己的反射对象,比如类对象、方法对象、成员变量对象、构造方法对象等。
    此外,垃圾回收一般是在程序空闲的时候(没有工作线程,GC线程优先级较低)或者堆内存不足的时候自动触发,也可以调用System.gc()主动的通知Java虚拟机进行垃圾回收,但这只是个建议,Java虚拟机不一定马上执行,启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用。

    3.2 触发GC的条件

    (1)GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行时被调用。但下面的条件例外。

    (2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制调用GC线程。若GC一次之后仍不能满足内存分配,JVM会再进行两次GC,若仍无法满足要求,则JVM将报“out of memory”的错误,Java应用将停止。

    3.3 减少GC开销的措施

    (1)不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数,大大的影响系统性能。
    (2)尽量减少临时对象的使用。临时对象在跳出函数调用后会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。
    (3)对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。
    (4)尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。
    (5)能用基本类型如int,long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。
    (6)尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
    (7)分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

    展开全文
  • Java内存管理方法小结

    2021-03-05 21:51:23
    这里向大家简单介绍一下Java内存管理的概念和方法,Java内存管理就是对象的分配和释放问题。首先看一下分配和释放的概念,分配:内存的分配是由程序完成的,程序员需要通过关键字new为每个对象申请内存空间(基本类型...

    这里向大家简单介绍一下Java内存管理的概念和方法,Java内存管理就是对象的分配和释放问题。首先看一下分配和释放的概念,分配:内存的分配是由程序完成的,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间;而对象的释放是由垃圾回收机制决定和执行的。

    Java内存管理总结

    1.Java是如何管理内存的

    Java内存管理就是对象的分配和释放问题。

    分配:内存的分配是由程序完成的,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。

    释放:对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

    2.什么叫Java的内存泄露

    在Java内存管理中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连(也就是说仍存在该内存对象的引用);其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

    与C++内存泄露的区别:

    50d240b060cc530c7df9f4ccc581f24b.png

    在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

    3.JVM的内存区域组成

    Java把内存分两种:一种是栈内存,另一种是堆内存

    1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配;

    2。堆内存用来存放由new创建的对象和数组以及对象的实例变量

    在函数(代码块)中定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由Java虚拟机的自动垃圾回收器来管理

    3。栈的优缺点

    堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。缺点就是要在运行时动态分配内存,存取速度较慢;

    栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。另外,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

    4。Java内存管理中数据如何存储

    a)基本数据类型

    Java内存管理中的基本数据类型共有8种,即int,short,long,byte,float,double,boolean,char(注意,并没有string的基本类型)。这种类型的定义是通过诸如inta=3;longb=255L;的形式来定义的。如inta=3;这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

    另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。

    比如:我们同时定义:

    inta=3;

    intb=3;

    编译器先处理inta=3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理intb=3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

    定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

    b)对象

    在Java内存管理中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。

    假设有类Rectangle定义如下:

    Java代码

    classRectangle{

    doublewidth,height;

    Rectangle(doublew,doubleh){

    wwidth=w;

    hheight=h;

    }

    }

    classRectangle{

    doublewidth,height;

    Rectangle(doublew,doubleh){

    wwidth=w;

    hheight=h;

    }

    }

    (1)明对象时的内存模型

    用Rectanglerect;声明一个对象rect时,将在栈内存为对象的引用变量rect分配内存空间,但Rectangle的值为空,称rect是一个空对象。空对象不能使用,因为它还没有引用任何“实体”。

    (2)对象实例化时的内存模型

    当执行rect=newRectangle(3,5);时,会做两件事:

    在堆内存中为类的成员变量width,height分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。

    返回堆内存中对象的引用(相当于首地址)给引用变量rect,以后就可以通过rect来引用堆内存中的对象了。

    c)创建多个不同的对象实例

    一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态。例如:

    Java代码

    Rectangler1=newRectangle(3,5);

    Rectangler2=newRectangle(4,6);

    Rectangler1=newRectangle(3,5);

    Rectangler2=newRectangle(4,6);

    此时,将在堆内存中分别为两个对象的成员变量width、height分配内存空间,两个对象在堆内存中占据的空间是互不相同的。如果有:

    Java代码

    Rectangler1=newRectangle(3,5);

    Rectangler2=r1;

    Rectangler1=newRectangle(3,5);

    Rectangler2=r1;

    则在堆内存中只创建了一个对象实例,在栈内存中创建了两个对象引用,两个对象引用同时指向一个对象实例。

    d)包装类

    Java内存管理中数据的基本型别都有对应的包装类:如int对应Integer类,double对应Double类等,基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。例如:inti=0;i直接存储在栈中。Integeri(i此时是对象)=newInteger(5);这样,i对象数据存储在堆中,i的引用存储在栈中,通过栈中的引用来操作对象。

    e)String

    String是一个特殊的包装类数据。可以用用以下两种方式创建:

    1.Stringstr=newString("abc");

    2.Stringstr="abc";

    第一种创建方式,和普通对象的的创建过程一样;

    第二种创建方式,Java内部将此语句转化为以下几个步骤:

    (1)先定义一个名为str的对String类的对象引用变量:Stringstr;

    (2)在栈中查找有没有存放值为“abc”的地址,如果没有,则开辟一个存放字面值为“abc”的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为“abc”的地址,则查找对象o,并返回o的地址。

    (3)将str指向对象o的地址。

    值得注意的是,一般String类中字符串值都是直接存值的。但像Stringstr="abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用。

    为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。

    Java代码

    Stringstr1=“abc”;

    Stringstr2=“abc”;

    System.out.println(s1==s2);//true

    Stringstr1=“abc”;

    Stringstr2=“abc”;

    System.out.println(s1==s2);//true

    注意,这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。

    我们再接着看以下的代码。

    Java代码

    Stringstr1=newString(“abc”);

    Stringstr2=“abc”;

    System.out.println(str1==str2);//false

    Stringstr1=newString(“abc”);

    Stringstr2=“abc”;

    System.out.println(str1==str2);//false

    创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

    以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

    f)数组

    当定义一个数组,intx[];或int[]x;时,在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。x=newint[3];将在堆内存中分配3个保存int型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。

    g)静态变量

    用static的修饰的变量和方法,实际上是指定了这些变量和方法在内存中的“固定位置”-staticstorage,可以理解为所有实例对象共有的内存空间。static变量有点类似于C中的全局变量的概念;静态表示的是内存的共享,就是它的每一个实例都指向同一个内存地址。把static拿来,就是告诉JVM它是静态的,它的引用(含间接引用)都是指向同一个位置,在那个地方,你把它改了,它就不会变成原样,你把它清理了,它就不会回来了。

    那静态变量与方法是在什么时候初始化的呢?对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。

    我们常可看到类似以下的例子来说明这个问题:

    Java代码

    classStudent{

    staticintnumberOfStudents=0;

    Student()

    {

    numberOfStudents++;

    }

    }

    classStudent{

    staticintnumberOfStudents=0;

    Student()

    {

    numberOfStudents++;

    }

    }

    每一次创建一个新的Student实例时,成员numberOfStudents都会不断的递增,并且所有的Student实例都访问同一个numberOfStudents变量,实际上intnumberOfStudents变量在内存中只存储在一个位置上。

    【编辑推荐】

    【责任编辑:程华权 TEL:(010)68476606】

    展开全文
  • Java内存管理机制

    2021-03-05 16:39:12
    JVM内存管理机制说到底就是为了解决两个问题:给对象分配内存以及回收分配给对象的内存。Java Heap被分为两部分:Young Generation 和 Old Gereration。Perm并不属于Heap。Young Generation (Young Gen)所有的new...

    JVM内存管理机制说到底就是为了解决两个问题:给对象分配内存以及回收分配给对象的内存。

    Java Heap被分为两部分:Young Generation 和 Old Gereration。Perm并不属于Heap。

    91becf1da6739ee5ddd036226a850e8b.png

    Young Generation (Young Gen)

    所有的new出来的对象都放在Young Gen,当Young Gen满了, 就会执行Garbage Collection (GC), 此时的GC称为Minor GC。 Young Gen被分成三部分:Eden Memory和两个Survivor Memory。

    多数情况下,对象都在新生代Eden区中分配,但一些大对象可能会直接进入到老年代。虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接进老年代分配,这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存拷贝。

    Eden满了怎么办?JVM就会执行Minor GC。被引用的对象都会存活下来,它们将被移到Survivor区域里,也就是 图中的S0或S1。

    同一时间的两个Survivor区,一个用来保存对象,另一个是空的;每次进行Minor GC垃圾回收时,就把Eden,From的可达对象复制到To区域中,一些生存时间长的就复制到老年代,接着就清除Eden,From空间,最后把原来的To空间变为From空间,原来的From空间变为To空间。( 有点类似于双缓冲队列 )

    虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并进过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivro区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

    动态对象年龄判定:为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

    Young区域大部分对象 朝生夕灭,因此Minor GC回收频率高且回收速度很快。

    新生代采用 复制算法 。

    Old Generation(Tenured Gen)

    回收机制:采用标记压缩算法回收垃圾。

    对象来源:

    大对象直接进入老年代

    Young代中生存时间长的可达对象。

    回收频率:因为很少对象会死掉,所以执行频率不高,而且需要较长时间来完成。

    Permanent Generation(永久代)

    绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟 “PermGen space”的内存溢出:

    import java.io.File;

    import java.net.URL;

    import java.net.URLClassLoader;

    import java.util.ArrayList;

    import java.util.List;

    public class PermGenOomMock{

    public static void main(String[] args) {

    URL url = null;

    List classLoaderList = new ArrayList<>();

    try {

    url = new File("/tmp").toURI().toURL();

    URL[] urls = {url};

    while (true){

    ClassLoader loader = new URLClassLoader(urls);

    classLoaderList.add(loader);

    loader.loadClass("java.lang.Object");

    }

    } catch (Exception e) {

    e.printStackTrace();

    }

    }

    }

    运行结果:

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

    at java.util.concurrent.locks.ReentrantLock.(ReentrantLock.java:262)

    at java.util.concurrent.ConcurrentHashMap$Segment.(ConcurrentHashMap.java:425)

    at java.util.concurrent.ConcurrentHashMap.(ConcurrentHashMap.java:825)

    at java.util.concurrent.ConcurrentHashMap.(ConcurrentHashMap.java:869)

    at java.lang.ClassLoader.(ClassLoader.java:281)

    at java.lang.ClassLoader.(ClassLoader.java:334)

    at java.security.SecureClassLoader.(SecureClassLoader.java:99)

    at java.net.URLClassLoader.(URLClassLoader.java:140)

    at com.boothsun.jvm.PermGenOomMock.main(PermGenOomMock.java:17)

    本例中使用的 JDK 版本是 1.7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 "java.lang.OutOfMemoryError: PermGen space " 异常了。这里之所以采用 JDK 1.7,是因为在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。

    MetaSpace(元空间)

    JDK1.8 永久代的废弃

    JDK1.8 永久代变化如下图:

    d93bbc1ed60b2b689821c75c2a9e297a.png

    新生代:Eden + From Survivor + To Survivor

    老年代:OldGen

    永久代(方法区的实现):PermGen ---> 替换为Metaspace(本地内存中)

    移除永久代的原因

    移除永久代是为融合HotSpot JVM与JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

    永久代大小不确定,PermSize指定的太小很容易造成永久代OOM,因为PermSize的大小很依赖于很多因素,比如JVM加载的class总数,常量池的大小,方法的大小等。

    元空间的内存大小

    元空间时方法区的具体实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常称为“非堆”。

    元空间的本地和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。

    常用配置参数

    MetaspaceSize

    初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。

    MaxMetaspaceSize

    限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。

    MinMetaspaceFreeRatio

    当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。

    MaxMetasaceFreeRatio

    当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。

    MaxMetaspaceExpansion

    Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。

    MinMetaspaceExpansion

    Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。

    展开全文
  • Java内存管理实质上就是JVM的内存管理JVM的内存分为两部分:stack和heapStack(栈)是指JVM的内存指令区。Java基本数据类型,Java指令代码,常量都存在stack中。heap(堆)是JVM的内存数据区。heap专门用来保存对象的...
  • java内存管理

    2021-03-04 02:02:23
    Java虚拟机在执行Java程序的过程中会把它所管理内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而...
  • Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型 规定了哪些东西呢,...
  • java内存管理机制

    2021-03-05 16:38:24
    转载:...堆内存用来存放由new创建的对象和数组以及对象的实例变量在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过...
  • 一、概述Java虚拟机在执行Java程序的过程中会把它所管理内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。Java虚拟机所管理内存将会包括以下几个运行时数据区域,如下图所示:下面就...
  • java中,引用在栈中,申明时,引用不占内存,实例化赋值时,引用才占空间。基本数据类型在栈中,OBJECT数据类型在堆中。C++中,指针变量在栈中,直接声明的变量在栈中,new操作分配的对象在堆中。具体的:先来看java...
  • Java的自己主动内存管理及垃圾回收技术使得Java程序猿不须要释放废弃对象的内存。从而简化了编程的过程。同一时候也避免了因程序猿的疏漏而导致的内存泄露问题。内存管理和垃圾回收是JVM很重要的一个部分。深入理解...
  • 1.Test.javaimport java.util.Scanner;public class Test {public static void main(String[] args) {Test.begin();}public static void begin(){Free f=new Free();Use u=new Use();System.out.println("》》》----...
  • Java内存管理机制

    2021-03-07 01:36:41
    摘取别人的文章原文1.Java内存管理就是对象的分配和释放分配:内存的分配是程序完成的,程序员通过new关键字为对象申请内存空间(基本数据类型除外),对象都是在堆(Heap)中分配空间。释放:对象的释放是由垃圾回收...
  • Java 内存分配策略 新生代和老年代的 GC 操作新生代 GC 操作:Minor GC发生的非常频繁,速度较块。老年代 GC 操作:Full GC / Major GC经常伴随着至少一次的 Minor GC;速度一般比 Minor GC 慢上 10 倍以上。优先在 ...
  • Java内存管理原理

    2021-03-05 21:51:56
    Java内存管理原理Java内存怎么划分?经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存...
  • Java内存管理简述

    2021-03-13 02:16:01
    转自:...Java虚拟机所管理内存将会包括以下几个运行时数据区域,如下图所示:下面就每一个区域进行阐述。二、运行时数据区域程序计数器程序计数器,可以看做是当前线程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 616,428
精华内容 246,571
关键字:

java的内存管理

java 订阅