按照线程使用情况和职责分成两大类:
1)线程独享(程序执行区)
虚拟机栈,本地方法栈,程序计数器
特点:不需要垃圾回收
2)线程共享(数据存储区域)
堆和方法区
存储类的静态数据和对象数据
特点:需要垃圾回收
是虚拟机所管理的内存中最大的一块,在虚拟器启动时创建,被所有线程共享。此区域的唯一目的就是存放对象实例,一般来说对象实例都是在这里分配内存,包括现对象、数组与运行时常量。
堆内存划分依据:
1.弱分代假说:大多数对象存活时间短。
2.强分代假说:熬过越多次的垃圾回收,就越难以被回收。
3.跨代引用假说:跨代引用的对象占少数。
根据这个学说堆被划分为两个区域:新生代和老年代
新生代与老年代
老年代比新生代生命周期长。
新生代与老年代空间默认比例 1:2:JVM 调参数,XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3。
HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例是:8:1:1。
几乎所有的 Java 对象都是在 Eden 区被 new 出来的,Eden 放不了的大对象,就直接进入老年代了。
堆的特点
对象分配过程
public class DemoError {
private static int cnt = 0;
private void call() {
cnt ++;
try {
call();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(cnt);
}
}
public static void main(String[] args) {
DemoError demoError = new DemoError();
demoError.call();
}
}
#空间进一步缩小,递归的深度会进一步缩小
java -Xss256k DemoError
本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(比如C++方法)服务。这个方法栈主要就是用于java调用非java的native方法。
方法区(Method Area)是可供各个线程共享的运行时内存区域,方法区本质上是Java语言编译后代码存储区域,它存储每一个类的结构信息,例如:运行时常量池、成员变量、方法数据、构造方法和普通方法的字节码指令等内容。很多语言都有类似区域。
方法区的具体实现有两种:永久代(PermGen)、元空间(Metaspace)
方法区存储下面这几种数据类型:
第一:已经被加载的类信息
第二:运行时常量池(字符串常量池):从class中的常量池加载而来,JDK1.7之后,转移到堆中存
储。字面量(就是定义的时候直接给了值,而不是一个等待赋值的变量)类型,引用类型–>内存地址
第三:JIT编译器编译之后的代码缓存
永久代和元空间的区别是什么?
1)JDK1.8之前使用的方法区实现是永久代,JDK1.8及以后使用的方法区实现是元空间。
2)存储位置不同:
永久代所使用的内存区域是JVM进程所使用的区域,它的大小受整个JVM的大小所限制。
元空间所使用的内存区域是物理内存区域。那么元空间的使用大小只会受物理内存大小的限
制。
3)存储内容不同:
永久代存储的信息基本上就是上面方法区存储内容中的数据。
元空间只存储类的元信息,而静态变量和运行时常量池都挪到堆中。
3)为什么要使用元空间来替换永久代?
字符串存在永久代中,容易出现性能问题和永久代内存溢出。
有三种常量池:
class常量池:一个class文件只有一个class常量池
字面量:数值型(int、float、long、double)、双引号引起来的字符串值等
符号引用:Class、Method、Field等
运行时常量池:一个class对象有一个运行时常量池
字面量:数值型(int、float、long、double)、双引号引起来的字符串值等
符号引用:Class、Method、Field等
字符串常量池:全局只有一个字符串常量池,双引号引起来的字符串值
我们定义字符串的时候如果直接用""进行引用则是存储在字符串常量池中,如果用new String()则是存储在堆中。字符串常量池底层是一个拉链哈希表,用来存储和索引,所以字符串常量池不会存储重复的字符串。
看下面这几个例子来理解:
例子1
String a = "abc";
String b = "abc";
// true 我们知道 == 比较的是存储地址,所以我们可以看到这两个字符串都是存储在字符串常量池中的,并且引用的是同一个地址,那么肯定是只存储了一份。
System.out.println(a == b);
例子2
String a = "abc";
String b = new String("abc");
// false b 这个对象是通过new出来的,他不会放到字符串常量池中
System.out.println(a == b);
例子3
String a = "abc";
String c = "def";
String d = a + c;
String f = "abcdef";
// false 所以只有"" 的字符串在在字符串常量池,涉及到任何其它操作都不会存储在字符串常量池中
System.out.println(f == d);
// intern将字符串移动到字符串常量池中
// 因为我们调用函数intern d 的引用移动了字符串常量池,他们的地址也就一样了
System.out.println(f == d.intern());
例子4 map的底层实现和字符串常量池基本一样
Map<String, Integer> map = new HashMap<>();
map.put("通话", 51);
map.put("重地", 55);
// 两个字符串的hashcode一模一样
System.out.println("通话".hashCode());
System.out.println("重地".hashCode());
// hashcode 相同的时候会进行拉链,也就是先比较hashcode如果相同在比较值,最后放到这个槽的链表上
System.out.println(map.size());
程序计数器(Program Counter Register),也叫PC寄存器,是一块较小的内存空间,它可以看作是当
前线程所执行的字节码指令的行号指示器。字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完
成。
为什么需要程序计数器?
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线
程切换(系统上下文切换)后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
存储的什么数据?
如果一个线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器的值则为空。
异常:此内存区域是唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区
域。
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,
它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象
作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制。
直接内存(堆外内存)与堆内存比较:
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
下面这个例子可以比较它们读写之前的差异:
public class ByteBufferCompare {
public static void main(String[] args) {
//allocateCompare(); //分配比较
operateCompare(); //读写比较
}
/**
* 直接内存 和 堆内存的 分配空间比较
* 结论: 在数据量提升时,直接内存相比非直接内的申请,有很严重的性能问题
*/
public static void allocateCompare() {
int time = 1000 * 10000; //操作次数,1千万
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocate(int capacity) 分配一个新的字节缓冲区。
//非直接内存分配申请
ByteBuffer buffer = ByteBuffer.allocate(2);
}
long et = System.currentTimeMillis();
System.out.println("在进行" + time + "次分配操作时,堆内存 分配耗时:" +
(et - st) + "ms");
long st_heap = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocateDirect(int capacity) 分配新的直接字节缓冲区。
//直接内存分配申请
ByteBuffer buffer = ByteBuffer.allocateDirect(2);
}
long et_direct = System.currentTimeMillis();
System.out.println("在进行" + time + "次分配操作时,直接内存 分配耗时:" +
(et_direct - st_heap) + "ms");
}
/**
* 直接内存 和 堆内存的 读写性能比较
* 结论:直接内存在直接的IO 操作上,在频繁的读写时 会有显著的性能提升
*/
public static void operateCompare() {
int time = 10 * 10000 * 10000; //操作次数,10亿
ByteBuffer buffer = ByteBuffer.allocate(2 * time);
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用来写入 char 值的相对 put 方法
buffer.putChar('a');
}
buffer.flip();
for (int i = 0; i < time; i++) {
buffer.getChar();
}
long et = System.currentTimeMillis();
System.out.println("在进行" + time + "次读写操作时,非直接内存读写耗时:" +
(et - st) + "ms");
ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
long st_direct = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用来写入 char 值的相对 put 方法
buffer_d.putChar('a');
}
buffer_d.flip();
for (int i = 0; i < time; i++) {
buffer_d.getChar();
}
long et_direct = System.currentTimeMillis();
System.out.println("在进行" + time + "次读写操作时,直接内存读写耗时:" +
(et_direct - st_direct) + "ms");
}
}
从数据流的角度,来看:
非直接内存作用链:本地IO –>直接内存–>非直接内存–>直接内存–>本地IO
直接内存作用链:本地IO–>直接内存–>本地IO
直接内存的使用场景:
有很大的数据需要存储,它的生命周期很长
适合频繁的IO操作,例如:网络并发场景
JDK 1.6:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区[永久代](字符串常量池、静态变量、运行时常量池、类常量池)
JDK 1.7:程序计数器、Java虚拟机栈、本地方法栈、堆(字符串常量池、静态变量)、方法区[永久代](运行时常量池、类常量池)
JDK 1.8:程序计数器、Java虚拟机栈、本地方法栈、堆(字符串常量)、元数据(静态变量、运行时常量池、类常量池)
JDK1.8中变化最大是,的Perm永久区用Metaspace进行了替换
下面来看几个例子搞明白这个变迁:
import java.util.ArrayList;
import java.util.List;
public class StringDemo {
public static void main(String[] args) {
String base = "";
List<String> strings = new ArrayList<String>();
for (int i = 0; i < Integer.MAX_VALUE; i ++) {
String str = base += "abcdefg";
strings.add(str.intern());
}
}
}
java -XX:PermSize=8m -XX:MaxPermSize=8m StringDemo
在1.6的运行结果:
1.7运行结果:
永久带还保留但是字符串常量已经存到了堆中去,因为报的是堆内存溢出,同时我们设置PermSize的时候没有提示参数不存在。
1.8的运行结果:
可以看到1.8永久带彻底被移除,字符串常量存储到了堆内存中。