❤️ 爆肝二十万字《Java从零到精通教程》,贴心保姆教你从零变大佬 ❤️(建议收藏),学不会找我!
Posted Java程序鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️ 爆肝二十万字《Java从零到精通教程》,贴心保姆教你从零变大佬 ❤️(建议收藏),学不会找我!相关的知识,希望对你有一定的参考价值。
💂 个人主页: Java程序鱼
🤟 整个Java 体系的面试题我都会分享,大家可以持续关注
💬 如果文章对你有帮助,欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
💅 有任何问题欢迎私信,看到会及时回复
👤 微信号:hzy1014211086,想加入技术交流群的可以加我好友,群里会分享学习资料
当年考研我给自己定了一个目标北邮,我在北邮外租了一个房子,全身心的投入考研,最终因为各种原因没有考上,然后我花了一个月时间把所有Java体系知识点全部复习总结了一遍,最终拿到了大厂offer,我把这些笔记全部整理出来分享给大家,每篇文章都是几万字,精心打磨,原创不易,希望大家多多支持(点赞、关注、收藏),预祝大家在金九银十都能拿到自己❤️ 心仪的offer❤️。
序号 | 内容 | 链接地址 |
---|---|---|
1 | Java基础知识面试题 | https://blog.csdn.net/qq_35620342/article/details/119636436 |
2 | Java集合容器面试题 | https://blog.csdn.net/qq_35620342/article/details/119947254 |
3 | Java并发编程面试题 | https://blog.csdn.net/qq_35620342/article/details/119977224 |
4 | Java异常面试题 | https://blog.csdn.net/qq_35620342/article/details/119977051 |
5 | JVM面试题 | https://blog.csdn.net/qq_35620342/article/details/119948989 |
6 | Java Web面试题 | https://blog.csdn.net/qq_35620342/article/details/119642114 |
7 | Spring面试题 | https://blog.csdn.net/qq_35620342/article/details/119956512 |
8 | Spring MVC面试题 | https://blog.csdn.net/qq_35620342/article/details/119965560 |
9 | Spring Boot面试题 | 待分享 |
10 | MyBatis面试题 | https://blog.csdn.net/qq_35620342/article/details/119956541 |
11 | Spring Cloud面试题 | 待分享 |
12 | Redis面试题 | https://blog.csdn.net/qq_35620342/article/details/119575020 |
13 | mysql数据库面试题 | https://blog.csdn.net/qq_35620342/article/details/119930887 |
14 | RabbitMQ面试题 | 待分享 |
15 | Dubbo面试题 | 待分享 |
16 | Linux面试题 | 待分享 |
17 | Tomcat面试题 | 待分享 |
18 | ZooKeeper面试题 | 待分享 |
19 | Netty面试题 | 待分享 |
20 | 数据结构与算法面试题 | 待分享 |
作者金华,上海张江信息技术专修学院副院长,上海师范大学兼职教授,软件与信息技术讲师,长期从事软件与信息技术技能培训与职业规划工作,本书将相关知识的系统整合,符合现在Java的主流应用,拒绝全面不实用;本书知识点主要围绕技术升级和面试技巧展开,让你在升级专业知识的同时更能顺利通过面试。
京东自营购买链接:
《Java核心技术及面试指南》- 京东图书
当当自营购买链接:
《Java核心技术及面试指南》- 当当图书
截止到9月24日14:00,留言获赞最高的两位同学,将获得《Java核心技术及面试指南》图书一本
前言
目前内存的动态分配与内存回收技术已经相当成熟,一切看起来都进入了"自动化"时代,那么为什么我们还要去了解GC和内存分配呢?
当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些"自动化"的技术进行必要的监控和调节。 要想实现性能调优,得具备相关工具监控程序性能,有了监控信息,才能进行调优。
一、虚拟机类加载机制
Class文件中描述的各种信息,最终都是要加载到虚拟机中之后才能运行和使用。
JVM把Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这个过程被称作虚拟机的类加载机制。与那些在编译时需要进行连接的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。(例如Java多态、动态代理)
Java虚拟机中的类加载(JVM把class文件加载到内存),按先后顺序需要经过加载、链接、初始化三个步骤。其中,链接过程中同样需要验证;而内存中的类没有经过初始化,同样不能使用。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
1.虚拟机类加载过程
加载阶段
什么情况下需要开始类加载过程的加载阶段?这个「 Java虚拟机规范」中没有强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,「 Java虚拟机规范」则是严格规定了有且只有六种情况必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)
-
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
(1)使用new关键字实例化对象的时候。(new)
(2)读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
(3)调用一个类型的静态方法的时候。(invokestatic) -
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
-
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,被标明为启动类的类(用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。
-
当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
-
当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
Java程序对类的使用方式分为:主动使用和被动使用。
除了上述6种方式,其他使用Java类的方式都被看作为是对类的被动使用,都不会导致类的初始化。
加载、验证、准备、初始化、卸载这5个阶段顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
加载阶段,Java虚拟机需要完成三件事:
- 通过类的全限定名(例如:org.apache.commons.lang3.StringUtils)来获取定义此类的二进制字节流(Class文件字节流)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
连接阶段
(1)验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
主要包括四种验证:文件格式验证、元数据验证、字节码验证、符合引用验证。
(2)准备
准备阶段是正式为类变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值(即0)的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。
public static int value = 123,那么value在准备阶段过后的初始值为0,而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放在类构造器
<clinit>()
方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。
public static final int value = 123456,final在编译的时候就会分配了,准备阶段会显示初始化,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123456。
(3)解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化阶段
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器<clinit>()
方法的过程。<clinit>()
并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及<clinit>()
方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作
(1)<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如下所示。
public class StaticTest {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.println(i); // 这句编译器会报错“非法向前引用”
}
static int i = 1;
}
public class StaticTest {
static {
i = 2; // 给变量复制可以正常编译通过
}
static int i = 1;
public static void main(String[] args) {
System.out.println(StaticTest.i);
}
}
答案:1,为什么可以呢?在linking阶段的准备阶段,已经把i加载到内存,并且赋初始值(零值)了。
如果没有静态变量赋值动作和静态语句块,就不会生成
(2)<clinit>()
方法与类的构造函数(即在虚拟机视角中的实例构造器()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()
方法执行前,父类的<clinit>()
方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()
方法的类型肯定是java.lang.Object。
由于父类的<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下所示,字段B的值将会是2而不是0。
public class TestDemo {
static class Parent{
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
(3)Java虚拟机必须保证一个类的<clinit>()
方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()
方法。如果在一个类的<clinit>()
方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。代下所示:
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName()+"开始");
DeadThread deadThread = new DeadThread();
System.out.println(Thread.currentThread().getName()+"结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName()+"初始化当前类");
while(true){
}
}
}
}
结果:
线程2开始
线程1开始
线程2初始化当前类
线程2在初始化当前类时死循环了,会造成后面所有的线程全部阻塞。
2.类加载器(ClassLoader)
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)
目前类加载器在类层次划分、OSGi、程序热部署、代码加密等领域大放异彩。
类加载器:把我们硬盘上编译好的.Class文件,通过类装载器将字节码文件加载到内存中,生成一个Class对象。
这里的四者是包含关系,不是上下层,也不是子父类的继承关系
ClassLoader:是一个抽象类,我们可以继承它实现自定义加载器。
启动类加载器(Bootstrap)
启动类加载器(Bootstrap):主要加载jre/lib/rt.jar(Java核心API ),getClassLoader为null。(C++实现的)
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
Object object = new Object();
object.getClass().getClassLoader();//null
String string = new String();
string.getClass().getClassLoader();//null
并不继承自java.lang.ClassLoader,没有父加载器
扩展类加载器(Extension)
扩展类加载器(Extension):通过反射创建Class实例,而这个类在jre/lib/ext的jar包中,这时加载器就是Extension ClassLoader,加载jre/lib/ext里的类。
getClassLoader:sun.misc.Launcher$ExtensionLoader@HashCode
直接继承自URLClassLoader,间接继承ClassLoader
应用程序类加载器(APP)
应用程序类加载器(APP):它负责加载用户类路径(ClassPath)上所有的类库。
getClassLoader:sun.misc.Launcher$AppLoader@HashCode
直接继承自URLClassLoader,间接继承ClassLoader
对于用户自定义的类,如果没有自定义过自己的类加载器,默认使用应用程序类加载器加载
可以通过ClassLoader.getSystemClassLoader();获取应用程序类加载器
自定义类加载器
自定义类加载器的父类是应用程序类加载器
sun.misc.Launcher:它是一个Java虚拟机的入口应用
获取父类加载器:classLoader.getParent()
扩展类加载器和应用程序类加载器都继承了ClassLoader.
获取ClassLoader方法:
方式一:获取当前类的ClassLoader
clazz.getClassLoader();
方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
3.用户自定义加载器
为什么要自定义类加载器?
- 隔离加载类(通过类加载器实现类的隔离、重载等功能)
- 修改类加载的方式
- 扩展加载源(增加除了磁盘位置之外的Class文件来源)
- 防止源码泄露
用户自定义类加载器实现步骤:
(1)开发人员通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
(2)在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载,但是在JDK1.2之后,已不再建议用户去覆盖loadClass()方法,而是建议把自定义类的加载逻辑写在findClass()中。
(3)在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()发放及其获取字节码流的方式,使自定义类加载器编写更加简洁。
举例:
防止源码泄露实现步骤:
(1)继承ClassLoader,重写findClass()
(2)在findClass()中,传入的name是加密的,先写解密逻辑,然后在获取字节码二进制流
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
(3)调用defineClass()把二进制流字节转化为Class
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。
4.双亲委派机制
双亲委派模型的工作原理:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
为什么根类加载器为NULL?
根类加载器并不是Java实现的,而且由于程序通常须访问根加载器,因此访问扩展类加载器的父类加载器时返回null。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
自定义一个包java.lang,自定义Java核心类库没有的类,运行时报错。
java.lang.SecurityException:prohibited package name:java.lang
举例:自定义一个包java.lang,自定义一个类String,然后里面声明main方法,运行时报错。(沙箱机制)
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println(1);
}
}
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
加载String时,使用的是BootstrapClassLoader,加载的是Java核心类库的String,并非我们自定义的String,核心类库的String类,没有main方法,因此报错。
沙箱机制:是由基于双亲委派机制上采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.(安全特性,防止恶意代码对Java的破坏)
双亲委派优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
二、Java运行时数据区
JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
JVM运行时数据区:Java代码运行的时候每个数据区的区块存的是什么用来干什么怎么存的
需提前理解的概念:
一个类可以看成三类,数据(int i = 0等…)、指令(int c = i…代码)、控制(if else switch…)
Java 虚拟机运行时数据区:Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和消耗的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和消耗。
1.Program Counter Register(程序计数器)
Program Counter Register:程序计数器
作用:程序计数器用来存储指向下一条指令的地址,也就是将要执行的指令代码。由执行引擎读取下一条指令。
它是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。
比如我A()方法调用了B()方法,执行完B之后怎么恢复,这时就需要程序计数器,字节码解释器就是通过改变计数器的值来选取下一条执行的字节码指令。
如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是native方法,这个计数器的值为undefined。
2.Java虚拟机栈
栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
它描述的是Java方法执行的线程内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在线程上执行的每一个方法都对应着一个栈桢
假如执行A方法创建一个A栈帧,A栈帧入栈,A方法调用B方法,需要为B方法创建一个B栈帧,然后入栈,B方法执行到方法出口之后,B栈帧出栈,然后A方法执行到方法出口之后,A栈帧出栈,这就是方法执行过程。
栈帧伴随着方法从创建到执行完成。
Java虚拟机规范允许Java栈的大小是动态或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将抛出一个StackOverflowError异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
栈没有GC
设置栈内存大小
我们可以使用参数-Xss(stack size)选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
默认值:
Linux/x64(64-bit):1024KB
macOs(64-bit):1024KB
JVM直接对Java栈的操作只有两个,就是对栈桢的压栈和出栈,遵循先进后出原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈桢,即只有当前正在执行的方法的栈桢(栈顶栈桢)是有效的,这个栈桢被称为当前栈桢,与当前栈桢对应的方法就是当前方法,定义这个方法的类就是当前类
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常。不管使用哪种方式,都会导致栈桢被弹出.
栈桢内部结构:
局部变量表(Local Variables)
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
局部变量所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
其中64位长度的long和double类型的数据会占用2个Slot,其余的数据类型只占用1个Slot,局部变量所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
注意:虚拟机栈的大小会变,因为不停的创建和销毁栈帧,还有别的操作都会改变虚拟机栈大小。
局部变量表最基本的存储单元是Slot(变量槽),32位一个变量槽
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中的指定局部变量值。(如果占两个槽,使用起始索引)
补充:0槽位放this,所以从1开始
注意:局部变量必须显式赋值
1)boolean——1byte 0为false 非0为true
2)byte——1 byte
3)short——2 bytes
4)int——4 bytes
5)long——8 bytes
6)float——4 bytes
7)double——8 bytes
8)char——2 bytes
操作数栈(Operand Stack)
在方法执行过程中,根据字节码指令,往栈中写入数据(ipush)或提取数据(iload),即入栈/出栈。
注意:这里栈不是指栈桢,指的是操作数栈
作用:用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
某些字节码指令值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈。
动态连接(Dynamic Linking)
方法返回地址(Return Address)
存储调用该方法的程序计数器的值
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的程序计数器的值作为返回地址。而通过异常退出的,返回地址是要通过异常表来确定,栈桢中一般不会保存这部分信息。
一些附加信息
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(死循环递归);如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
递归死循环调用,会一直创建栈帧,从而导致栈内存溢出,假如我们不限制栈深度,无法申请到足够的内存就会抛出内存溢出
方法中定义局部变量是否线程安全?
- 内部定义内部消亡,是线程安全的。
- 内部产生,但是没有在内部消亡,返回到方法外,这是线程不安全的。(逃逸)
3.本地方法栈(线程私有)
本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务,在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(Sun公司的HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常
4.Java堆
在虚拟机启动时创建,其空间大小也就确定了,是JVM管理的最大一块内存空间(堆内存的大小可调节)。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
注意:这里不是所有的对象实例都分配在堆内存。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap),从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,再细致一点的有Eden空间(伊甸园)、From Survivor(幸存者0区)空间、To Survivor(幸存者1区)空间等,从内存分配角度来看,线程共享的Java堆中可能划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),不过无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都仍然是对象实例,进一步划分目的是为了更好地回收内存,或者更快地分配内存。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,就像我们的磁盘空间一样,在实现时,既可以实现成固定大小的,也可以是可扩展的,不过目前主流的虚拟机都是按照可扩展的来实现的,通过-Xmx和-Xms控制,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常(OutOfMemoryError: Java Heap space)。
-XX:SurvivorRatio,设置新生代中Eden和S0/S1空间的比例,默认-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1,假设设置成-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1
总结:SurvivorRatio值就是设置Eden区的比例占多少,S0/S1相同
-XX:NewRatio,配置年轻代与老年代堆结构的占比,默认-XX:NewRatio=2新生代占1,老年代占2,年轻代占整个堆的1/3,假如-XX:NewRatio=4新生代占1,老年代占4,年轻代占整个堆的1/5
总结:NewRatio值就是设置老年代的占比,剩下的1给新生代
(1)new的对象先放伊甸园区。此区有大小限制
(2)当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃
圾回收(Minor GC), 将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
(3)然后将伊甸园中的剩余对象移动到幸存者0区
(4)如果再次触发垃圾回收,【本次存活对象】 和 【上次幸存下来的放到幸存者0区的且本次没有被回收的对象】,都会被放到幸存者1区。
(5)如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区(form/to)
(6)啥时候能去养老区呢?可以设置次数。默认是15次。
补充:可以设置参数: -XX:MaxTenuringThreshold=进行设置。
针对幸存者 S0,S1区总结:复制之后有交换,谁空谁是 to
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集
注意:Eden 区满时,会触发 Minor GC,此时会回收 Eden 区和幸存者区,但是幸存者区满了不会触发Minor GC,那怎么办?
当Survivor空间不足以容纳一次 Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代) 进行分配担保(Handle Promotion)。 (Serial、ParNew等新生代收集器均采用这种策略来设计新生代的内存布局)(来源JVM深入理解虚拟机)
内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了,内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。这对虚拟机来说就是安全的。
如果Survivor 区中相同年龄的存活对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
理由:相同年龄对象占Survivor空间的一半,每次复制算法都要从form到to,非常耗时
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
为什么有TLAB(Thread Local Allocation Buffer)?
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
什么是TLAB(Thread Local Allocation Buffer)?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
- 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
①尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
②在程序中,开发人员可以通过选项“-Xx :UseTLAB” 设置是否开启TLAB空间。
③默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通
过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
④一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操
作的原子性,从而直接在Eden空间中分配内存。
一个JVM实例只存在一个堆内存, 堆内存的大小是可以调节的。 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑上分为三部分:新生代+老年代+元数据(JDK8)
新生代包含:伊甸园区、幸存0区、幸存1区
堆:
优点:运行时的数据区,可以动态的分配内存大小,生存期也不必事先告诉编译器,因为它是运行时动态分配内存空间,垃圾收集器会自动收走不再使用的数据
缺点:运行时动态时分配内存空间,因此存取速度慢些
栈:(线程私有)
优点:存取速度比堆快,仅次于计算机里的寄存器,栈的数据可以共享。
缺点:大小和生存期是确定的,缺乏灵活性。
对象的引用存放在栈中,对象本身存放在堆中。
5.方法区
方法区(Method Area)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息(类的版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non Heap),目的是与Java堆区分开来。
例如java核心java,会加载到方法区
(1)栈、堆、方法区关系
虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpot JVM而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。
(2)方法区基本理解:
- 方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区
溢出,虚拟机同样会抛出内存溢出错误: java.lang .OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace。 - 关闭JVM就会释放这个区域的内存。
(3)Hotspot中方法区的演进
在JDK7及之前,习惯上把方法区称为永久代。JDK8开始,使用元空间取代了永久代
补充:可以把方法区理解为Java接口,永久代是Java接口实现类
本质上,方法区和永久代并不等价,仅是对HotSpot而言。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit / IBM J9 中不存在永久代的概念。
现在看来,当年使用永久代,不是好的idea,导致Java程序更容易OOM(超过-XX:MaxPermSize上限)
到了JDK8时,HotSpot终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常(OutOfMemoryError: Metaspace)
当Oracle收购BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java Mission Control管理工具,移植到HotSpot虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK 6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK8,终于完
以上是关于❤️ 爆肝二十万字《Java从零到精通教程》,贴心保姆教你从零变大佬 ❤️(建议收藏),学不会找我!的主要内容,如果未能解决你的问题,请参考以下文章
❤️爆肝十二万字《python从零到精通教程》,从零教你变大佬❤️(建议收藏)
❤️手把手教将Java程序部署到Centos7带视频教程肝了十万字全网最详细教程强烈建议收藏❤️
❤️手把手教将Java程序部署到Centos7带视频教程肝了十万字全网最详细教程强烈建议收藏❤️