三种JVM
| 公司 | 名字 |
|---|---|
| Sun公司 | HotSpot(我们用的) |
| BEA | JRockit |
| IBM | J9VM |
1、JVM的位置

2、JVM的体系结构

我们说的垃圾处理百分之99都在堆中,因为像栈、计数器他们是不会产生垃圾的
3、类加载器

public class Car {
public static void main(String[] args) {
//类是模板,对象是具体的
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println("car1:"+car1.hashCode());
System.out.println("car2:"+car2.hashCode());
System.out.println("car3:"+car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println("aClass1:"+aClass1.hashCode());
System.out.println("aClass2:"+aClass2.hashCode());
System.out.println("aClass3:"+aClass3.hashCode());
}
}
执行结果:

通过打印结果就更能体现类是模板,对象是具体的,就好比类是人类,而对象是具体的人
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.getClass().getClassLoader());
System.out.println(student.getClass().getClassLoader().getParent());
System.out.println(student.getClass().getClassLoader().getParent().getParent());
}
执行结果:

我们可以清楚的看到BootStrap(启动类加载器)是打印的null,这是因为他是由C语言和C++写的我们使用Java是调用不出来的。
4、双亲委派机制

1、类加载器收到类加载的请求
2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
4、重复3步骤
实例:
我们自己创建一个java.lang.Srting这个类
package java.lang;
/**
* @author acoffee
* @create 2021-11-13 10:24
*/
public class String {
//双亲委派机制
//APP→Exc→Boot(最终执行)
public String toString(){
return "Hello";
}
public static void main(String[] args) {
String str = new String();
str.toString();
}
}
执行结果:

它会报找不到main方法的错误,这是因为执行程序它会一层层向上找,直到BootStrap(启动类加载器),会先看这里面有没有这个类,如果没有就会向下到Ext(扩展类加载器)中去找,再找不到才会到App(系统类加载器)去找,这里面就是我们自己写的类。如果还是找不到,就会报我们熟知的ClassNotFound这个异常。

这种机制主要是为了安全考虑,防止核心的API被随意的篡改。
简述双亲委派机制
① 如果一个类加载器收到了类加载请求,它并不会自己去加载,而是把这个请求委托给父类的加载器去执行。
② 如果父类记载其还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
③ 如果父类加载器可以完成类加载的任务,就成功返回,倘若父类加载器可以完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
5、沙箱安全机制
Java安全模型的核心就是lava沙箱(sandbox),什么是沙箱?
沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。 沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示最新的安全模型(jdk 1.6)

组成沙箱的基本组件:
字节码校验器(bytecode verifier): 确保lava类文件遵循lava语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。java javax
类装载器(class loader) : 其中类装载器在3个方面对Java沙箱起作用
。它防止恶意代码去干涉善意的代码;
。它守护了被信任的类库边界;
。它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
1.从最内层VM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
存取控制器(access controller)︰ 存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
安全管理器(security manager)∶ 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包(security package) : java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
① 安全提供者。
② 消息摘要。
③ 数字签名。
④ 加密。
⑥ 鉴别
6、native
我们去看start的源码
public class Demo {
public static void main(String[] args) {
new Thread(()->{},"my Thread").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) {
}
}
}
//凡是带了native关键字的,说明java的作用范围达不到,去调用底层c语言的库!
private native void start0();
native :凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层c语言的库!会进入本地方法栈,调用本地方法本地接口JNI

JNI: Java Native lnterface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈,其他的就是Java栈
Native lnterface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是在Native Method Stack中登记native方法,在(Execution Engine )执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
Native Method Stack
它的具体做法是Native Method Stack中登记native方法,在(Execution Engine )执行引擎执行的时候加载Native Libraies。【本地库】
7、PC寄存器
程序计数器: Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向
像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
8、方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
static、final、Class、常量池
9、栈
栈是一种常见的数据结构
程序 = 数据结构+算法 :持续学习
程序 = 框架+业务逻辑 :吃饭
栈:先进后出、后进先出:桶
队列:先进先出(FIFO : First Input First Output )
喝多了吐就是栈,吃多了拉就是队列
栈帧
栈帧就相当于是一个站空间里面的一块空间,栈帧随着方法的调用而创建,随着方法结束而销毁,存储了方法的局部变量信息

上述程序在栈中执行流程

两个栈帧之间的关联,有点像上面的test2方法

栈是线程级的,每个线程都有自己的栈
而我们常见的例子就是main方法,最先执行,最后结束:对应的就是最先入栈,最后出站
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over
栈:八大数据类型+对象引用+实例的方法
10、堆
Heap,一个JVM只有一个堆内存

11、新生区、老年区

GC垃圾回收,主要是在伊甸园区和养老区
假设内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:java堆空间在JDK8以后,永久存储区改了个名字(元空间);

12、永久区
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收! 关闭VM虚拟就会释放这个区域的内存。
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM。
jdk1.6之前:永久代,常量池是在方法区;
jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中.
jdk1.8之后:无永久代,常量池在元空间
13、堆内存调优
介绍三个概念
1.maxMemory:这个方法返回的是java虚拟机(这个进程)能构从操纵系统那里挖到的最大的内存
2.totalMemory:程序运行的过程中,内存总是慢慢的从操纵系统那里挖的,基本上是用多少挖多少,直 挖到maxMemory()为止,所以totalMemory()是慢慢增大的
3.freeMemory:挖过来而又没有用上的内存,实际上就是 freeMemory(),所以freeMemory()的值一般情况下都是很小的(totalMemory一般比需要用得多一点,剩下的一点就是freeMemory)
没调之前
public class Demo {
public static void main(String[] args) {
long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
System.out.println((double) max/1024/1024+"M");
System.out.println((double)total/1024/1024+"M");
}
}
执行结果:

-Xms:设置初始化内存分配大小1/64
-Xmx:设置最大分配内存,默认1/4
-XX:+PrintGCDetails:打印GC垃圾回收信息
-XX:+HeapDumpOnOutOfMemoryError :oom Dump
我们可以通过-Xms1024m -Xmx1024m -XX:+PrintGCDetails对堆内存进行改变,步骤如下


解决OOM:
1、尝试扩大堆内存看结果
2、分析内存,看一下那个地方出现了问题(专业工具Jprofiler)
下载地址
下载过后也要在Idea中安装插件

匹配安装软件的位置

我们模拟一个OutOfMemoryError,我们就可以去看具体是哪一行报错:-XX:+HeapDumpOnOutOfMemoryError我们可以把内存改小一点在进行测试。
//-XX:+HeapDumpOnOutOfMemoryError
public class Demo3 {
byte[] array = new byte[1 * 1024 * 1024];
public static void main(String[] args) {
ArrayList<Demo3> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new Demo3());//问题所在
count += 1;
}
} catch (Exception e) {
System.out.println("count:" + count);
e.printStackTrace();
}
}
}


具体查看步骤如下

虽然这只是测试OutOfMemory的但是其他报错的测试同理。
14、GC
1、对象经历了15次 Minor GC 依旧存活 (默认值 -XX:MaxTenuringThreshold = 15)
2、Survivor区中 同龄对象大小超过Survivor区空间的50%,大于此年龄的对象会进入老年代 (动态对象年龄判定) (其实是这个年龄以及低于这个年龄的对象占据超过Survivor 50% 1+2+3大于50% 则大于等于3岁的对象会进入老年代)
此处可以参考:https://blog.csdn.net/u014493323/article/details/82921740
3、Minor GC后对象大小大于Survivor大小,会进入老年代 (空间担保机制)
4、大对象直接进入老年代 (-XX:PretenureSizeThreshold=1M 只对Serial和ParNew两款收集器有效)
GC的两种类型: 轻GC和重GC
Minor GC ,Full GC 触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
15、GC算法
①引用计数法(Java中没有采用)
java中没有采用这种方法,因为没有解决循环引用的问题

②复制算法

③标记清除法
没被标记就被清除

优点:不需要额外的空间
缺点:两次扫描,严重浪费时间,会产生内存碎片
④标记压缩法

标记的过程是找出当前还存活对对象,并进行标记;清除则是遍历整个老年区,找已经标记的对象并进行清除,然后把存活的对象移动到整个内存区的一端,使得另一端是一块连续的空间,方便进行内存分配和复制。
这个可以优化,可以先标记清除几次,然后在压缩,但是也只是优化,本质没改变。
GC算法的总结
| 性能名称 | 效率 |
|---|---|
| 内存效率 | 复制算法>标记清除算法>标记压缩算法(时间复杂度) |
| 内存整齐度 | 复制算法=标记压缩算法>标记清除算法 |
| 内存利用率 | 标记压缩算法=标记清除算法>复制算法 |
思考一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法 → GC∶分代收集算法
年轻代:
存活率低·复制算法!
老年代:
区域大:存活率
标记清除(内存碎片不是太多)+标记压缩混合实现
JVM常见题目:
JVM的内存模型和分区,详细到每个区放什么?

方法区
用于储存虚拟机加载的类,常量,静态变量等数据。
堆
存放对象的实例,所有对象和数组都在堆上分配,是jvm所管理的内存中最大的一块区域。
栈
虚拟机栈,java执行方法的内存模型。存储局部变量表,操作数栈,动态链接,方法出口等信息。
本地方法栈
和虚拟机栈类似,不过本地方法栈是native关键字修饰的(底层为C),虚拟机栈是虚拟机执行的java方法。(我们上面研究的hotspot虚拟机是将java栈和本地方法栈合在一起的)
程序计数器
jvm中最小的一块区域,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
堆里面的分区有哪些?Eden、form、to、老年区,说说他们的特点!
Eden区: Eden是进行内存分配的地方,是一块连续的空闲内存区域,在里面进行内存分配速度非常快,因为不需要进行可用内存块的查找。新对象总是在Eden区中生成,只有经受住了一定的考验后才能顺利的进入到Survivor区中。
Form、To: 两个Survivor区是交替使用,循环往复,在下一次垃圾回收时,之前被清除的存活区又用来放置存活下来的对象了。一般来说,年轻代区域较小,而且大部分对象是需要进行清除的,采用了"复制算法"进行垃圾回收。
在新生代中经历了N次回收后仍然没有被清除的对象,就会被放到老年代中,都是生命周期较长的对象。对于老年代和永久代,采用标记-整理的算法(标记压缩法)。
Minor GC与FullGC分别什么时候发生?
如果Eden空间满了,会触发Minor GC。Minor GC后仍然存活的对象会被复制到S0中去。这样Eden就被清空可以分配给新的对象。又触发了一次Minor GC,S0和Eden中存活的对象被复制到S1中,并且S0和Eden被清空。在同一时刻只有Eden和一个Survivor区同时被操作。当每次对象从Eden复制到Survivor或者从Survivor中的一个复制到另外一个,有一个计数器会自动增加值。默认情况下,如果复制发生超过16次,JVM机会停止复制并把他们移动到老年代中去。
如果一个对象不能在Eden中创建,它会直接被创建在老年代中。如果老年代的空间被占满会触发老年代的GC,也被称为Full GC。Full GC是一个压缩处理过程,所以很慢。

335

被折叠的 条评论
为什么被折叠?



