1
JVM 内存可简单分为三个区:
- 堆区(heap): 用于存放所有对象,是线程共享的
- 栈区(stack): 用于存放基本数据类型的数据和对象的引用,是线程私有的(分为:虚拟机栈和本地方法栈)
- 方法区(method): 用于存放类信息,常量,静态变量,编译后的字节码等,是线程共享的(也被称为非堆,即None-Heap)
Java 的垃圾回收器(GC) 主要针对堆区
2
由于跨平台性的设计,Java的指令都是根据栈来设计。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
JVM 生命周期
虚拟机启动
Java 虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成,这个类是由虚拟机的具体实现指定的
虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
- 程序开始执行时他才运行,程序结束时就停止
- 执行一个所谓的Java程序的时候,真正在执行的是一个叫做Java虚拟机的进程
虚拟机退出
有如下几种情况:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机终止
- 某线程调用Runtime类 或 System 类的exit方法,或halt方法
类加载器及类加载过程
- 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由 Execution Engine决定
- 加载的类信息存放于一块称为 方法区的内存空间。除了类的信息外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色
- class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候要加载到 JVM 当中来根据这个文件实例化出 n 个一模一样的实例
- class file 加载到 JVM 中,被称为 DNA 元数据模板,放在方法区
- 在 .class 文件 -> JVM -> 最终成为元数据模板,此过程就要一个 运输工具(类加载器 Class Loader) , 扮演一个快递员的角色
类加载过程
加载:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的放嗯入口
链接:
初始化:
- 初始化阶段就是执行类构造器方法<clinit>()的过程
- 此方法不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令按语句在源文件中出现的顺序执行
- <clinit>() 不同于类的构造器
- 虚拟机必须保证一个类的<clinit>() 方法在多线程下被同步加锁
类加载器的分类
- JVM 支持两种类加载器,分别为 引导类加载器(Bootstrap ClassLoader) 和 自定义类加载器(User-Defined ClassLoader)
- 从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范没有这么定义,而是 将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
关于 Class Loader
ClassLoader 类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
自定义类加载器源码
Launcher.java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package sun.misc;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.nio.file.Paths;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.HashSet;
import java.util.StringTokenizer;
import java.util.Vector;
import sun.net.www.ParseUtil;
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
public ClassLoader getClassLoader() {
return this.loader;
}
public static URLClassPath getBootstrapClassPath() {
return Launcher.BootClassPathHolder.bcp;
}
private static URL[] pathToURLs(File[] var0) {
URL[] var1 = new URL[var0.length];
for(int var2 = 0; var2 < var0.length; ++var2) {
var1[var2] = getFileURL(var0[var2]);
}
return var1;
}
private static File[] getClassPath(String var0) {
File[] var1;
if (var0 != null) {
int var2 = 0;
int var3 = 1;
boolean var4 = false;
int var5;
int var7;
for(var5 = 0; (var7 = var0.indexOf(File.pathSeparator, var5)) != -1; var5 = var7 + 1) {
++var3;
}
var1 = new File[var3];
var4 = false;
for(var5 = 0; (var7 = var0.indexOf(File.pathSeparator, var5)) != -1; var5 = var7 + 1) {
if (var7 - var5 > 0) {
var1[var2++] = new File(var0.substring(var5, var7));
} else {
var1[var2++] = new File(".");
}
}
if (var5 < var0.length()) {
var1[var2++] = new File(var0.substring(var5));
} else {
var1[var2++] = new File(".");
}
if (var2 != var3) {
File[] var6 = new File[var2];
System.arraycopy(var1, 0, var6, 0, var2);
var1 = var6;
}
} else {
var1 = new File[0];
}
return var1;
}
static URL getFileURL(File var0) {
try {
var0 = var0.getCanonicalFile();
} catch (IOException var3) {
}
try {
return ParseUtil.fileToEncodedURL(var0);
} catch (MalformedURLException var2) {
throw new InternalError(var2);
}
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
protected PermissionCollection getPermissions(CodeSource var1) {
PermissionCollection var2 = super.getPermissions(var1);
var2.add(new RuntimePermission("exitVM"));
return var2;
}
private void appendToClassPathForInstrumentation(String var1) {
assert Thread.holdsLock(this);
super.addURL(Launcher.getFileURL(new File(var1)));
}
private static AccessControlContext getContext(File[] var0) throws MalformedURLException {
PathPermissions var1 = new PathPermissions(var0);
ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
return var3;
}
static {
ClassLoader.registerAsParallelCapable();
}
}
private static class BootClassPathHolder {
static final URLClassPath bcp;
private BootClassPathHolder() {
}
static {
URL[] var0;
if (Launcher.bootClassPath != null) {
var0 = (URL[])AccessController.doPrivileged(new PrivilegedAction<URL[]>() {
public URL[] run() {
File[] var1 = Launcher.getClassPath(Launcher.bootClassPath);
int var2 = var1.length;
HashSet var3 = new HashSet();
for(int var4 = 0; var4 < var2; ++var4) {
File var5 = var1[var4];
if (!var5.isDirectory()) {
var5 = var5.getParentFile();
}
if (var5 != null && var3.add(var5)) {
MetaIndex.registerDirectory(var5);
}
}
return Launcher.pathToURLs(var1);
}
});
} else {
var0 = new URL[0];
}
bcp = new URLClassPath(var0, Launcher.factory, (AccessControlContext)null);
bcp.initLookupCache((ClassLoader)null);
}
}
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
void addExtURL(URL var1) {
super.addURL(var1);
}
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
private static URL[] getExtURLs(File[] var0) throws IOException {
Vector var1 = new Vector();
for(int var2 = 0; var2 < var0.length; ++var2) {
String[] var3 = var0[var2].list();
if (var3 != null) {
for(int var4 = 0; var4 < var3.length; ++var4) {
if (!var3[var4].equals("meta-index")) {
File var5 = new File(var0[var2], var3[var4]);
var1.add(Launcher.getFileURL(var5));
}
}
}
}
URL[] var6 = new URL[var1.size()];
var1.copyInto(var6);
return var6;
}
public String findLibrary(String var1) {
var1 = System.mapLibraryName(var1);
URL[] var2 = super.getURLs();
File var3 = null;
for(int var4 = 0; var4 < var2.length; ++var4) {
URI var5;
try {
var5 = var2[var4].toURI();
} catch (URISyntaxException var9) {
continue;
}
File var6 = Paths.get(var5).toFile().getParentFile();
if (var6 != null && !var6.equals(var3)) {
String var7 = VM.getSavedProperty("os.arch");
File var8;
if (var7 != null) {
var8 = new File(new File(var6, var7), var1);
if (var8.exists()) {
return var8.getAbsolutePath();
}
}
var8 = new File(var6, var1);
if (var8.exists()) {
return var8.getAbsolutePath();
}
}
var3 = var6;
}
return null;
}
private static AccessControlContext getContext(File[] var0) throws IOException {
PathPermissions var1 = new PathPermissions(var0);
ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
return var3;
}
static {
ClassLoader.registerAsParallelCapable();
instance = null;
}
}
private static class Factory implements URLStreamHandlerFactory {
private static String PREFIX = "sun.net.www.protocol";
private Factory() {
}
public URLStreamHandler createURLStreamHandler(String var1) {
String var2 = PREFIX + "." + var1 + ".Handler";
try {
Class var3 = Class.forName(var2);
return (URLStreamHandler)var3.newInstance();
} catch (ReflectiveOperationException var4) {
throw new InternalError("could not load " + var1 + "system protocol handler", var4);
}
}
}
}
获取ClassLoader
package org.free.demo.classloader;
public class ClassLoaderDemo {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
}
}
获取上层类加载器
package org.free.demo.classloader;
public class ClassLoaderDemo {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//获取上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
}
}
Java 核心类库都是使用 引导类加载器进行加载
引导类加载器
扩展类加载器
应用程序类加载器
为什么需要自定义类加载器
自定义类加载器的步骤
对类加载器的引用
双亲委派机制
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
沙箱安全机制
类的主动使用和被动使用
运行时数据区内部结构
阿里jvm示意图
每个虚拟机有且只对应一个 Runtime实例
线程
- 线程是一个程序里的运行单元。JVM 允许一个应用由多个线程并行的执行
- 在 Hotspot JVM 里,每个线程都与操作系统的本地线程直接映射。当一个 Java 线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法
JVM 系统线程
程序计数器
JVM 中的 PC寄存器是对物理PC 寄存器的一种抽象模拟
虚拟机栈
由于跨平台的设计,Java 的指令都是根据 栈 来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的
优点:跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
内存中的栈与堆
栈是 运行时 的单位,而堆是 存储 的单位
栈解决程序的运行问题,即 程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即 数据怎么放,放在哪儿。
基本内容
优点
栈可能出现的异常
设置栈内存大小
我们可以使用参数 -Xss 选项来设置线程的最大占空间,栈的大小直接决定了函数调用的最大可达深度
栈的存储单位
- 每个线程都有自己的栈,栈中的数据都是以 **栈帧(Stack Frame) **的格式存在
- 在这个线程上正在执行的每个方法都各自对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈运行原理
栈帧的内部结构
每个栈帧中存储着:
- 局部变量表
- 操作数栈
- 动态链接(指向运行时常量池的方法引用)
- 方法返回地址 (方法正常退出或者异常退出的定义)
- 一些附加j信息
局部变量表(重要)
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义一个数字数组,主要用于存储 方法参数 和 定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此 不存在数据安全问题
- 局部变量表所需的容量大小是在 编译期 定下来的,并保存在方法的 Code 属性的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表大小的
变量槽slot
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
- 局部变量表,最基本的存储单元是 slot(变量槽)
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量
- 在局部变量表里,32位以内的类型只占用一个 slot(包括returnAddress类型),64位的类型(long 和 double) 占用两个 slot
- byte,short,char 在存储前被转换为 int;boolean 也被转换为 int
- long 和 double 则占据两个 slot
实例方法对应的栈帧的数组种的第一个元素即为 this
public void test(int arg1,long arg2){
int myPrint = 1;
System.out.println(myPrint);
}
反编译的结果
public void test(int, long);
descriptor: (IJ)V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=3
0: iconst_1
1: istore 4
3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
6: iload 4
8: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
11: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 11
LocalVariableTable:(重点关注这里,发现数组第一个元素即为this,其他才是 方法参数 和 局部变量 )
Start Length Slot Name Signature
0 12 0 this Lorg/free/demo/stack/StackErrorDemo;
0 12 1 arg1 I
0 12 2 arg2 J
3 9 4 myPrint I
MethodParameters:
Name Flags
arg1
arg2
静态方法种没有 this 变量
public static void test2(){
int i1 = 1;
int i2 = 2;
System.out.println("test2");
}
反编译的结果
public static void test2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: iconst_1
1: istore_0
2: iconst_2
3: istore_1
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #4 // String test2
9: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 18: 0
line 19: 2
line 20: 4
line 21: 12
LocalVariableTable:(重点故关注这里,发现只有局部变量 i1 和 i2)
Start Length Slot Name Signature
2 11 0 i1 I
4 9 1 i2 I
slot 的重复利用
栈帧中的局部变量表中的槽位是可以重用的,
操作数栈(重要)
- 每个独立的栈帧中除了包含局部变量表以外,还包含一个 后进先出 的操作数栈,也可称之为表达式栈
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
- 某些字节码指令将值压入操作操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
- 比如:执行赋值,交换,求和等操作
- Java 虚拟机的解释引擎是基于 栈 的执行引擎,其中栈指的是操作数栈
动态链接
方法返回地址
- 存放调用该方法的pc寄存器的值
- 一个方法的结束,有两种方式
- 正常执行完成
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过 异常表 来确定,栈帧中一般不会保存这部分信息。
实例:
栈顶缓存技术
方法的调用
在 JVM 中,将 符号引用 转换为调用方法的 直接引用 与方法的绑定机制有关
-
静态链接
当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接
-
动态链接
如果 **被调用的方法在编译期无法确定下来,**也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
虚函数 是不能再 编译期 确定该调用哪个版本
虚拟机提供了以下几条方法调用指令
- 普通调用指令
- invokestatic:调用静态方法
- invokespecial:调用<init>方法,私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- 动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
堆区
转自:
堆时 OOM 故障最主要的区域,它是内存中区域最大的一块区域,被所有线程共享。存储着几乎所有的实例对象。所有的对象实例以及数组都要在堆上分配。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java 堆是垃圾收集器管理的主要区域。从内存回收的角度看,由于现在收集器基本都采用 分代收集算法,所以 Java堆还可以细分为:新生代和老年代。
新生代
- Eden空间
- From Survivor空间
- To Survivor空间
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
堆区调整
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整
如何调整呢?
通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M
,其中 -X
这个字母代表它是JVM运行时参数,ms
是memory start
的简称,中文意思就是内存初始值,mx
是 memory max
的简称,意思就是最大内存。
值得注意的是,在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力 所以在线上生产环境中 JVM的Xms
和 Xmx
会设置成同样大小,避免在GC 后调整堆大小时带来的额外压力。
堆的默认空间分配
堆空间内存分配的大体情况
执行如下命令,就可以看到当前 JDK 版本所有默认的 JVM 参数
java -XX:+PrintFlagsFinal -version
对应输出应该有几百行,主要看两个关键参数
>java -XX:+PrintFlagsFinal -version
[Global flags]
...
uintx InitialSurvivorRatio = 8
uintx NewRatio = 2
...
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
参数解释
参数 | 作用 |
---|---|
-XX:InitialSurvivorRatio | 新生代Eden/Survivor空间的初始比例 |
-XX:NewRatio | Old区/Young区的内存比例 |
因为新生代是由Eden + S0 + S1组成的,所以按照上述默认比例,如果eden区内存大小是40M,那么两个survivor区就是5M,整个young区就是50M,然后可以算出Old区内存大小是100M,堆区总大小就是150M。
-XX:+HeapDumpOnOutOfMemoryError
可以让JVM在遇到OOM异常时,输出堆内信息
创建一个新对象,内存分配流程
绝大部分对象在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection,即 YGC
。垃圾回收的时候,在 Eden 区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。Survivor区分为so和s1两块内存空间。每次YGC
的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC
要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了18岁就会成年一样,在JVM中-XX:MaxTenuringThreshold
参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是15, 可以在Survivor区交换14次之后,晋升至老年代。
元空间
在 HotSpot JVM 中,**永久代( ≈ 方法区)*中用于*存放类和方法的元数据以及常量池,比如 Class 和 Method。每当一个类被初次加载的时候,它的元数据都会放到永久代。
永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen
,为此我们不得不对虚拟机做调优。
那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?
- 由于 PermGen 内存经常会溢出,引发恼人的
java.lang.OutOfMemoryError: PermGen
,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM - 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量池移至堆区。
准确来说,Perm 区中的字符串常量池被移到了堆内存中是在Java7 之后,Java 8 时,PermGen 被元空间代替,其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如java/lang/Object
类元信息、静态属性System.out
、整形常量 100000
等。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。(和后面提到的直接内存一样,都是使用本地内存)
In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.
对应的JVM调参:
参数 | 作用 |
---|---|
-XX:MetaspaceSize | 分配给Metaspace(以字节计)的初始大小 |
-XX:MaxMetaspaceSize | 分配给Metaspace 的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。 |
-XX:MinMetaspaceFreeRatio | 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 |
-XX:MaxMetaspaceFreeRatio | 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 |
本地方法接口
什么是本地方法
*一个 Native Method就是一个 Java 调用非 Java 代码的接口。*一个 Native Method 是这样一个 Java 方法:该方法的实现由非 Java 语言实现,比如 C。
例如 Object.java 中的 getClass() 方法就是一个 native 方法
native 与 abstract 的区别
native 是有方法体的,只不过是用 C 来实现
而 abstract 是没有方法体的,具体由子类去实现。
native 和 abstract 两者不能共用。
变量
变量的分类:
1.基本数据类型 2.引用数据类型
按照在类中声明的位置分类:
-
成员变量:在使用前,都经历过默认初始化赋值
- 类变量:linking的prepare阶段,给类变量默认赋值 ----》 initial:给类变量显示赋值
- **实例变量:**随着对象创建,会在堆空间分配实例变量空间,并进行默认赋值
-
局部变量:在使用前,必须要进行显式赋值!否则,编译不通过
方法调用:关于 invokedynamic 指令
方法的调用:方法重写的本质
方法的调用:虚方法表
- 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM 采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
- 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。
垃圾回收器
对象引用分类
参考:
强引用
在代码中普遍存在的,类似Object obj = new Object()
这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用(SoftReference)
有用但并非必需 的对象,可用SoftReference
类来实现软引用。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用(WeakReference)
非必需 的对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK
提供了WeakReference
类来实现弱引用。无论当前内存是否足够,用软引用相关联的对象都会被回收掉。
虚引用(PhatomReference)
虚引用也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK
提供了PhantomReference
类来实现虚引用。为一个对象设置虚引用的唯一目的是:能在这个对象在垃圾回收器回收时收到一个系统通知
finalize() 二次标记
一个对象是否应该在垃圾回收器在GC
时回收,至少要经历两次标记过程。
第一次标记过程,通过可达性分析算法分析对象是否与GC Roots
可达。经过第一次标记,并且被筛选为不可达的对象会进行第二次标记。
第二次标记过程,判断不可达对象是否有必要执行finalize
方法。执行条件是当前对象的finalize
方法被重写,并且还未被系统调用过。如果允许执行那么这个对象将会被放到一个叫F-Query
的队列中,等待被执行。
注意:由于finalize由一个优先级比较低的Finalizer线程运行,所以该对象的的finalize方法不一定被执行,即使被执行了,也不保证finalize方法一定会执行完。如果对象第二次小规模标记,即finalize方法中拯救自己,只需要重新和引用链上的任一对象建立关联即可。
垃圾回收算法
参考:
标记-清除算法
- 标记
标记出所有需要回收的对象
**一次标记:**在经过 可达性分析算法 后,对象没有与 GC Root 相关的引用链,那么则被第一次标记,并且进行一次筛选:当对象有必要执行 finalize() 方法时,则将该对象放入 F-Queue 队列中
**二次标记:**对 F-Queue 队列中的对象进行二次标记。在执行 finalize() 方法时,如果对象重新与 GC Root 引用链上的任意对象建立了关联,则将其移除 “即将回收” 集合。否则即将被回收
对被第一次标记且被第二次标记的,就可以判定位可回收对象了。
- 清除
两次标记后,还在 “即将回收” 集合的对象进行回收
执行过程如下:
**优点:**最基础的可达性算法,后续的收集算法都是基于这种思想实现的
**缺点:**标记和清除效率不高,产生大量不连续的内存碎片,导致创建大对象找不到连续的空间,不得不提前触发另一次的垃圾回收
复制算法
将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉
复制算法过程如下:
**优点:**实现简单,效率高。解决了标记-清除算法导致的内存碎片问题
**缺点:**代价大,将内存缩小了一半。效率随对象的存活率升高而降低。
现在的商业虚拟机都采用这种算法(需要改良1:1的缺点)来回收新生代。
- 改良算法
1.弱代理论
分代垃圾收集基于弱代理论。具体描述如下:
- 大多说分配了内存的对象并不会存活太长时间,在处于年轻时代就会死掉。
- 很少有对象会从老年代变成年轻代。
其中IBM研究表明:新生代中98%的对象都是"朝生夕死"; 所以并不需要按1:1比例来划分内存(解决了缺点1);
2.Hotspot虚拟机新生代内存布局及算法
新生代内存分配一块较大的Eden空间和两块较小的Survivor空间
每次使用Eden和其中一块Survivor空间
回收时将Eden和Survivor空间中存活的对象一次性复制到另一块Survivor空间上
最后清理掉Eden和使用过的Survivor空间。
Hotspot虚拟机默认Eden和Survivor的大小比例是8:1。
分配担保
如果另一块 Survivor 空间没有足够内存来存放上一次新生代收集下来的存活对象,那么这些对象则直接通过担保机制进入老年代。
标记-整理算法
标记-整理算法是根据老年代的特点应运而生
- 标记
标记过程和标记-清理算法一致(也是基于可达性分析算法)。
- 整理
和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理。让存活对象都向一端移动,然后直接清理掉边界以外的内存。
标记-整理算法示意图
**优点:**不会像复制算法那样随着存活对象的升高而降低效率,不像标记-清除算法那样产生不连续的内存碎片
**缺点:**效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率更低。
分代收集算法
当前商业虚拟机的垃圾收集都是采用“ 分代收集 ”算法。
根据对象存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代。JVM根据各个年代的特点采用不同的收集算法。
新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此比较适合复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
老年代中,因为对象存活率较高,没有额外的空间进行分配担保,所以适合标记-清理、标记-整理算法来进行回收。
七种垃圾回收器
参考:
在 JVM
中,具体实现有
Serial
,ParNew
,Pallel Scavenge
,CMS
,Serial Old(MSC)
,Parallel Old
,G1
等。
在下图中,你可以看到 不同垃圾回收器 适合于 不同的内存区域,如果两个垃圾回收器之间 存在连线,那么表示两者可以 配合使用。
如果当 垃圾回收器 进行垃圾清理时,必须 暂停 其他所有的 工作线程,直到它完全收集结束。我们称这种需要暂停工作线程才能进行清理的策略为 Stop-the-World。
以上回收器中, Serial
、ParNew
、Parallel Scavenge
、Serial Old
、Parallel Old
均采用的是 Stop-the-World
的策略。
图中有 7
种不同的 垃圾回收器,它们分别用于不同分代的垃圾回收。
- **新生代回收器:**Serial,ParNew,Parallel Scavenge
- **老年代回收器:**Serial Old,Parallel Old,CMS
- **整堆回收器:**G1
两个 垃圾回收器 之间有连线表示它们可以 搭配使用,可选的搭配方案如下:
新生代 | 老年代 |
---|---|
Serial | Serial Old |
Serial | CMS |
ParNew | Serial Old |
ParNew | CMS |
Parallel Scavenge | Serial Old |
Parallel Scavenge | Parallel Old |
G1 | G1 |
单线程垃圾回收器
Serial收集器是最基本的、发展历史最悠久的收集器。
**特点:**单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于Client模式下的虚拟机。
Serial / Serial Old收集器运行示意图
Serial Old是Serial收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途(在后续中详细讲解···):
- 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
Serial / Serial Old收集器工作过程图(Serial收集器图示相同):
多线程垃圾回收器(吞吐量优先)
ParNew收集器其实就是Serial收集器的多线程版本。
除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
和Serial收集器一样存在Stop The World问题
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
ParNew/Serial Old组合收集器运行示意图如下:
与吞吐量关系密切,故也称为吞吐量优先收集器。
特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
-
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
-
XX:GCRatio 直接设置吞吐量的大小。
是Parallel Scavenge收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
Parallel Scavenge/Parallel Old收集器工作过程图:
其他的回收器(停顿时间优先)
一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的工作过程图:
CMS收集器的缺点:
-
对CPU资源非常敏感。
-
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
-
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
-
G1收集器
一款面向服务端应用的垃圾收集器。
特点如下:
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
区域化内存划片 Region,整体编为了一些不连续的内存区域,避免了全内存区的GC操作
核心思想是将整个堆内存区域分成大小相同的子区域(Region), 在 JVM 启动时会自动设置这些子区域的大小。
在堆的使用上,G1 并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB),默认将整堆划分为2048个分区
大小范围在 1MB~32MB,最多能设置 2048 个区域,也即能够支持的最大内存为:32MB * 2048 = 65536MB = 64G内存
针对 Eden 区进行收集,Eden 区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内存碎片
- Eden 区的数据移动到 Survivor 区,假如出现 Survivor 区空间不够,Eden 区数据会晋升到 Old 区
- Survivor 区的数据移动到新的 Survivor 区,数据晋升到 Old 区
- 最后 Eden 区收拾干净,GC 结束,用户的应用程序继续执行
G1为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。
G1与其他收集器的区别:
其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
G1收集器存在的问题:
Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。
G1收集器是如何解决上述问题的?
采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤:
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
G1收集器运行示意图: