JVM架构图
垃圾都在heap堆上产生,对象实例和数组都是在堆上分配的,GC主要是针对这两类数据进行回收。JVM调优也是针对堆调优的。
类的加载、连接、初始化
加载:查找并加载类的二进制数据
连接:
- 验证:保证被加载的类的正确性;
- 准备:给类静态变量分配内存空间,赋值一个默认的初始值;
- 解析:把类中的符号引用转换为直接引用。
在把java编译为class文件时,虚拟机并不知道所引用的地址;助记符,符号引用,转为真正的直接引用。
- 初始化:把正确的值赋给类的静态变量。
public class Test{
public static int a = 1;
}
// 1、加载 编译文件为 .class 文件,通过类加载,加载到JVM
// 2、连接
//验证(1) 保证Class类文件没有问题
//准备(2) 给int类型分配内存空间,a = 0;
//解析(3) 符号引用转换为直接引用
// 3、初始化
//经过这个阶段的解析,把1 赋值给 变量 a;
类的加载
package com.example.springboot.test.jvm;
public class test01 {
public static void main(String[] args) {
System.out.println(child.str);
}
}
class parent {
static String pStr = "parent str";
public parent () {
System.out.println("parent构造方法");
}
static {
System.out.println("parent static");
}
}
class child extends parent {
static String str = "child str";
static {
System.out.println("child static");
}
}
打印结果:
parent static
child static
child str
final常量
package com.example.springboot.test.jvm;
public class finalTest {
public static void main(String[] args) {
System.out.println(test02.str);
}
}
class test02 {
final static String str = "test str";
static {
System.out.println("test static");
}
}
打印结果:
test str
**分析:**final 常量在编译阶段的时候放入常量池,代码中把str常量放入了finalTest的常量池中,之后和finalTest和test02就没有关系了。如果str不是final,则会打印出test static。
如果把代码换成下面的
package com.example.springboot.test.jvm;
import java.util.Random;
public class finalTest {
public static void main(String[] args) {
System.out.println(test02.str);
}
}
class test02 {
final static String str = String.valueOf(new Random(10).nextInt());
static {
System.out.println("test static");
}
}
则输出结果为:
test static
-1157793070
分析: 当一个常量的值并非编译期间可以确定的,那这个值就不会被方法调用类的常量池中!程序运行期间的时候,会主动使用常用所在的类。
ClassLoader 分类
1、java虚拟机自带的加载器
- BootStrap 根加载器 (加载系统的包,JDK 核心库中的类 rt.jar)
- Ext 扩展类加载器 (加载一些扩展jar包中的类)
- Sys/App 系统(应用类)加载器 (我们自己编写的类)
2、用户自己定义的加载器
- ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载器
双亲委派机制
双亲委派机制 可以保护java的核心类不会被自己定义的类所替代,
一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推。
native方法
只要是带了native这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!
程序计数器
每个线程都有一个程序计数器,是线程私有的。
程序计数器就是一块十分小的内存空间;几乎可以不计。
作用: 看做当前字节码执行的行号指示器。
分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成!
方法区
方法区(Method Area )是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!
JDK1.7之前
永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等。。。。这些东西都会放到永久代中;
永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen
JDK1.8之后
彻底将永久代移除 HotSpot jvm ,取而代之的是元空间;
元空间就是方法区在HotSpot jvm中的实现;
方法区重要就是来存:类信息,常量,字符串、静态变量、符号引用、方法代码。
元空间和永久代,都是对JVM规范中方法区的实现。
元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存!
栈stack
什么是栈?
栈是管理程序运行的,存储一些基本类型的值、对象的引用、方法等。
栈的优势:存取速度比堆快!仅次于寄存器,栈的数据是不可以共享的;
栈里面是一定不会存在垃圾回收的问题的,只要线程一旦结束,该栈就结束了。生命周期和线程一致;
stack原理
java栈的组成元素:栈帧。
栈、堆、方法区的交互关系图:
栈中的对象引用引用堆中的具体的实例化对象,堆的实例化对象模板在方法区内。
三种JVM
- SUN 公司 HotSpot
- BEA 公司 JRockit
- IBM 公司 J9VM
堆heap
Java7之前:
Heap 堆,一个JVM实例中只存在一个堆,堆的内存大小是可以调节的。
可以存的内容:类、方法、常量、保存了类型引用的真实信息;
分为三个部分:
- 新生区:Young (Eden-s0-s1)(伊甸园区、幸存0区、幸存1区)
- 养老区:Old Tenure
- 永久区:Perm
堆内存在逻辑上分为三个部分:新生、养老、永久(JDK1.8以后,叫元空间)
物理上只有 新生、养老;元空间在本地内存中,不在JVM中!
GC 垃圾回收主要是在 新生区和养老区,又分为 普通的GC 和 Full GC,如果堆满了,就会爆出 OutOfMemory;
新生区
新生区 就是一个类诞生、成长、消亡的地方!
新生区细分: Eden、s(from to),所有的类Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区(),… 清理了 15次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入养老区!运行了几个月之后,养老区满了,就会触发一次 Full GC;假设项目1年后,整个空间彻彻底底的满了,突然有一天系统 OOM,排除OOM问题,或者重启;
Sun HotSpot 虚拟机中,内存管理(分代管理机制:不同的区域使用不同的算法!)
99% 的对象在 Eden 都是临时对象;
养老区
15次都幸存下来的对象进入养老区,养老区满了之后,触发 Full GC
默认是15次,可以修改!但是只能改小,不能超过15。
永久区(Perm)
放一些 JDK 自身携带的 Class、Interface的元数据;
几乎不会被垃圾回收的;
- JDK1.6之前: 有永久代、常量池在方法区;
- JDK1.7:有永久代、但是开始尝试去永久代,常量池在堆中;
- JDK1.8 之后:永久代没有了,取而代之的是元空间;常量池在元空间中;
方法区和堆一样,是共享的区域,是JVM 规范中的一个逻辑的部分,但是记住它的别名非堆
元空间:本地内存!