写在前面
本次笔记整理学习来源–>B站传送门–>【狂神说Java】JVM快速入门篇
感谢狂神?
这位up的短视频知识点教学也很棒–>B站up主较真的度 [程序员5分钟] 白话JVM内存结构,死也忘不了; [程序员5分钟] 带你认识java中jvm虚拟机栈; [程序员5分钟]理解了1+1的过程,你才算真正理解了虚拟机栈
感谢较真的度 ?
知乎博主写的这篇关于类加载机制的博文也不错,学习时可以参考参考–> 老大难的 Java ClassLoader 再不理解就老了
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
栈区: 每个线程包含一个栈区,栈中只保存方法中(不包括对象的成员变量)的基础数据类型和自定义对象的引用(不是对象),对象都存放在堆区中 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
堆区: 存储的全部是对象实例,每个对象都包含一个与之对应的class的信息(class信息存放在方法区)。 jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身,几乎所有的对象实例和数组都在堆中分配。
方法区:又叫静态区,跟堆一样,被所有的线程共享。它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
1.JVM探究
抛出几道面试题—> 对于Jvm的了解;Java8的虚拟机有哪些变化;什么是OOM,什么是栈溢出? JVM常用的调优参数? 内存快照如何抓取,如何分析Dump文件?对于类加载器的认识?
需要学习的知识点–> 1.JVM的位置 2.JVM的体系结构 3.类加载器 4.双亲委派机制 5.沙箱安全机制 6.Native本地方法 7.PC寄存器 8.方法区 9.栈 10.三种JVM
可以命令行输入java -version 看看自己用的是哪种虚拟机
11.堆 12.新生区,老年区 13.永久代 14.堆内存的调优 15.GC垃圾回收机制,常用的算法 16.JVM
实践较少,主要以理论为主
2.JVM的位置
3.JVM的体系结构
JVM的调优是针对于方法区和堆 ;而大部分的调优针对于堆
4.类加载器;
它的作用是–>加载Class文件 比如说new构造方法创建了一个对象的过程;引用放在栈中,而这个具体的对象内容放入堆中;
就看看一个Car汽车类的过程–>
进入JVM后,先进入类加载器ClassLoader,—>然后初始化 得到Car类的 Class对象; (就是反射机制的那个Class类对象,全局就一份;所以说类是抽象的模板,而对象是具体的表示);
package demo01;
public class Car {
private String name;
public static void main(String[] args) {
Car car0 = new Car();
Car car1 = new Car();
System.out.println(car0.hashCode());
System.out.println(car1.hashCode());
System.out.println("---------------");
Class c0 =car0.getClass();
Class c1 =car1.getClass();
System.out.println(c0.hashCode());
System.out.println(c1.hashCode());
}
}
不同的对象,但是它的Class类对象是一致的.
OK,具体来看看类加载器
- 虚拟机自带的加载器;
- 启动类加载器(根加载器);
- 扩展类加载器;
- 应用程序加载器(系统类加载器)
还是在刚才的Car类中试试
package demo01;
public class Car {
private String name;
public static void main(String[] args) {
Car car = new Car();
Class<? extends Car> aClass = car.getClass();
ClassLoader classLoader = aClass.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
}
应用程序加载器获取到的位于rt.jar 包下的java.lang包下的ClassLoader 类
看看jdk的安装目录;扩展程序位于jre的lib 目录下的ext 目录
而它的上一块位于lib 目录的rt.jar 包
5.关于双亲委派机制
比如说手动创建java.lang 包;然后创建String 类;
然后定义toString()方法,写个main方法去调用执行;
package java.lang;
public class String {
@Override
public String toString() {
return "I am toString";
}
public static void main(String[] args) {
String string = new String();
string.toString();
}
}
运行出现错误提示;
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
也就是说,他并没有走定义的main方法;
说说双亲委派机制;是一种安全机制; 运行一个类之前,它会向上找; 比如说,在应用程序加载器(AppClassLoader )位置;; 它会向上找到扩展类加载器(ExtensionClassLoader ),然后继续向上找启动类加载器(BootstrapClassLoader );
上面的没有找到关于这个类的内容,又会向下走,回到应用程序加载器;
然后再去自定义的demo01 包下新建一个Student类试试;(这次没有和Java定义的类重名 );还是定义toString()方法;
package demo01;
public class Student {
@Override
public String toString() {
return "Student";
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.toString());
}
}
运行之,
Student
Process finished with exit code 0
大致过程
- 1 .类加载器收到了类加载的请求
- 2 .此请求向上委托–>父类…直到根加载器
- 3 .上级的加载器先看自己能不能处理(加载这个类),可以的话自己就处理了,不能处理的就抛异常,通知子类加载器执行;…这一步是可以重复的,直到最下面的应用程序加载器.
看看java线程类的start 方法,线程级别的Java无法处理,就去调用本地方法start0() ,(C++写的 ),这就和操作系统有关系了.
6.沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox) 沙箱是一个限制程序运行的环境。 沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。 沙箱主要限制系统资源访问,(系统资源==> CPU、内存、文件系统、网络。) 不同级别的沙箱对这些资源访问的限制也可以不一样。所有的Java程序运行都可以指定沙箱,可以定制安全策略。 在Java中将执行程序分成本地代码和远程代码 两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型.
但严格的安全机制也给程序的功能扩展带来障碍, 比如当用户希望远程代码访问本地系统的文件时,就无法实现。 因此在后续的Java1.1版本中,针对安全机制做了改进增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型
在Java1.2版本中,再次改进了安全机制,增加了代码签名 。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。 JDK1.2安全模型
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机 会把所有代码加载到不同的系统域和应用域 ,系统域部分专门负责与关键资源进行交互 ,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示最新的安全模型(jdk 1.6)
在linux系统中,root具有最高权限;而 来宾/普通用户 是guest
比如说,写了一个类,方法1调用方法2,方法2去调用方法1;
package demo01;
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo.method1();
}
public void method1(){
method2();
}
public void method2(){
method1();
}
}
出现栈溢出的错误;
组成沙箱的基本组件 ●字节码校验器 (bytecode verifier) :确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类 。 ●类装载器(class loader) :其中类装载器在3个方面 对Java沙箱起作用
- 1
防止恶意代码去干涉善意的代码 ; - 2
守护了被信任的类库边界 ; - 3
将代码归入保护域,确定了代码可以进行哪些操作 。
虚拟机为不同的类加载器载入的类提供不同的命名空间 ,命名空间由一系列唯一的名称组成, 每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的 ,它们互相之间甚至不可见。 ?类装载器采用的机制是双亲委派模式 。 ?1.从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载 从而无法使用; ?2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。 ●存取控制器 (access controller) :存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。 ●安全管理器 (security manager) : 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。 ●安全软件包 (security package) : java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括: 安全提供者; 消息摘要;数字签名;加密;鉴别.
7.Native本地方法
带了native 关键字的,Java作用范围达不到,就要去底层调用C语言的库; 进入本地方法栈;调用本地方法接口(JNI );[扩展Java的使用,融合不同的编程语言];
本地方法栈是:在内存区域开辟的标记区域(Native Method Stack) ,标记native方法;最终执行时,通过JNI 加载本地方法库中的方法
还是看看线程类的启动方法start()
Native Method Stack 具体做法是Native Method Stack中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。[本地库]
Native Interface本地接口 (JNI) ?本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序, Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。 ?目前该方法使用的越来越少,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。 ?因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等
8.PC寄存器(Program Counter Register)
每个线程都有一个程序计数器,是线程私有的 ,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计
9.方法区(Method Area)
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,所有定义的方法的信息都保存在该区域,此区域属于共享区间
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
10.栈(Stack)
栈是一种数据结构,(后进先出)
而队列是先进先出的数据结构
为什么main方法是最先被执行的,但是最后结束
main方法最先被压入栈,最后被弹出
栈的别名:栈内存 ; 主要管理程序的运行;生命周期与线程同步,线程结束,栈内存即被释放;它不存在垃圾回收相关的问题.
栈里面存放的主要就是 8大基本数据类型 以及 对象的引用 以及实例方法.
关于栈帧 ,是一片一片的…;压入一个方法入栈,就有栈帧. 栈 2;先进来被调用;
正在执行的方法,是位于栈顶位置的;
栈,堆,方法区的关系
11.三种JVM
12.堆(Heap)
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
比如说,运行一个类.可以去设置它的内存.
类加载器读取了类文件后,将类, 方法,常量,变量,保存所有引用类型的真实对象;存入堆中.
堆还分化了三块区域;
新生区(Yong); 老年区(Old);永久区(Perm)
新生区会经历轻量级的垃圾回收;幸存的会进入幸存区;在这幸存区之间是动态传递的;from ; to
当整个新生区都满了,即(伊甸园区域和幸存区域都满了),有重量级的垃圾回收 ;幸存下来的就进入老年区; 要是老年区满了,进行重量级回收 ,幸存的进入永久代区域.
假设内存满了;就会出现OOM错误;堆内存溢出错误.
案例,死循环进行追加字符串;
package demo01;
public class Demo3 {
public static void main(String[] args) {
String s="ADASDAasdadadaadadadadadadadad";
while (true){
s+="sda"+s;
}
}
}
在JDK8之后,永久存储区更改了名字–>元空间.
13.新生区,老年代,永久区
新生区是一个类诞生,成长,甚至于死亡的区域; 伊甸园区域:对象诞生new的区域; 幸存区域(由两部分);
实际的真相:内存中99%的都是临时对象.
关于永久区;(常驻于内存之中),和对象没关系;用来存放JDK自身携带的Class对象;Interface元数据;存储JAVA运行时的一些环境活着类信息,该区域也不存在垃圾回收; 那么它的内存释放时间—>关闭虚拟机VM就会释放它的内存空间.
jdk1.6之前就存在永久代 ;常量池位于方法区
jdk1.7时还是存在着永久代 ;但是逐渐退化; 常量池会放入堆中 .
jdk1.8后出现元空间 ,永久代没了; 这时候 常量池存入了元空间中 ;
当一个启动的类;加载了大量的第三方jar包;Tomcat 部署过多的应用,大量地动态生成反射类;不断地被加载;直到内存满了,就会产生OOM错误.
案例
package demo;
public class Demo {
public static void main(String[] args) {
Long l1 = Runtime.getRuntime().maxMemory();
System.out.println("计划最大内存的字节=>"+l1+"字节"+"---"+(l1/(double)1024/1024)+"MB");
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("初始化总内存的字节=>"+totalMemory+"字节"+"---"+(totalMemory/(double)1024/1024)+"MB");
}
}
默认: 分配的总内存是电脑内存的1/4 ; 初始化的内存 是电脑内存的1/64;
调整-Xms527m -Xmx527m -XX:+PrintGCDetails
然后再次执行demo类;
新生区+老年区–>157696K+360448K=518144K; 然后除以1024 为 506M.
所以说这个永久存储的区域,在逻辑上是存在的;但是在物理上不存在.
对刚才的OOM案例试试,调整内存分配;
package demo01;
public class Demo3 {
public static void main(String[] args) {
String s="ADASDAasdadadaadadadadadadadad";
while (true){
s+="sda"+s;
}
}
}
-Xms5m -Xmx5m -XX:+PrintGCDetails
执行之; 垃圾回收过程.
14.使用JPROFilter工具分析OOM原因
先去idea中安装插件
安装之后需要重启IDEA
然后,这只是一个插件,还需要安装实际的客户端应用 官网下载位置–>下载 JPROFILER
具体的安装–>可查看这位博主的详细说明–>安装JPROFILER
本次学习就下载9.2版本的了
注意安装的位置路径不要有空格/中文
OK,IDEA重启后,会出现这里的提示
打开IDEA的设置Settings,找到工具Tools ,这里的Jprofilter还需要安装扩展应用,注意路径是刚才安装的客户端的工具的bin目录下的exe文件;
这样的话,出现OOM问题时,就会提示找到原因;
案例;
package demo;
import java.util.ArrayList;
public class Test {
byte[] bytes = new byte[1*1024*1024];
public static void main(String[] args) {
ArrayList<Test> arrayList = new ArrayList<>();
int num = 0;
try {
while (true){
arrayList.add(new Test());
num = num + 1;
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("计数num->"+num);
}
}
}
把它的jvm调优为1m,总大小8m; -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
这时运行的话,会生成一个文件
选择在文件中打开这个类
这里肯定没有生成的文件;顺着路径一直向上找;这不就出现那个java_pid1256.hprof 文件了吗
刚才安装过客户端了;那么直接双击打开这个文件;
直接去看大类的对象;这里ArrayList集合直接占用了89%的
提示线程的那个位置错误
15.关于GC回收算法
GC垃圾回收时,并不是直接对(新生区,老年区,幸存区)进行垃圾回收;
大部分都是回收新生代
GC分为轻量级GC 和重量级GC ;
需要重点学习的部分 (1)JVM的内存模型和分区; (2)堆里面的的分区;Eden(伊甸园区;);from,to(幸存区);老年区;他们的特点; (3)GC的常用算法;–>标记清除法;标记整理;复制算法;引用计数器法; (4)轻量级GC和重量级的GC分别在什么时候回收;
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度) 内存整齐度:复制算法=标记压缩算法>标记清除算法 内存利用率:标记压缩算法=标记清除算法>复制算法
新生区:存活率低,主要用复制算法;
老年区:存活率高; 主要用标记清除+标记压缩
那么先来说说引用计数法; 首先要给每个对象分配一个引用计数器;计数器本身也会有消耗;
复制算法
那么,首先要注意如何区分幸存区的From 和To区的问题,他们两个是动态地变化地;哪个是空的,那个就是To区域 ;
每次GC都会将Eden(伊甸园区域 )活的对象移到幸存区中:一旦Eden区被GC后,就会变为空的;
进入幸存区域的对象,会把From区域的复制一份送到To区域,然后身份互换;空的变为To区域;动态地变化;
然后,默认的是,当一个对象经过15次的GC以后,还在幸存区活着的话,就把它送到老年区;
好处:没有内存的碎片 不利之处:浪费内存空间;多余一半To空间; 假如说对象100%存活,全部复制的话是比较危险的.所以复制算法主要用户对象存活率较低的场景
有一则小故事 —> 我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“To”区,自从去了Survivor区,我就开始漂泊了,因为Survivor的两个区总是交换名字,所以我总是搬家,搬到To区域居住,一会又搬到from区域;搬来搬去,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
几张原理图
标记清除算法
会对这些对象进行扫描;进行标记;没有标记的就清除掉;
缺点:进行两次扫描;会浪费时间;产生内存碎片; 好处:不浪费内存空间
标记压缩清除
防止内存碎片化; 即进行了3次扫描
结束
JMM
- JMM: Uava Memory Model的缩写)
- 作用:缓存一致性协议, 用于定义数据读写的规则;
- JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
|