欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM学习第一节

程序员文章站 2022-03-11 17:35:47
...

JVM学习之路1

你好! 本系列章节内容多出自于JVM虚拟机一书,有兴趣的可以对照该书进行阅读,为了方便,里面很多例子我也是用了书中的内容,图片多出自于网上找的,或是别人的图我截的,请见谅,本人画图水平实在不咋地

读者知悉

你好! 本系列章节内容多出自于JVM虚拟机一书,有兴趣的可以对照该书进行阅读,为了方便,里面很多例子我也是用了书中的内容,图片多出自于网上找的,或是别人的图我截的,请见谅,本人画图水平实在不咋地

JVM内存模型

JVM学习第一节

Java运行时数据区也就是我们的Java虚拟机主要分为以下几部分:
1.程序计数器:线程私有,放的是正在执行的或者下一行执行的JVM指令码,字节码解释器工作的时候通过改变这个计数器的值来选取
下一条需要的字节码指令,例如:分支、循环、跳转、异常处理、线程恢复等。如果线程执行的是一个方法,那么计数器记录
的就是方法的字节码指令的地址,如果是native方法,计数器就为空,计数器这块区域是唯一一个在JAVA虚拟机中没有规定
OutOfMemoryError情况的区域。

2.JAVA虚拟机栈:线程私有,描述的是JAVA方法执行的内存模型,每个方法执行时都会创建一个栈桢,用于存储局部变
量表(例如基本数据类型),操作数栈,动态链接,方法出口等信息。
(1)局部变量表:主要包含boolean,byte,char,short,int,float,long,double,reference(对象引用),returnAddress类型
当我们进入方法的时候,该方法需要多少的内存空间是在编译的时候就已经确定了。
(2)操作数栈:操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。例如a+b 该过程就是在操作数栈中进行,中转存储,
在方法的执行过程中,会有各种各样的字节码指令往操作数栈中写入和提取内容(出栈/入栈)
(3)动态链接:存储的是方法的指令码,运行过程中动态生成的。方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的
调用版本在运行期是不可改变的。可以概括为:编译期可知、运行期不可变。这个是静态链接。但是我们在使用抽象类或者实现类的时候,
编译期我们往往不知道我们的具体类型,那么这个关联关系在运行时生成,那么就是动态链接(个人理解,不当之处还请谅解)
《深入理解JAVA虚拟机》一书中说class文件的常量池中存在得有大量的引用,字节码中的方法调用指令就以常量池中指向方法的符号引用
作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这叫静态解析。另一部分将在每一次运行期间转化为
直接引用,这个叫动态链接。
(4)(方法出口)方法返回地址:一个方法执行后只有两种结果退出该方法,一种是执行结束直接返回,另一种是发生异常就是异常返回,栈桢中通常
不会保存异常退出的信息。

3.本地方法栈:也就是我们常用的native方法,和虚拟机栈的功能类似,只不过本地方法栈是为native方法服务的,虚拟机栈是执行Java方法的。

4.JAVA堆:在虚拟机启动时创建,是被所有线程共享的一片区域,JAVA堆是垃圾回收器管理的主要区域,主要分为新生代,老年代,再细致一点
又分为eden区,From survivor空间,To survivor空间等,存储的是对象实例。static修饰的区域主要存放在方法区。如果我们的堆无法扩展
那么将会发生堆溢出异常,我们通常研究的最多的就是这块,调优也是这块

5.方法区:各个线程共享的区域,主要存放常量静态变量、类信息、即时编译器编译后的代码数据,也有另一个叫法(Non-Heap)非堆区,在JDK1.7以前
这块有另一种叫法叫永久代,现在1.8的版本的里面叫元空间,真正的数据存放在直接内存中。
(1)运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息之外,还有常量池用来存放编译期生成
的各种字面量和符号引用,这部部分内容在类加载后进入方法区的运行时常量池中存放。直接引用也存放在常量池中。
(2)直接内存:直接内存不会受到JAVA堆大小限制,受到本机的硬件限制,我们基于通道的I/O 也是存放在该区域,我们申请大对象的时候,也有可能
在该区域进行操作 如下图:各区域显示情况:
JVM学习第一节

内存溢出常见的几种情况

这块demo学习后对内存溢出这一块可能会更加的加深印象

代码demo1

package com.bjsxt.jconsole;

import java.util.ArrayList;
import java.util.List;

/** 
 * @author libo 
 * @time   2019年9月1日 下午11:39:09 
 * 
 * VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError  ,设置获取Dump出现内存溢出时的快照以便事后分析
 设置堆的大小为20M,设置成一样的目的是避免堆自动扩展
 */
public class HeapOOM {
	static class OOMObjectAdd {}
	public static void main(String[] args) {
		List<OOMObjectAdd> list = new ArrayList<>();
		while (true) {
			list.add(new OOMObjectAdd());
		}
	}
}

通过以上代码我们可以查看到如下结果
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid21709.hprof …
Heap dump file created [27579156 bytes in 0.104 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.bjsxt.jconsole.HeapOOM.main(HeapOOM.java:21)
JAVA堆异常后,会跟着出现Java heap space,可以将dump文件拿出解析进行复现

代码片段2

虚拟机栈和本地方法栈溢出

package com.bjsxt.jconsole;
/** 
 * 栈溢出
 * 
 * VM参数: -Xss256k  设置栈内存容量
 * 
 *由于定义了大量的本地变量,增大此方法栈中本地变量的长度,结果抛出*Error异常时输出的堆栈深度相应缩小
 *理论上我们不应该给栈桢分配过大的空间,因为我们给每个线程分配的内存越大,越容易产生内存溢出,我们操作系统分配给每个
 *进程的内存是有限制的例如我们2G运行内存的机器,我们需要减去Xmx(最大堆容量),再减去MaxPermSize(最大方法去容量),
 *程序计数器可以忽略,如果虚拟机进程本身耗费的内存资源不计,那剩下的内存由虚拟机栈和本地方法栈瓜分,每个线程分配到
 *的容量越大,可以建成的资源数就越少,线程就容易把剩下的内存耗尽了。
 */
public class JavaVMStackSOF {
	
	private int stackLength = 1;
	
	public void stackLeak() {
		stackLength++;
		stackLeak();
		
	}
	public static void main(String[] args) {

		JavaVMStackSOF oom = new JavaVMStackSOF();
		try {
			oom.stackLeak();
		} catch (Throwable e) {
			System.out.println("栈的深度: "+oom.stackLength);
			throw e;
		}
		
	}
}

结果如下:
栈的深度: 2006Exception in thread “main”
java.lang.*Error
at com.bjsxt.jconsole.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
at com.bjsxt.jconsole.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
at com.bjsxt.jconsole.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
at com.bjsxt.jconsole.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)

创建线程导致内存溢出
如果建立过多的多线程导致内存溢出,在不能减少线程数或者更换虚拟机的情况下,就只能通过减少最大堆和减少
栈容量来换取更多的线程。

代码demo3

package com.bjsxt.jconsole;
/** 
 * @author libo 
 * 
 * VM: -Xss2M
 * 验证的时候容易造成系统假死,电脑卡得不要不要的,
 * 这块容易造成  Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
 切记要在JDK1.7中进行验证,1.8验证会失败,一直卡,但是一直在运行
 */
public class JavaVMStackOOM {

	public void dontStop() {
		while(true) {}
	}
	
	public void stackLeakByThread() {
		while(true) {
			Thread thread = new Thread(new Runnable() {
				@Override
				public void run() {
					dontStop();
				}
			});
			thread.start();
		}
	}
	
	public static void main(String[] args) {
		JavaVMStackOOM java = new JavaVMStackOOM();
		java.stackLeakByThread();
	}
}

代码demo4 方法区和运行时常量池溢出

package com.bjsxt.jconsole;

import java.util.ArrayList;
import java.util.List;

/** 
 * @time   2019年9月2日 下午9:28:51
 *  VM args:-XX:PermSize=10M -XX:MaxPermSize=10M   由于在JDK1.7以前的版本中,常量池分配在永久代中,所以通过限制
 *  永久代的大小来进行操作,让我们的常量池中的常量太多导致溢出 
 * 方法区和运行时常量池溢出
 * String 的intern方法是将字符常量刷到内存中
 */
public class RuntimeConstantPoolOOM {
	public static void main(String[] args) {

		List<String> list = new ArrayList<>();
		int i = 0;
		while(true) {
			list.add(String.valueOf(i++).intern());
		}
	}
}

我们一定要在JDK1.6中运行,否则会提示
Java HotSpot™ 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
Java HotSpot™ 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
在JDK1.6中会提示:Exception in thread “main” java.lang.OutOfMemoryError: Permgen space
我们需要安装一个1.6的JDK

代码demo5 本机直接内存溢出

package com.bjsxt.jconsole;

import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**

  • 本机直接内存溢出
  • VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M 此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定
  • 大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),在程序中中可以
  • 获得-XX:MaxDirectMemorySize的设置的值。

*/
public class DirectMemoryOOM {

private static final int _1MB = 1024*1024;
public static void main(String[] args) {
	List<ByteBuffer> buffers = new ArrayList<>();
    int count = 1;
    while (true) {
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1MB);
      buffers.add(byteBuffer);
      System.out.println(count++);
    }
}

}
打印结果如下:
1
2
3
4
5
6
7
8
9
10
Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.bjsxt.jconsole.DirectMemoryOOM.main(DirectMemoryOOM.java:22)

接下来更新JVM 垃圾收集器相关内容

垃圾收集器
1.垃圾收集器的分类
2.垃圾回收算法
3.理解GC日志
4.内存分配与回收策略,调优解读(第五章)
虚拟机性能监控工具的使用
JDK常用命令
类文件结构
虚拟机加载机制(类加载机制 第九章)
JAVA 语法糖(泛型类型擦除)
11章节(优化提升)
高效并发(12章)(内存可见性,锁)
字节码执行引擎
对象逃逸分析
接下来持续更新调优,class字节码信息阅读,JAVA常用监控工具使用,堆栈信息