JVM-Java虚拟机(一)

JVM

为什么学习 JVM

面试的需要

学过 Java 的程序员对 JVM 应该并不陌生。程序员为什么要学习 JVM 呢,其实不懂 JVM 也可以照样写出优质的代码,但是不懂 JVM 有可能别被面试官虐得体无完肤。

中高级程序员必备技能

项目管理,性能调优

虚拟机

所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。

大名鼎鼎的 VMware 就属于系统虚拟机,它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型的代表就是 java虚拟机了,它专门为执行某个单个计算机程序而设计。在 java 虚拟机中执行的指令我们称为 java 字节码指令。

Java 虚拟机是一种执行 java 字节码文件的虚拟计算机,它拥有独立的运行机制。

Java 技术的核心就是 java 虚拟机,因为所有的 java 程序都运行在 java 虚拟机内部。

JVM 作用

Java 虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条 java 指令,java 虚拟机中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪儿。

特点:

一次编译到处运行

自动内存管理

自动垃圾回收功能

现在的 JVM 不仅可以执行 java 字节码文件,还可以执行其他语言编译后的字节码文件,是一

个跨语言平台。

JVM 整体组成部分

1.类加载器(ClassLoader)(负责加载字节码文件)

2.运行时数据区(Runtime Data Area)(存储运时数据,堆,java虚拟机栈(运行java自己的方法),方法区,程序计数器,本地方法栈)

3.执行引擎(Execution Engine)(更底层,把字节码翻译为机器码)

4.本地库接口(Native Interface)

5.垃圾回收

程序在执行之前先要把 java 代码转换成字节码(class 文件),jvm 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中的运行时数据区(Runtime Data Area) ,而字节码文件是 jvm 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(NativeInterface) 来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能。

而我们通常所说的 JVM 组成指的是运行时数据区(Runtime DataArea),因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的 Heap(堆)模块。

JVM 结构-类加载

类加载子系统

类加载器子系统负责从文件系统或者网络中加载 class 文件。 classLoader只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

加载的类信息存放于一块称为方法区的内存空间。

class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个实例.

class file 加载到 JVM 中,被称为 DNA 元数据模板.

此过程就要有一个运输工具(类加载器 Class Loader),扮演一个快递员的角色.

类加载过程

加载

1. 通过类名(地址)获取此类的二进制字节流.

2. 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构.3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类的各种数据的访问入口。

链接

验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;

验证文件格式是否一致: class 文件在文件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头);主,次版本号是否在当前 java 虚拟机接收范围内.

元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类(final 修饰的类).....

准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值

不包含用 final 修饰的 static 常量,在编译时进行初始化.

例如: public static int value = 123;

value 在准备阶段后的初始值是 0,而不是 123.

解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文件的逻辑符号,直接引用指向的方法区中某一个地址).

初始化

初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。初始化阶段就是执行底层类构造器方法<clinit>()的过程。

此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的。

类什么时候初始化?

JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化.

  • 通过 new 关键字创建对象

  • 访问类的静态变量,包括读取和更新

  • 访问类的静态方法

  • 对某个类进行反射操作

  • 初始化子类会导致父类的的初始化

  • 执行该类的 main 函数

其实除了上面的几种主动使用,以下两种情况为被动使用不会加载类.

1.引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载,比如:

public final static int NUMBER = 5 ; //不会导致类初始化,被动使用

public final static int RANDOM = new Random().nextInt() ; //会导致类加载

2.构造某个类的数组时不会导致该类的初始化,比如:

Student[] students = new Student[10] ;

类的初始化顺序

对 static 修饰的变量或语句块进行赋值.如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

顺序是:父类 static –> 子类 static

类加载器分类

站在 JVM 的角度看,类加载器可以分为两种:

1. 引导类加载器(启动类加载器 Bootstrap ClassLoader).

2. 其他所有类加载器,这些类加载器由 java 语言实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader.

站在 java 开发人员的角度来看,类加载器就应当划分得更细致一些.自 JDK1.2 以来 java 一直保持者三层类加载器.

引导类加载器(启动类加载器 BootStrap ClassLoader)

这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库.

并不继承于 java.lang.ClassLoader 没有父加载器.

负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器.

出于安全考虑,引用类加载器只加载存放在<JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数锁指定的路径中存储放的类.

扩展类加载器(Extension ClassLoader)

Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现.

派生于 ClassLoader 类.

从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载.

应用程序类加载器(系统类加载器 Application ClassLoader)

Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.

派生于 ClassLoader 类.

加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类.

该类加载器是程序中默认的类加载器.

ClassLoader 类 , 它是一个抽象类 , 其后所有的类加载器都继承 自ClassLoader(不包括启动类加载器)

双亲委派机制

Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.

工作原理:

1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行.

2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器.

3. 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制. 如果均加载失败,就会抛出 ClassNotFoundException 异常。

思考:我们自己创建一个名为 java.lang 的包,再创建一个名为 String 的类,当我们new String()时,会将加载创建核心类库中的 String 对象还是创建我们自己创建的 String 类对象?

双亲委派优点?

1.安全,可避免用户自己编写的类替换 Java 的核心类,如 java.lang.String.

2.避免类重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次.

如何打破双亲委派机制

Java 虚拟机的类加载器本身可以满足加载的要求,但是也允许开发者自定义类加载器。

在 ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被 final 修饰,也就表示其他子类可以重写.

重写 loadClass 方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制, 不推荐)

重写 findClass 方法 (推荐)

我们可以通过自定义类加载重写方法打破双亲委派机制, 再例如 tomcat 等都有自己定义的类加载器.

JVM 运行时数据区

运行时数据区组成概述

JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:

Java 虚拟机定义了程序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.另外一些则是与线程一一对应的.

这些与线程对应的区域会随着线程开始和结束而创建销毁.

如图:红色的为多个线程共享,灰色的为单个线程私有的,

线程间共享:堆,方法区.

线程私有:程序计数器,栈,本地方法栈.

程序计数器(Program Counter Register)

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

是一块很小的内存空间,用来记录每个线程运行的指令位置,

是线程私有的,每个线程都拥有一个程序计数器,生命周期与线程一致,

是运行时数据区中,唯一 一个不会出现内存溢出的空间.

运行速度最快.

Java 虚拟机栈(Java Virtual Machine Stacks)

描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

基本作用特征:

栈是运行单位,管理方法的调用运行

是用来运行java方法的区域.

可能会出现栈溢出.

是线程私有的.

运行原理:

先进后出的结构

最顶部的称为当前栈帧,

栈帧结构:

一个栈帧包含:

局部变量表(存储在方法中声明的变量)

操作数栈(实际计算运行)

动态链接

void A(){

B();//B方法的地址

}

方法返回地址

本地方法栈(Native Method Stack)

与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。

用来运行本地方法的区域;

是线程私有;

空间大小可以调整;

可能会出现栈溢出;

Java 堆(Java Heap)

是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存.

基本作用特征

是存储空间,用来存储对象,是内存空间最大的一块儿区域,

在jvm启动时就被创建,大小可以调整(jvm调优)

本区域是存在垃圾回收的.是线程共享的区域

堆空间的分区:

年轻代(新生区/新生代)

伊甸园区(对象刚刚创建存储在此区域)

幸存者1区

幸存者2区

老年代(老年区)

为什么要分区

可以根据对象的存活的时间放在不同的区域,可以区别对待.

频繁回收年轻代,较少回收老年代.

创建对象,在堆内存中分布

1.新创建的对象,都存储在伊甸园区

2.当垃圾回收时,将伊甸园中垃圾对象直接销毁,将存活的对象,移动到幸存者1区,

3.之后创建的新对象还是存储在伊甸园区,再次垃圾回收到来时,将伊甸园中的存活对象移动到幸存者2区,同样将幸存者1区的存活对象移动到幸存者2区,每次保证一个幸存者区为空的,相互转换.

4.每次垃圾回收时,都会记录此对象经历的垃圾回收次数,当一个对象经历过15次回收,仍然存活,就会被移动到老年代垃圾回收次数,在对象头中有一个4bit的空间记录 最大值只能是15,

5.老年区回收次数较少,当内存空间不够用时,才会去回收老年代.

堆空间的配置比例

默认的新生代与老年代的比例: 1:2 可以通过 -XX:NewRatio=2 进行设置

如果项目中生命周期长的对象较多,就可以把老年代设置更大.

在新生代中,伊甸园和两个幸存者区比例: 8:1:1

可以通过-XX:SurvivorRatio=8 进行设置

对象垃圾回收的年龄 -XX:MaxTenuringThreshold=<N>

分代收集思想 Minor GC、Major GC、Full GC

对年轻代进行垃圾回收称为 Minor GC /yong GC 是频繁进行的回收

对老年代进行垃圾回收称为 Major GC / old gc 回收的次数较少

Full GC 整堆收集 尽量避免

System.gc();时 程序员几乎不用

老年区空间不足

方法区空间不足

堆空间的参数设置

官方文档

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

字符串常量池

在jdk7之后,将字符串常量池的位置从方法区转移到了堆空间中,

因为方法区的回收在整堆收集时发生,回收频率低,

堆空间回收频率高.

方法区(Methed Area)

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法区是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.

作用: 主要用来存储加载的类信息, 以及即时编译期编译后的信息, 以及运行时常量池

特点: 在jvm启动时创建,大小也是可以调整, 是线程共享,也会出现内存溢出.

方法区,堆,栈交互关系

方法区存储类信息(元信息)

堆中存储创建的对象

栈中存储对象引用

方法区大小设置

-XX:MetaspaceSize 设置方法区的大小

windows jdk默认的大小是21MB

也可以设置为-XX:MaxMetaspaceSize 的值是-1,级没有限制. 没有限制 就可以使用计算机内存

可以将初始值设置较大一点,减少了FULL GC发生

方法区的内部结构

类信息

以及即时编译期编译后的信息,

以及运行时常量池(指的就是类中各个元素的编号)

方法区的垃圾回收

在FULL GC时方法区发生垃圾回收.;

主要是回收类信息, 类信息回收条件比较苛刻,满足以下3点即可:

1.在堆中,该类及其子类的对象都不存在了;

2.该类的类加载器不存在了;

3.该类的Class对象不存在了;

也可以认为类一旦被加载就不会被卸载了.

特点总结

程序计数器,java栈,本地栈是线程私有的;

程序计数器不会出现内存溢出;

java栈,本地栈,堆,方法区可能会出现内存溢出;

java栈,本地栈,堆,方法区大小是可以调整的;

堆,方法区是线程共享的,是会出现垃圾回收的;

程序计数器(Program Counter Register)

JVM 中的程序计数寄存器(Program Counter Register)这里翻译为程序计数器更容易理解.

程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读取下一条指令.

它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.

在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致.

程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址.

它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需

要依赖这个计数器来完成.它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.

Java 虚拟机栈(Java Virtual Machine Stacks)

栈的基本概念

栈是运行时的单位,即栈解决程序的运行问题,即程序如何执行,或者说如何处理数据.

Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈.每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用.Java 虚拟机栈是线程私有的.主管 Java 程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回.

栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器.

JVM 直接对 java 栈的操作只有两个:调用方法入栈.执行结束后出栈. 对于栈来说不存在垃圾回收问题。

栈中会出现异常,当线 程请 求的 栈深 度大 于虚 拟机 所允 许的 深度时 , 会 出现StackOverflowError。

栈的运行原理

JVM 直接对 java 栈的操作只有两个,就是对栈帧的入栈和出栈,遵循先进后出/

后进先出的原则.

在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class).

执行引擎运行的所有字节码指令只针对当前栈帧进行操作.

如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧.

不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法).

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧.

Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出.

栈帧的内部结构

每个栈帧中存储着:

局部变量表(Local Variables)

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。

操作数栈(Operand Stack)(或表达式栈)

栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)

因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

本地方法栈(Native Method Stack)

Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用.

本地方法栈也是线程私有的.

允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的.

如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError.本地方法是用 C 语言写的.

它的具体做法是在 Native Method Stack 中登记 native 方法,在 ExecutionEngine 执行时加载本地方法库.

Java 堆内存

堆内存概述

一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域.

Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间.

堆内存的大小是可以调节.

例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)

一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率.

《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的.

所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.

《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上.

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.

堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.

堆内存区域划分

Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)

新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区

为什么分区(代)?

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

对象创建内存分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片.

1. new 的新对象先放到伊甸园区,此区大小有限制.

2. 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.

3. 然后将伊甸园区中的剩余对象移动到幸存者 0 区.

4. 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.

5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区.

6. 什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为 15-XX:MaxTenuringThreshold=<N>

在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111,即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。

7. 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理.

8. 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常.

Java.lang.OutOfMemoryError:Java heap space

例如:

public static void main(String[] args) {

List<Integer> list = new ArrayList();

while(true){

list.add(new Random().nextInt());

}

}

新生区与老年区配置比例

配置新生代与老年代在堆结构的占比(一般不会调)

1. 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3

2. 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5

3. 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优

在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 :1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8

新生区的对象默认生命周期超过 15 ,就会去养老区养老。

分代收集思想 Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集.

部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:

新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.

老年区收集(Major GC / Old GC):只是老年区的垃圾收集.

整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集.

整堆收集出现的情况:

System.gc();时

老年区空间不足

方法区空间不足

开发期间尽量避免整堆收集.

堆空间的参数设置

官网地址:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+PrintFlagsInitial

查看所有参数的默认初始值

-XX:+PrintFlagsFinal

查看所有参数的最终值(修改后的值)

-Xms:初始堆空间内存(默认为物理内存的 1/64)-Xmx:最大堆空间内存(默认为物理内存的 1/4)

-Xmn:设置新生代的大小(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

-XX:+PrintGCDetails 输出详细的 GC 处理日志

字符串常量池

字符串常量池为什么要调整位置?

JDK7 及以后的版本中将字符串常量池放到了堆空间中。因为方法区的回收效率很低,在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、方法区不足时才会触发。

这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

public static void main(String[] args) {

String temp = "world";

for (int i = 0; i < Integer.MAX_VALUE; i++) {

String str = temp + temp;

temp = str;

str.intern();//将字符串存储到字符串常量池中

}

}

方法区

方法区的基本理解

方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据、static final 常量、static 变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。

Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开.

所以,方法区看做是一块独立于 java 堆的内存空间.

方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.

方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展.

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误关闭 JVM 就会释放这个区域的内存.

方法区,栈,堆的交互关系

方法区大小设置

Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.

元数据区大小可以使用参数-XX:MetaspaceSize 和-XX:MaxMataspaceSize 指定,替代上述原有的两个参数.

默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB,

-XX:MaxMetaspaceSize 的值是-1,级没有限制.

这个-XX:MetaspaceSize 初始值是 21M 也称为高水位线 一旦触及就会触发 Full GC.

因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一个较高的值

方法区的垃圾回收

1. 有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。

《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。

2. 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。

下面也称作类卸载

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。

2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。

3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

本地方法接口

什么是本地方法

简单来讲,一个 Native Method 就是一个 java 调用非 java 代码的接口,一个Native Method 是这样一个 java 方法:该方法的底层实现由非 Java 语言实现,比如 C。这个特征并非 java 特有,很多其他的编程语言都有这一机制在定义一个 native method 时,并不提供实现体(有些像定义一个 Javainterface),因为其实现体是由非 java 语言在外面实现的。

关键字 native 可以与其他所有的 java 标识符连用,但是 abstract 除外。

为什么要使用 Native Method

Java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

1.与 java 环境外交互有时 java 应用需要与 java 外面的环境交互,这是本地方法存在的主要原因。 你可以想想 java 需要与一些底层系统,如某些硬件交换信息时的情况。本地方法正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐细节。

2.Sun 的解释器是用 C 实现的,这使得它能像一些普通的 C 一样与外部交互。jre大部分是用 java 实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用 Java 实现的,但是它实现调用的事该类里的本地方法 setPriority0()。

猜你喜欢

转载自blog.csdn.net/m0_56492261/article/details/129707332