一、Java中的内存管理:
1、程序,无论是代码还是数据,都需要存储在内存中,JVM为Java程序提供并管理所需要的内存空间。
2、JVM内存分为堆(heap)、栈(stock)、方法区(method)三个区域,分别用于储存不同的数据。
3、HotSpot是Sun JDK和Open JDK中所带的虚拟机(Sun JDK和Open JDK除了注释,代码实现基本上是相同的)。
下面我们来分别了解一下他们都分别存储了哪些数据:(图片来源于网络)
二、堆(heap): 堆部分内容参考文章
1、 JVM只有一个堆区,在虚拟机启动时创建,被所有线程共享,堆区不放基本类型(成员变量除外)和对象的引用,只存储对象本身(包括class对象和异常对象)和数组,堆是GC所管理的主要区域(对不需要的对象进行标记,而后进行清除)。
2、Java中堆内存划分: (下面是JDK1.8之前的空间组成)
a、在整个JVM的堆内存中实际上将内存分为了三部分:
b、在JDK1.8之后,将最初的永久代内存空间取消了,该图为JDK1.8之前的空间组成。
c、取消永久代的目的是将 HotSpot 于 JRockit 的两个虚拟机标注联合为一个。
3、GC(Garbage Controller)流程:
GC类型:(根据不同区域分类)
垃圾回收器类型:(根据运行机制分类)
实际上,JVM 有三种类型的垃圾回收器 (GC),程序员可以选择使用其中一种。一般情况下,JVM 会根据底层硬件来选择垃圾回收器类型。
GC具体流程:
4、堆内存参数调整:(调优关键)
堆内存空间调整参数:
可通过Runtime类获取内存的整体信息:
代码如下:
package cn.liang.jvm;
public class memoryTest {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long totalMemory = runtime.totalMemory();
System.out.println("max_memory=" + maxMemory /(double)1024/1024 + "M");
System.out.println("total_memory=" + totalMemory /(double)1024/1024 + "M");
}
}
输出结果:
max_memory=3641.0M
total_memory=245.5M
说明整个内存空间的可变范围(伸缩区):245.5M ~ 3641.0M之间,有可能造成整个程序的性能。
为了避免伸缩区的可调策略,使初始化内存等于最大内存,从而提升整个程序性能:
输出结果:
max_memory=981.5M
total_memory=981.5M
Heap
PSYoungGen total 305664K, used 15729K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 262144K, 6% used [0x00000007aab00000,0x00000007aba5c420,0x00000007bab00000)
from space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000)
to space 43520K, 0% used [0x00000007bab00000,0x00000007bab00000,0x00000007bd580000)
ParOldGen total 699392K, used 0K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000)
object space 699392K, 0% used [0x0000000780000000,0x0000000780000000,0x00000007aab00000)
Metaspace used 2708K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 293K, capacity 386K, committed 512K, reserved 1048576K
观察GC的触发操作
代码如下:
package cn.liang.jvm;
import java.util.Random;
public class gctest {
public static void main(String[] args) {
Random random = new Random();
String str = "hello liang";
while (true) {
str +=str + random.nextInt(99999999);
str.intern();
}
}
}
输出结果:
[GC (Allocation Failure) [PSYoungGen: 1769K->511K(2560K)] 1769K->775K(9728K), 0.0015982 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2374K->240K(2560K)] 2638K->1119K(9728K), 0.0011725 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2100K->256K(2560K)] 7841K->5996K(9728K), 0.0005402 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 256K->240K(2560K)] 5996K->5980K(9728K), 0.0005811 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 240K->0K(2560K)] [ParOldGen: 5740K->3925K(7168K)] 5980K->3925K(9728K), [Metaspace: 2662K->2662K(1056768K)], 0.0064126 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 41K->32K(2560K)] 6397K->6388K(9728K), 0.0003653 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 32K->32K(1536K)] 6388K->6388K(8704K), 0.0003294 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(1536K)] [ParOldGen: 6356K->2710K(7168K)] 6388K->2710K(8704K), [Metaspace: 2662K->2662K(1056768K)], 0.0035285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 19K->0K(2048K)] 5160K->5140K(9216K), 0.0004489 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 5140K->5140K(9216K), 0.0003114 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 5140K->5140K(7168K)] 5140K->5140K(9216K), [Metaspace: 2662K->2662K(1056768K)], 0.0030502 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 5140K->5140K(9216K), 0.0003198 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 5140K->5127K(7168K)] 5140K->5127K(9216K), [Metaspace: 2662K->2662K(1056768K)], 0.0039555 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.liang.jvm.gctest.main(gctest.java:11)
Heap
PSYoungGen total 2048K, used 40K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 3% used [0x00000007bfd00000,0x00000007bfd0a120,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
ParOldGen total 7168K, used 5127K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
object space 7168K, 71% used [0x00000007bf600000,0x00000007bfb01c78,0x00000007bfd00000)
Metaspace used 2693K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 292K, capacity 386K, committed 512K, reserved 1048576K
日后如果发现你的程序执行速度变慢了,可以针对程序的运行内存进行分析:
三、栈(stock): 栈部分内容参考文章
1、每个线程包含一个栈区(堆只有一个所有线程共享),栈中只保存基本数据类型的对象和自定义对象的引用,对象都存放在堆区中。
2、每个栈中的数据(原始类型 和 对象引用)都是私有的,其他栈不能访问。
3、栈分为3部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
4、过程:栈用于存储程序运行时在方法中声明的所有局部变量(栈主要存储方法中的局部变量)。JVM会为每一个方法分配一个对应的空间,这个空间称为该方法的栈帧。一个栈帧对应一个正在调用的方法,栈帧中存储了该方法的参数、局部变量等数据。当某一方法调用完成后,其对应的栈帧将被清除,局部变量失效。(方法结束,局部变量失效,从栈中清除)
四、方法区(method):
1、方法区又叫静态区,里存储着class文件的信息和动态常量池,class文件的信息包括类信息和静态常量池。
2、用于储存已被虚拟机加载的类信息、常量、静态常量、即使编译器编译后的代码等数据。
3、垃圾收集行为在方法区很少出现,这块区域回收的主要目标是针对常量池的回收和对类型的卸载。
4、运行常量池是方法区的一部分,常量池用于存放编译期生成的各种字面量和符号引用(还有翻译出来的直接引用),这部分内容在类加载后进入方法区的运行时常量池中存放。
5、运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,运行期也可能将新的常量放入池中。
6、字面量:如文本字符串,声明为final的常量值等。
public stick final int i =3;
String s="abc";
7、符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。在对java文件进行编译的过程中,并不会向C语言那样有连接这一步,也就是说class文件中不会存储方法、字段的最终内存布局信息,所以符号引用是不能被虚拟机直接使用的,虚拟机会在加载类时动态的去获取常量池中的符号引用,然后解析到对应的内存地址中,才可以使用
8、方法区用于存放类的信息,Java程序运行时,首先会通过类装载器载入文件的字节码信息,经过解析后将其装入方法区。类的各种信息(包括方法)都在方法区储存。(将类的成员都加载到方法区)类在实例化对象时,多个对象会拥有各自在堆中的空间,但所有实例对象是共用在方法区中的一份方法定义的。方法只有一份。
五、Java 堆栈方法区总结:
1、基础类型直接在栈空间分配,方法的形式参数直接在栈中分配,当方法调用完成后从栈空间回收。
2、引用数据类型,需要new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量。
3、方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,在方法调用完成后从栈空间回收。
4、局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间会被立即回收,堆空间区域等待GC回收。
5、方法调用时传入literal(常量)参数,在方法调用完成后从栈空间分配。
6、字符串常量在 DATA 区域分配 ,this 在堆空间分配 。
7、数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小。
8、static在DATA区域分配。
从Java的这种分配机制来看,堆栈又可以这样理解:堆栈是操作系统在建立某个进程或者线程(在支持多线程的操作系统中是线程)上为这个线程建立的储存区域,该区域具有先进后出的特性。
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中创建的所有类实例或数组都放在堆中,并由应用所有的线程共享,跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的储存空间都是在堆中分配的,但是这个对象的引用却是在栈中分配的,也就是说建立一个对象时从两个地方都分配了内存,在堆中分配的内存实际上建立了这个对象,而在栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
六、例:创建一个对象过程运行时数据区的变化: 例子原链接
AppMain.java
public class AppMain { //运行时, jvm 把appmain的信息都放入方法区
//main 方法本身放入方法区。
public static void main(String[] args) {
//test1是引用,所以放到栈区里, Sample是自定义象应该放到堆里面
Sample test1 = new Sample( " 测试1 " );
Sample test2 = new Sample( " 测试2 " );
test1.printName();
test2.printName();
}
//Sample.java
public class Sample { //运行时, jvm 把appmain的信息都放入方法区
private name; //new Sample实例后, name 引用放入栈区里, name 对象放入堆里
public Sample(String name) { //构造方法
this .name = name;
}
public void printName() { //print方法本身放入 方法区里。
System.out.println(name);
}
}
}
下面是行动向导图:
系统收到我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取整个文件中的二进制数据,然后把AppMain.class类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令,这个main()方法的第一条语句是:Sample test1 = new Sample(“测试1”);
语句很简单,就是让Java虚拟机创建一个Sample实例,并且,使引用变量test1引用这个实例。下面就让我们跟踪一下虚拟机,看看它究竟是怎么来执行这个任务的:
1、Java虚拟机一看,不就是想创建一个Sample实例嘛,简单,于是直奔方法区而去,先找到Sample类的类型信息再说,结果呢,没找到,这会方法区里还没有Sample类呢,于是虚拟机立马加载了Sample实例,分配内存,把Sample类的类型信息存放在方法区里。
2、好啦、资料找到啦,下面开始干活,Java虚拟机先是在堆中为一个新的Sample实例分配内存,这个Sample实例持有着指向方法区的Sample类的类型信息的引用,这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址,其实,就有点类似C语言里的指针了,而这个地址呢,就存放在Sample实例的数据区里。
3、在Java虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据,OK,原理讲完了,就让我们来继续我们的跟踪行动,位于"=“前的test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,就会被添加到了执行main()方法的主线程的Java方法调用栈中。而”="将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
4、OK,到这里为止呢,Java虚拟机就完成了简单语句的执行任务,参考我们的行动图,我们终于初步摸清Java虚拟机的一点底细了!
接下来,Java虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后一次执行他们的printName()方法,当Java虚拟机执行test1.printName()时,Java虚拟机会根据局部变量持有的引用,定位到堆中的Sample实例,再根据Sample实例持有的引用,定位到方法区中Sanple类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令。
七、例:创建一个基本类型的值的过程: 例子原链接
//栈中的数据可以共享:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会再栈中创建一个变量a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存进来,然后将a指向3,接着处理int b = 3;在创建完b的引用后,因为栈中已经有3这个值,便将3直接指向3,这样就出现了a于b同时指向3的情况。这时,如果再令a = 4;那么编译器会重新搜索栈中是否有4,如果没有,则将4存起来,将a指向4,如果已经有了,则直接将a指向这个地址,因此a值得改变不会影响b值。要注意这种数据得共享与两个对象得引用指向一个对象得这种共享是不同的,因为这种情况a的修改并不会影响到b,它是由编译器完成的,它有利于节省空间,而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。
==比较的是对象的地址,也就是是否是同一个对象;
equal比较的是对象的值。
上面就是关于内存管理的部分内容,希望能对你有帮助!如果喜欢请点了赞吧!也可以点个关注支持一下,谢谢