- 深入理解JVM的内存区域
- 1. 深入理解运行时数据区
- 2. 从底层深入理解运行时数据区
- 堆空间分带划分
- GC概念
- JHSDB工具
- JDK1.8的开启方式
- 3. 深入辨析堆和栈
- 4. 虚拟机内存优化技术
- 栈的优化技术--栈帧之间数据的共享
- 5. 内存溢出
- 栈溢出
- 堆溢出
- 方法区溢出
- 本机直接内存溢出
- 6. 常量池
- 6.1 Class常量池(静态常量池)
- 6.2 运行时常量池
- 6.3 字符串常量池
- 7 String
- 7.1 String类分析(JDK1.8)
- 7.2 String 对象的不可变性
- 7.3 String的创建方式及内存分配的方式
代码实例:
package com.wanzi.test; public class JVMObject { public final static String MAN_TYPE = "man"; // 常量 public static String WOMAN_TYPE = "woman"; // 静态变量 public static void main(String[] args)throws Exception { Student student1 = new Student(); student1.setName("文轩"); student1.setSexType(MAN_TYPE); student1.setAge(36); for(int i =0 ;i<15 ;i++){ System.gc();//主动触发GC 垃圾回收 15次--- T1存活 } Student student2 = new Student(); student2.setName("李"); student2.setSexType(MAN_TYPE); student2.setAge(18); Thread.sleep(Integer.MAX_VALUE);//线程休眠 } } class Student{ String name; String sexType; int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSexType() { return sexType; } public void setSexType(String sexType) { this.sexType = sexType; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
- JVM向 *** 作系统申请内存
JVM第一步就是通过配置参数或者默认配置参数向 *** 作系统申请内存空间,根据内存大小找到具体得内存分配表,然后把内存段的起始地址和终止地址分配给jvm,接下来JVM 就进行内部分配 - JVM 获取内存空间后,会根据配置参数分配堆,栈以及方法区的内存大小.
-Xms30m -Xmx30m -XX:MaxmetaspaceSize=30m - 类加载(类加载的细节后面会说):
这里主要是吧class放入方法区,还有class中的静态变量和常量也要放入方法区 - 执行方法及创建对象:
启动main线程,执行main方法,开始执行第一行代码.此时堆内存会创建一个student对象,对象引用student1就存放在栈中.
后续代码遇到new关键字,会再创建一个student对象,对像引用student2就存放在栈中
总结一下JVM运行内存的整体流程
JVM在 *** 作系统上启动时,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行犯法.
方法的执行和退出过程在内存上的体现就是虚拟机栈中栈帧的入栈和出栈
同时在方法的执行过程中创建的对象一般情况下都是放在堆中(逃逸分析是在栈上分配),最后堆中的对象也是需要进行垃圾回收清理的
堆被划分为新生代和老年代(Tenured),新生代又被进一步划分为Eden和Survivor区,最后 Survivor 由 From Survivor 和 To Survivor组成.(先需要有概念,后续对象分配和垃圾回收会细讲这块)
GC- Garbage Collection 垃圾回收,在JVM中是自动化的垃圾回收机制,我们一般不用去关注,在jvm中GC的重要区域是堆空间.
我们也可以通过一些额外的方式主动发起它,比如System.gc()主动发起(只是通知jvm进行垃圾回收,什么时候回收无法控制,项目中切记不要使用)
JHSDB 是一款基于服务性代理实现的进程外调试工具。服务性代理是 HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的,主要基于 Java 语言实现的 API 集合。
JDK1.8的开启方式开始HSDB工具(偷个懒 略过一下)
3. 深入辨析堆和栈功能
- 栈: 以栈帧的方式存储方法调用的过程,并存储方法调用过程中的基本数据类型的变量以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放
- 堆: 而堆内存用来存储java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
线程独享还是共享 - 栈: 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存.
- 堆: 堆内存中的对象对所有线程可见.堆内存中的对象可以被所有线程访问.
空间大小
栈的内存要远远小于堆内存
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的JVM在实现中会进行一些优化,使得两个栈帧出现一部分重叠.(主要体现在方法中有参数传递的情况下),让下面栈帧的 *** 作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用是就可以直接公用一部分数据,无需进行额外的参数复制传递了.
使用JHSDB工具查看栈空间一样可以看到
代码示例:
package com.wanzi.test; public class JVMStack { public int work(int x) throws Exception{ int z =(x+5)*10;//局部变量表有, 32位 Thread.sleep(Integer.MAX_VALUE); return z; } public static void main(String[] args)throws Exception { JVMStack jvmStack = new JVMStack(); jvmStack.work(10);//10 放入main栈帧 10 -> *** 作数栈 } }5. 内存溢出 栈溢出
HotSpot版本中栈的大小是固定的,是不支持拓展的
java.lang.StackOverflowError一般的方法调用是很难出现的,如果出现了可能会是无限递归.例如:
public static void main(String[] args) { a(); } public static void a(){ a(); }
虚拟机栈带给我们的启示: 方法的执行因为要打包成栈帧,所以天生要比实现同样功能的循环慢,所以树的遍历算法中: 递归和非递归(循环来实现)都有存在的意义.递归代码简介,非递归代码复杂但是速度快.
OutOfMemoryError:不断建立线程,JVM申请栈内存,机器没有足够的内存.(一般演示不出,演示出来机器也死了)
同时要注意,栈区的空间JVM没有办法去限制的,因为JVM在运行的过程中会有线程不断的运行,没办法限制,所以只能限制单个虚拟机栈的大小.
栈的参数配置具体查看官网:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABHDABI
内存溢出: 申请内存空间,超过最大内存空间.
如果是内存溢出,则通过调大-Xms,-Xmx参数.
如果不是内存泄露,就是说内存中的对象都是必须存活的.那么就应该检查JVM的堆参数配置,与机器内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长,持有状态时间过长,存储结构设计不合理等情况,尽量减少程序运行时的内存消耗.
(1) 运行时常量池溢出
(2) 方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们的配置
注意Class要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制);
1.该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例.
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法.
cglib是一个强大的,高性能的,高质量的Code生产类库,它可以在运行期扩展Java类与实现Java接口.
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类. 除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码.当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.
直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常; 由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可 以考虑重点排查下直接内存方面的原因
6. 常量池 6.1 Class常量池(静态常量池)在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用
字面量: 给基本类型变量赋值的方式就叫做字面量或者字面值.
比如: String a = “b”,这里"b"就是字符串字面量,同样类推还有整数字面值,浮点类型字面量,字符字面量等
符号引用: 符号引用以一组符号来描述所引用的目标.符号引用可以是任何形式的字面量,JAVA在编译的时候每一个java类都会被编译成一个class文件,但在编译的时候并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续JVM类加载会具体降到)就是为了把这个符号引用转化成真正的地址的阶段
一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址。
6.2 运行时常量池 运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,他包括了若干种不同的常量;从编译期间可知的数值字面量到必须运行期解析后才能获得的方法或字段引用.(这个是虚拟机规范中的描述,很生涩)
运行时常量池是在类加载完成之后,将Class常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用.运行时常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)
在JDK1.8中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father".变动的知识方法区中内容的物理存放位置,但是运行时常量池和字符串常量池被移动到了堆中.但是不论它们物理上如何存放,逻辑上还是属于方法区的.
字符串常量池这个概念是最有争议的,因为没有这个概念的官方定义,所以与运行时常量池的关系不去抬杠,我们从它的作用和JVM设计它用于解决什么问题的点来分析.
以JDK1.8为例,字符串常量池是存放在堆中的,并且与java.lang.String类有很大关系.设计这块内存区域的原因在于: String对象作用java语言中重要的数据类型,是内存中占据空间最大的一个对象.高效的使用字符串,可以提升系统的整体性能.
所以要彻底弄懂,我们的重心其实在于深入理解String
string对象是对char数组进行了封装实现的对象,主要有2个成员变量: char数组,hash值.
了解了 String对象的实现后,你有没有发现在实际代码中String 类 被 final关键字修饰了,而且变量char数组也被final修饰了.
我们知道类被final修饰代表该类不可继承,而char[]被final+private 修饰,代表了String对象不可以被更改,java实现的这个特性叫做String类的不可变性,即String对象一旦创建成功,就不能再对它进行改变.
java这样做的好处在哪里?
第一, 保证String对象的安全性,假设String对象是可变的,那么String对象将可能被恶意修改.
第二, 保证hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现的 key-value缓存功能
第三, 可以实现字符串常量池.在java中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str = “abc”; 另一种是字符串变量通过new 形式的创建,如 String str = new String(“abc”);
1.String str = “abc”;
当代码中使用这种方式创建字符串对象时,JVM首先会检查该对象是否存在字符串常量池中,如果在,就返回该对象的引用,否则新的字符串将在常量池中被创建.这种方式可以减少同一个值的字符串对象的重复创建.节约内存(str只是一个引用)
2.String str = new String(“abc”);
首先在编译类文件时,"abc"常量字符串将会放入常量结构中,在类加载时,"abc"将会在常量池中创建;其次,在调用new时,jvm命令将会调用String的构造函数,在堆内存中创建一个String对象,同时引用常量池中的"abc"字符串;最后,str将引用String对象
使用new,对象会创建在堆中,同时赋值的话,会在常量池中创建一个字符串对象,复制到堆中。
具体的复制过程是先将常量池中的字符串压入栈中,在使用String的构造方法是,会拿到栈中的字符串作为构方法的参数。这个构造函数是一个char数组的赋值过程,而不是new出来的,所以是引用了常量池中的字符串对象。存在引用关系。
注:
public void mode(){ Location location = new Location(); location.setCity("北京"); location.setRegion("A1000"); }
3.String str = “ab”+“cd”+“ef”;
编程过程中,字符串的拼接很常见.但是String对象是不可变的,如果我们使用String对象相加,拼接我们想要的字符串,是不是就会产生多个对象呢?
例如:
String str = "ab"+"cd"+"ef";
分析代码可知:首先会生成ab对象,再生成abcd对象,最后生成abcdef对象,从理论上来说,这段代码是低效的
编译器自动优化了这行代码,编译后的代码如下
String str = "abcdef";
4.大循环使用+
public void mode1(){ String str = "abcdef"; for (int i = 0; i < 1000; i++) { str = str + i; } //编译器同样对这段代码进行了优化,java在进行字符串的拼接时,偏向使用StringBuilder,这样可以提高效率 // String str = "abcdef"; // for (int i = 0; i < 10000; i++) { // str = new StringBuilder(str).append(i).toString(); // } }
intern
Stirng的intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用
public static void mode2(){ String a = new String("文轩"); String b = new String("文轩").intern(); String c = "文轩"; System.out.println("a==b::"+(a==b)); System.out.println("a==c::"+(a==c)); System.out.println("b==c::"+(b=c)); }
结果
- new String()会在堆内存中创建一个a的String对象,"文轩"将会在常量池中创建
- 调用 new String()会在堆内存中创建一个b的String对象.
- 在调用intern方法之后,回去常量池中查找是否有等于该字符串对象的引用,有就返回引用.所以b和c引用的是同一个对象
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)