天天看点

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

JVM运行时数据区

  • 概述
  • 分配JVM内存空间
      • 分配堆的大小
      • 分配方法区的大小
      • 分配线程空间的大小
  • 运行时数据区剖析
    • 方法区
      • 永久代和元空间的区别是什么?
      • 方法区OOM
      • **类加载太多导致的OOM**
      • **字符串常量池OOM**
    • 字符串常量池(重点)
      • 三大常量池
      • ``字符串常量池面试题``
      • ``jdk1.7+ String类之intern()方法``
  • 堆空间(重点)
    • jvisualvm工具查看堆
    • 通过打印GC日志查看堆内存空间
    • 使用jstat命令查看堆空间
    • 使用jmap命令查看堆空间

概述

JVM由三部分组成 1.类加载机制 2.运行时数据区 3.执行引擎

按照职责划分

线程独享区域(程序执行)
	1.不需要垃圾回收
	2.虚拟机栈、本地方法栈、程序计数器
线程共享(存储)
	1.方法区
	2.堆
	3.需要垃圾回收
	4.存储类的静态数据和对象数据
           

分配JVM内存空间

讲这块前需要先区分一下 jdk1.7方法区的实现是永久代 jdk1.8之后方法区的实现是元空间

分配堆的大小

永久代1.7
-Xms 堆的初始容量
-Xmx 堆的最大容量

元空间1.8
-XX:InitialHeapSize=xxx
-XX:MaxHeapSize=xxx

#为了提高性能,一般互联网公司都将初始容量和最大容量设置成一样的
#因为申请堆内存空间其实是非常消耗性能的,所谓的空间换时间
           

分配方法区的大小

永久代1.7
-XX:PermSize 初始容量
-XX:MaxPermSize 最大容量

元空间1.8
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
           

分配线程空间的大小

-Xss:xxx
为jvm启动的每个线程分配的内存大小 jdk1.5+默认是1M
           

运行时数据区剖析

方法区

###方法区存储什么数据

类型信息  Class(com.sxw.User类)
方法信息  Method(方法名称、参数列表、方法返回值)
字段信息  Field(字段类型、字段修饰符)

code区,存储的是方法执行对应的字节码指令
方法表 在A类的main方法中去调用B类的1方法,是根据B类的方法表去查找合适的方法
进行调用的

重点:静态变量
----JDK1.7之后转移到堆中存储

重点:运行时常量池--从class中的常量池加载而来
 jdk1.7之后转移到堆中从存储
1.字面量类型
  1.1双引号引起来的字符串值 "sxw"---会进入字符串常量池(重点)
  1.2final修饰的变量
  1.3非final修饰的变量
2.应用类型->内存地址
  2.1类的符号应用
  
JIT编译器编译之后的代码缓存
           

永久代和元空间的区别是什么?

时间线:
	jdk1.8以前方法区的实现是永久代
	jdk1.8之后方法区的实现是元空间
存储位置不同:
	永久代使用的内存区域是JVM进程所使用的区域,		它的大小受限于整个JVM内存的大小
元空间使用的内存区域是物理内存区域,也就是你本地的内存,很少发生OOM
存储内容不同:
	永久代存储了元数据以及静态变量和运行时常量池常量池---1.6(1.7的时候已经把变量和池移到堆了)
元空间存储了元信息,而静态变量和运行时常量池都挪到了堆中
           

为什么要用永久代替换元空间?

1.字符串存在永久代中容易出现性能问题和永久代溢出
2.永久代GC比堆复杂效率低下
3.类以及方法的信息等确定大小比较难,对永久代的大小指定比较困难。
           

方法区OOM

以下是伪代码

类加载太多导致的OOM

while(true){
    #循环加载一个类
    classloader.loadClass("com.xxx.User")
}

#结论
jdk1.7:java.lang.outOfMemoryError:PermGen space-永久代溢出OOM
jdk1.8:java.lang.outofMemoryError:Metaspace-元空间溢出OOM
           

字符串常量池OOM

while(true){
    String base = "xxx";
    String str = base +base;
    list.add(str);
}

#结论:
jdk1.6:java.lang.outOfMemoryError:PermGen space-永久代溢出OOM
jdk1.7:java.lang.OutOfMemoeryError:Java Heap space-堆空间溢出OOM
jdk1.8: java.lang.outOfMemoryError:PermGen space-永久代溢出OOM
           

综上述的测试 也可以证明jdk各个版本中方法区的区别 这是面试的重点!

字符串常量池(重点)

三大常量池

  1. class常量池(静态常量池)
(一个class-文件只有一个class常量池)
  1. 运行时常量池
(包含了个个Class文件的运行时常量池+一个全局的字符串常量池)
  1. 字符串常量池
(全局的)

背景

为了提高匹配速度以便查找某个字符串是否存在于常量池中。目的:重复字符串不要重复创建
存储结构为StringTable 
类似于java中的hashTable/ 数组+单链表 就是个hashMap 

stringtable:
k:字符串内容
v:该字符串内容的引用(有可能指向字符串常量池内也有可能指向堆空间内字符串常量池之外)
           
图1代表了一个stringtable的数据结构
String str = "James";
在stringtable中:
k:"James"
v:字符串的引用0x01
           
JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

字符串常量池面试题

String str1 = "abc";
		String str2 = new String("abc");
		System.out.println(str1 == str2);
		 /**解析:
		 str1是存在字符串常量池的(StringTable中) 返回该应用 比如0x02
str2是new的 凡是new出来的都在字符串常量池之外堆空间之内 因此是false
注意:如果单纯这一行代码 String str2 = new String("abc") 字符串常量池会有一个abc
堆空间也有一个abc 两个!**/

		String str3 = new String("abc");
		System.out.println(str3 == str2);
		/**
		 解析:new的都是新的一块空间 因此==比较的是内存引用地址 因此是false
		**/

		String str4 = "a" + "b";
		System.out.println(str4 == "ab");
		/**
		解析:"a"+"b"会进行编译优化,在字符串常量池时就已经							是"ab" 此时假如返回引用地址0x02 那么"ab"的引用实际上就是str4 因此是true
		**/
	
		final String s = "a";
		String str5 = s + "b";
		System.out.println(str5 == "ab");
		/**
		 解析:s加了final那么就变成了一个常量 那么s+"b"就会在编译期间就变成了"ab"
 因此都是在字符串常量池的 并且引用也一样 同上述一样 因此是true  
		**/

		String s1 = "a";
		String s2 = "b";
		String str6 = s1 + s2;
		System.out.println(str6 == "ab");
		/**
		解析:由于s1和s2都是变量 编译不会进行优化 s1+s2实际上是在运行期间发生拼接的 底层使用的是Stringbuffer进行拼接的 那么是new的操作 因此s1+s2的引用Str6在字符串"ab"
常量池之外堆内存之内 "ab"是在字符串常量池的 两个位置引用都不一样 因此是false
		**/

		String str7 = "abc".substring(0, 2);
		System.out.println(str7 == "ab");
		String str8 = "abc".toUpperCase();
		System.out.println(str8 == "ABC");
		/**
		解析:subString/toUpperCase实际上也是new Sting  那么位置不一样因此不相等
		**/

		String s5 = "a";
		String s6 = "abc";
		String s7 = s5 + "bc";
		System.out.println(s6 == s7.intern());
		/**
		s7.intern()???什么鬼?? 没见过
		**/
           

jdk1.7+ String类之intern()方法

作用: 1.返回该值在stringtale中的引用值 2.如果该值不存在stringtable中,则动态将该值添加到stringtable中,并吧该引用指向自己(此时在stringtable中有这个值但是这个值的引用指向了堆内存内字符串常量池之外)----请细品这句话

上面试题 一下就明白了!

String c = "xxx"
    c.intern() == c? true
/**解析:c.intern()会拿着"xxx"这个值去stringtable中找对应的引用 如果存在就返回
此时是存在的 并且c就是这个值得引用 因此是true**/

	Strig d= new String("L")
    d.intern()==d?false
/**解析:"L"会在堆和字符串常量池中都存在一份 假如常量池的引用是0x89
那么d.intern()返回的是0x89 但是d实际上是指向new 的那块 也就是堆空间的
两者位置不一致因此是false**/

	//难点
 	String e = new String("xo") + new String("po");
    e.intern() == e? true
    /**解析:
首先执行完之后 String e = new String("xo") + new String("po")会有五个地方发生变化  "xo"和"po"分别在字符串常量池和堆中各有一份 堆中
还有一份new String("xopo")
当执行e.intern()时拿着它的值 也就是完整的"xopo" 去stringtable中去找引用
此时要注意 字符串常量池是没有一份叫做"xopo"的字符串的 那么此时他会将这个值放入到字符串常量池中并将引用指向e 那么e.intern之后拿到的引用其实就是e 因此true
注意:如果是"ja" +"va"这种默认的 那么结果是fasle 因为它们一开始就存在字符串 常量池了 除了"java"还有一些 感兴趣的可以查看一下文档
	String e = new String("ja") + new String("va");
    e.intern() == e? false
**/
           

intern()方法总结:

intern()的好处:当程序中需要大量使用String 并且值会有重复的情况下 不要频繁 创建相同值的String 而是考虑使用intern()方法!

使用jmap命令可以查看堆内存中存 活的对象的实例有多少个

intern优化案例

简单解读一下这个程序 arr[]数组可以放一千万的元素 此时我们生成10个不重复的数字 达到这个数组中存放 1000w个0-9的数的效果

static final int MAX = 1000 * 10000;
	static final String[] arr = new String[MAX];

	public static void main(String[] args) throws Exception {
		Integer[] DB_DATA = new Integer[10];
        for (int i = 0; i < 10; i++) {
            DB_DATA[i] = i;
            System.out.println(DB_DATA[i]);
        }
		for (int i = 0; i < MAX; i++) {
			//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
			arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
		}
		System.out.println("等待中。。。。。");
		System.in.read();
	}
           

当我们不使用intern方法时 也就是去找该值是否已经被加载过了 当10个不重复的值都第一次被加载后 后续放入的都是该值的引用 而不是再去new String()

jsp -l #查看所有的java进程并显示进程号

jamp -histo:live pid #查看堆内存中还存活的对象并统计

此时结果 String类型的对象有1000w+个 多余的4000是JVM自己需要的字符串 此时容易引发堆内存OOM

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

当我们使用intern方法时 也就是每次都用new Stirng来放入数组中

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

结论

使用了intern之后程序的性能将会大幅度提升,这就是学习JVM的魅力所在

堆空间(重点)

ps:图中新生代占整个堆空间的三分之一 老年代占堆空间的三分之二
新生代
	1.eden区
	2.s0
	3.s1
老年代
	一整块
           
JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

jvisualvm工具查看堆

jvisualvm是jdk自带的工具 cmd后输入jvisualvm可接打开 需要在工具中安装Visual Gc插件 通过该图也可以验证上述新生代和老年代的比例

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

通过打印GC日志查看堆内存空间

**-XX:+PrintGCDetails 命令**

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)
JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)
JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

为了方便查看 将堆内存设置成60M(-Xms60m -Xmx60m -XX:+PrintGCDetails) 按照比例 新生代应占20M 老年代应占40M 如上图

使用jstat命令查看堆空间

jps -l 可以查看运行中所有的java程序的进程号

jstat -gc pid

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)

使用jmap命令查看堆空间

jmap -heap pid

JVM运行时数据区概述分配JVM内存空间运行时数据区剖析堆空间(重点)