JVM入门
1 JVN入门探究
- 谈谈对JVM的理解?java8虚拟机和之前的变化更新?
- OOM内存溢出,*Error栈溢出,怎么分析?
- JVM 的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,类加载器你的认识?rt-jar ext applicatoin
2 JVM的体系研究
2.1 JVM位置
2.2 JVM的体系结构
- 栈百分百没有垃圾(因为栈的运行方式,不存会在垃圾)
- 所谓的 JVM 调优,99%都是在调方法区和堆,其中主要的是堆!
- 第三方插件,主要是在执行引擎上做,类加载器上比较少
2. 类加载器
**作用:**加载Class文件
- 类是抽象的,对象是具体的
- 对象的引用地址在栈,具体数据在堆
- 第三方插件,主要是在执行引擎上做,类加载器上比较少
2.1 理解类加载器:
class Test{
public static int a = 1;
}
//我们程序中给定的是 public static int a = 1;
但是在加载过程中的步骤如下:
- 加载阶段 编译文件为 .class文件,然后通过类加载,加载到JVM
- 连接阶段
第一步(验证):确保Class类文件没问题
第二步(准备):先初始化为 a=0。(因为你int类型的初始值为0)
第三步(解析):将引用转换为直接引用 - 初始化阶段: 通过此解析阶段,把1赋值为变量a
输出都为同一个类:
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
Class<? extends Car> aClass = car1.getClass();
Class<? extends Car> aClass1 = car2.getClass();
Class<? extends Car> aClass2 = car3.getClass();
System.out.println(aClass);
System.out.println(aClass2);
System.out.println(aClass2);
}
}
Class Car
2.2 类加载器的执行顺序
类的加载指的是将类的.class文件中二进制数据读入到内存中,将其放在运行时数据区内的方法区内,然后再内存中创建一个 java.lang.Class 对象用来封装类在方法区内的数据结构。
- 对于静态字段来说,只有直接定义了该字段的类才会被初始化;
- 当一个类在初始化时,要求其父类全部都已经初始化完毕了;
- 所有Java虚拟机实现必须在每个类或者接口被Java程序“首次主动使用”时才初始化他们
public class Ltest {
public static void main(String[] args) {
System.out.println(Child.str2);
}
}
class MyParent{
public static String str1 = "hello";
static {
System.out.println("MyParent static");
}
}
class Child extends MyParent{
public static String str2 = "world";
static {
System.out.println("Child static");
}
}
输出:
MyParent static
Child static
world
2.3 常量池的概念
常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中 本质上,调用类并没有直接用用到定义常量的类,因此并不会触发定义常量的类的初始化。 注意:这里指的是将常量存放到了MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系 了。
public class Ltest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str3);
}
}
class MyParent2{
public static final String str3 = "qwe";
static {
System.out.println("MyParent2 static");
}
}
qwe
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中, 这是在程序运行时,会导致主动使用这个常量所在的类,显然就会导致这个类被初始化。
import java.util.UUID;
public class Ltest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str3);
}
}
class MyParent2{
public static final String str3 = UUID.randomUUID().toString();
static {
System.out.println("MyParent2 static");
}
}
因为这个例子的值,是只有当运行期才会被确定的值,所以需要运行static方法。而上一个例子的值,是编译时就能被确定的值。
MyParent2 static
669d1384-d4e9-4e97-bb76-7d96058b58a6
2.4 ClassLoader分类
类加载器的分类
1、Java虚拟机自带的加载器
- 根类加载器(BootStrap)(BootClassLoader) sun.boot.class.path (加载系统的包,包含jdk核
心库里的类) - 扩展类加载器(Extension)(ExtClassLoader) java.ext.dirs(加载扩展jar包中的类)
- 系统(应用)类加载器(System)(AppClassLoader) java.class.path(加载你编写的类,编译后
的类)
2、用户自定义的类加载器 - Java.long.ClassLoader的子类(继承),用户可以定制类的加载方式
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Class<? extends Car> aClass = car1.getClass();
ClassLoader classLoader = aClass.getClassLoader();
System.out.println(classLoader);//AppClassLoader 应用程序加载器
System.out.println(classLoader.getParent());//PlatformClassLoader 平台类加载器 \jre\lib\ext
System.out.println(classLoader.getParent().getParent());//null 1.不存在 2.Java程序获取不到 rt.jar
}
}
输出:
jdk.internal.loader.ClassLoaders$AppClassLoader@2f0e140b
jdk.internal.loader.ClassLoaders$PlatformClassLoader@16b98e56
null
3 双亲委派机制
- 为了保证核心 class 安全,不被篡改
- 防止重复加载同一个 class
- APP --> EXC --> BOOT (最终*执行)
- BOOT --> EXC --> APP (当*没有,再一级一级往回执行)
- 执行顺序
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动加载器检查是否能够加载当前这个类载(使用findClass()方法),能加载就结束,使用当前的加载器
4.否则,抛出异常,通知子加载器进行加载
5.重复 3 、4 步骤
String 默认情况下是启动类加载器进行加载的,自定义一个String:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println(1);
}
}
发现自定义的String 可以正常编译,但是永远无法被加载运行。
这是因为申请自定义String 加载时,总是启动类加载器,而不是自定义加载器,也不会是其他的加载器。
双亲委派机制可以确保Java核心类库所提供的类**,不会被自定义的类所替代**。
4 Native方法
编写多线程类启动
public static void main(String[] args) {
System.out.println(MyParent2.str3);
new Thread(()->{
},"123").start();
}
}
start方法源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//这个Thread是一个类,这个方法定义在这里是不是很诡异!看这个关键字native;
private native void start0();
凡是带了native关键字的,说明 java的作用范围达不到,去调用底层C语言的库!
JNI:Java Native Interface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈;
Native Method Stack 本地方法栈
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java在诞生的时候是
C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标
记为native的代码,它的具体做法是 在 Native Method Stack 中登记native方法,在 ( Execution
Engine ) 执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统
管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用
Socket通信,也可以使用Web Service等等,不多做介绍
4 程序计数器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的。
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是一个非常小的内存空间,几乎可以忽略不计。
5 方法区
Method Area 方法区 是 Java虚拟机规范 中定义的运行时数据区域之一,它与堆(heap)一样在线程之间共享。
Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
JDK7 之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。每当一个类初次被加载的时候,它的元数据都会被放到永久代中。永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出,即 java.lang.OutOfMemoryError: PermGen。
JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置内存大小。
如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。
6 栈(Stack)
栈:后进先出 / 先进后出
队列:先进先出(FIFO : First Input First Output)
栈管理程序运行
存储一些基本类型的值、对象的引用、方法等。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
思考为什么main方法最后执行!为什么一个test() 方法执行完了,才会继续走main方法!
栈是每个线程独有的,每次调用函数时,栈就向下增长,调用完就弹出栈,main函数的栈没法和其他函数的栈重叠,因为当main函数出栈时,主线程就结束了。即使还有其他线程在运行,其他线程拥有自己的堆栈,相互也不会影响的。
说明:
1、栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程
结束栈内存也就释放。
2、对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程
私有的。
3、方法自己调自己就会导致栈溢出(递归死循环测试)
public class StackDemo {
public static void main(String[] args) {
a();
}
private static void a() {
b();
}
private static void b() {
a();
}
}
6.1 栈运行原理
Java栈的组成元素—栈帧
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的,一个线程有自己
的一个栈帧。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生了栈帧
F2也被压入栈中,B方法又调用了C方法,于是产生栈帧F3也被压入栈中…执行完毕后,先弹出F3,
然后弹出F2,在弹出F1…
遵循 “先进后出” / “后进先出” 的原则。
7 堆(Heap)
Java7之前
Heap 堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需
要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为
三部分:
- 新生区 Young Generation Space Young/New
- 养老区 Tenure generation space Old/Tenure
- 永久区 Permanent Space Perm
堆内存逻辑上分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)
GC垃圾回收主要是在 新生区和养老区,又分为 轻GC 和 重GC,如果内存不够,或者存在死循环,就会导致 java.lang.OutOfMemoryError: Java heap space
7.1 新生区
新生区是类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区
被new出来的,幸存区有两个:0区 和 1区,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾
回收器将对伊甸园区进行垃圾回收(Minor GC)。将伊甸园中的剩余对象移动到幸存0区,若幸存0区也
满了,再对该区进行垃圾回收,然后移动到1区,那如果1区也满了呢?(这里幸存0区和1区是一个互相
交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(Full GC),进行养老
区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常
“OutOfMemoryError ”。
如果出现 java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因
如下:
1、Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环
7.2 Sun HotSpot内存管理
分代管理,不同的区域使用不同的算法:
真相:经过研究,不同对象的生命周期不同,在Java中98%的对象都是临时对象。
7.3 永久区(Perm)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存
储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释
放此区域所占用的内存。
如果出现 java.lang.OutOfMemoryError:PermGen space,说明是 Java虚拟机对永久代Perm内存设
置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包,例如:在一个Tomcat下部署
了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
注意:
Jdk1.6之前: 有永久代,常量池1.6在方法区
Jdk1.7: 有永久代,但是已经逐步 “去永久代”,常量池1.7在堆
Jdk1.8及之后:无永久代,常量池1.8在元空间
熟悉三区结构后方可学习JVM垃圾回收机制
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载
的:类信息+普通常量+静态常量+编译器编译后的代码,虽然JVM规范将方法区描述为堆的一个逻辑部
分,但它却还有一个别名,叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为 “永久代(Parmanent Gen)”,但严格本质上说
两者不同,或者说使用永久代实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个
实现,Jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本,字段,方法,接口描述信息
外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放!
7.3 堆内存调优
了解完基本的堆的信息之后,我们就可以简单学习下关于堆内存调优的说明了!我们是基于 HotSpot 虚
拟机的,JDK1.8;
JDK1.7:
JDK 1.8:
使用 IDEA 调整堆内存大小测试
堆内存调优
-Xms :设置初始分配大小,默认为物理内存的 “1/64”
-Xmx :最大分配内存,默认为物理内存的 “1/4”
-XX:+PrintGCDetails :输出详细的GC处理日志
代码测试:
public class Demo01 {
public static void main(String[] args) {
//返回Java虚拟机试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory();
//返回Java虚拟机中的内存总量
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("MAX_MEMORY"+maxMemory+"(字节)、"+(maxMemory/(double)1024/1024)+"MB");
System.out.println("MAX_MEMORY"+totalMemory+"(字节)、"+(totalMemory/(double)1024/1024)+"MB");
}
}
MAX_MEMORY4263510016(字节)、4066.0MB
MAX_MEMORY268435456(字节)、256.0MB
在IDEA中进行JVM调优参数设置,然后启动
发现,默认的情况下分配的内存是总内存的 1/4,而初始化的内存为 1/64 !
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
VM参数调优:把初始内存,和总内存都调为 1024M,运行,查看结果!
[0.005s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.017s][info ][gc] Using G1
[0.026s][info ][gc,init] Version: 15.0.1+9-18 (release)
[0.026s][info ][gc,init] CPUs: 12 total, 12 available
[0.026s][info ][gc,init] Memory: 16257M
[0.026s][info ][gc,init] Large Page Support: Disabled
[0.026s][info ][gc,init] NUMA Support: Disabled
[0.026s][info ][gc,init] Compressed Oops: Enabled (32-bit)
[0.026s][info ][gc,init] Heap Region Size: 1M
[0.026s][info ][gc,init] Heap Min Capacity: 1G
[0.026s][info ][gc,init] Heap Initial Capacity: 1G
[0.026s][info ][gc,init] Heap Max Capacity: 1G
[0.026s][info ][gc,init] Pre-touch: Disabled
[0.026s][info ][gc,init] Parallel Workers: 10
[0.026s][info ][gc,init] Concurrent Workers: 3
[0.026s][info ][gc,init] Concurrent Refinement Workers: 10
[0.026s][info ][gc,init] Periodic GC: Disabled
[0.027s][info ][gc,metaspace] CDS archive(s) mapped at: [0x0000000800000000-0x0000000800b50000-0x0000000800b50000), size 11862016, SharedBaseAddress: 0x0000000800000000, ArchiveRelocationMode: 0.
[0.027s][info ][gc,metaspace] Compressed class space mapped at: 0x0000000800b50000-0x0000000840b50000, size: 1073741824
[0.027s][info ][gc,metaspace] Narrow klass base: 0x0000000800000000, Narrow klass shift: 3, Narrow klass range: 0x100000000
MAX_MEMORY1073741824(字节)、1024.0MB
MAX_MEMORY1073741824(字节)、1024.0MB
[0.154s][info ][gc,heap,exit] Heap
[0.154s][info ][gc,heap,exit] garbage-first heap total 1048576K, used 2048K [0x00000000c0000000, 0x0000000100000000)
[0.154s][info ][gc,heap,exit] region size 1024K, 3 young (3072K), 0 survivors (0K)
[0.154s][info ][gc,heap,exit] Metaspace used 1020K, capacity 4550K, committed 4864K, reserved 1056768K
[0.154s][info ][gc,heap,exit] class space used 96K, capacity 405K, committed 512K, reserved 1048576K
Process finished with exit code 0
再次证明:元空间并不在虚拟机中,而是使用本地内存。
7.4 Dump内存快照
1、IDEA插件安装
2、安装JProfile监控软件
下载地址:https://www.ej-technologies.com/download/jprofiler/version_92
3、安装:安装路径,建议选择一个文件名中没有中文,没有空格的路径 ,否则识别不了。然后一直点Next
4、注册码:
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
5、配置IDEA运行环境
Settings–Tools–JProflier–JProflier executable选择JProfile安装可执行文件。(如果系统只装了一个版本,
启动IDEA时会默认选择)保存
6、选择你要分析的项目,点击JProfiler图标启动, 启动完成会自动弹出JProfiler窗口,在里面就可以监
控自己的代码性能了。
代码测试:
import java.util.ArrayList;
public class Demo03 {
byte[] byteArray = new byte[1*1024*1024];//1M=1024K
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03());
count+=1;
}
} catch (Error e) {
System.out.println("count:" + count);
e.printStackTrace();
}
}
}
VM参数: -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid21432.hprof ...
Heap dump file created [6424272 bytes in 0.010 secs]
count:3
java.lang.OutOfMemoryError: Java heap space
at Demo03.<init>(Demo03.java:3)
at Demo03.main(Demo03.java:9)
Process finished with exit code 0
找到java_pid21432.hprof的位置,并直接打开,默认使用JProfiler进行打开。
GC详解
沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
- java中的安全模型
在Java中将执行程序分成本地代码和远程代码两种,**本地代码默认视为可信任的,而远程代码则被看作是不受信的。**对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。 - JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。