1、程序,无论是代码还是数据,都需要存储在内存中,JVM为Java程序提供并管理所需要的内存空间。
2、JVM内存分为堆(heap)、栈(stock)、方法区(method)三个区域,分别用于储存不同的数据。
3、HotSpot是Sun JDK和Open JDK中所带的虚拟机(Sun JDK和Open JDK除了注释,代码实现基本上是相同的)。
下面我们来分别了解一下他们都分别存储了哪些数据:
1、 JVM只有一个堆区,在虚拟机启动时创建,被所有线程共享,堆区不放基本类型(成员变量除外)和对象的引用,只存储对象本身(包括class对象和异常对象)和数组,堆是GC所管理的主要区域(对不需要的对象进行标记,而后进行清除)。
2、Java中堆内存划分: (下面是JDK1.8之前的空间组成)
a、在整个JVM的堆内存中实际上将内存分为了三部分:
注: JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
b、在JDK1.8之后,将最初的永久代内存空间取消了,该图为JDK1.8之前的空间组成。
c、取消永久代的目的是将 HotSpot 于 JRockit 的两个虚拟机标注联合为一个。
3、GC(Garbage Controller)流程:
GC类型:(根据不同区域分类)
复制算法:
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但是,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
标记-清除算法:标记-清除算法
老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
一般的,首先是进行年轻代的GC,然后是老年代的GC,如果要压缩,每个代需要分别压缩。
如果年老区本身就已经很满了,满到无法放下从survivor熬出来的对象,那么young GC就不会再次触发,而是会使用full GC堆整个堆进行GC(除了CMS这种GC,因为CMS不能对年轻代进行GC)。
CMS GC是一款并发、使用标记-清除算法的gc,主要针对老年代进行回收的。
垃圾回收器类型:(根据运行机制分类)
实际上,JVM 有三种类型的垃圾回收器 (GC),程序员可以选择使用其中一种。一般情况下,JVM 会根据底层硬件来选择垃圾回收器类型。
3.1 G1垃圾回收器——一种暂停时间在可接受范围内的高吞吐量 GC,使用 -XX:+UseG1GC 开启。
3.2 并发标记扫描垃圾回收器 —— 最小化应用线程暂停时间的 GC,可以通过 -XX:+UseConcMarkSweepGC 指定。在 JDK 9 中,这种 GC 已被弃用。
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
日后如果发现你的程序执行速度变慢了,可以针对程序的运行内存进行分析:
1、每个线程包含一个栈区(堆只有一个所有线程共享),栈中只保存基本数据类型的对象和自定义对象的引用,对象都存放在堆区中。
2、每个栈中的数据(原始类型 和 对象引用)都是私有的,其他栈不能访问。
3、栈分为3部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
4、过程:栈用于存储程序运行时在方法中声明的所有局部变量(栈主要存储方法中的局部变量)。JVM会为每一个方法分配一个对应的空间,这个空间称为该方法的栈帧。一个栈帧对应一个正在调用的方法,栈帧中存储了该方法的参数、局部变量等数据。当某一方法调用完成后,其对应的栈帧将被清除,局部变量失效。(方法结束,局部变量失效,从栈中清除)
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程序运行时,首先会通过类装载器载入文件的字节码信息,经过解析后将其装入方法区。类的各种信息(包括方法)都在方法区储存。(将类的成员都加载到方法区)类在实例化对象时,多个对象会拥有各自在堆中的空间,但所有实例对象是共用在方法区中的一份方法定义的。方法只有一份。
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引用这个实例。下面就让我们跟踪一下虚拟机,看看它究竟是怎么来执行这个任务的:
//栈中的数据可以共享:
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比较的是对象的值。