diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e233607..0000000 --- a/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Created by .ignore support plugin (hsz.mobi) -### Example user template template -### Example user template - -# IntelliJ project files -.idea -*.iml -out -gen -/target/ - - - diff --git a/README.md b/README.md new file mode 100644 index 0000000..e325fcc --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +## 声明 + +**关于仓库** + +本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,其中大部分都是笔者根据自己的理解加上个人博客总结而来的。 + +其中有少数内容可能会包含瞎XX说,语句不通顺,内容不全面等各方面问题,还请见谅。 + +每篇文章都会有笔者更加详细的一系列博客可供参考,这些文章也被我发表在CSDN技术博客上,整理成博客专栏,欢迎查看━(*`∀´*)ノ亻! + +详细内容请见我的CSDN技术博客:https://blog.csdn.net/a724888 + +**更多校招干货和技术文章请关注我的公众号:程序员江湖** + +| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | +| :------: | :---------: | :-------: | :---------: | :---: | :---------:| :---------: | :---------: | :---------:| +| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| + +## 算法 :pencil2: + +> [剑指offer算法总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%89%91%E6%8C%87offer.md) + +> [LeetCode刷题指南](https://github.com/h2pl/Java-Tutorial/blob/master/md/LeetCode%E5%88%B7%E9%A2%98%E6%8C%87%E5%8D%97.md) + +## 操作系统 :computer: + +> [操作系统学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) + +> [Linux内核与基础命令学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Linux%E5%86%85%E6%A0%B8%E4%B8%8E%E5%9F%BA%E7%A1%80%E5%91%BD%E4%BB%A4%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) + +## 网络 :cloud: + +> [计算机网络学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) + +## 数据库 :floppy_disk: + +> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +> [Redis原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Redis%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +## Java :couple: + +> [Java核心技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) + +> [Java集合类总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E9%9B%86%E5%90%88%E7%B1%BB%E6%80%BB%E7%BB%93.md) + +> [Java并发技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E5%B9%B6%E5%8F%91%E6%80%BB%E7%BB%93.md) + +> [JVM原理学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/JVM%E6%80%BB%E7%BB%93.md) + +> [Java网络与NIO总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E7%BD%91%E7%BB%9C%E4%B8%8ENIO%E6%80%BB%E7%BB%93.md) + +## JavaWeb :coffee: + +> [JavaWeb技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/JavaWeb%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) + +> [Spring与SpringMVC源码解析](https://github.com/h2pl/Java-Tutorial/blob/master/md/Spring%E4%B8%8ESpringMVC%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E6%80%BB%E7%BB%93.md) + +## 分布式 :sweat_drops: + +> [分布式理论学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA%E6%80%BB%E7%BB%93.md) + +> [分布式技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +## 设计模式 :hammer: +> [设计模式学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) + +## Hadoop :speak_no_evil: + +> [Hadoop生态学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Hadoop%E7%94%9F%E6%80%81%E6%80%BB%E7%BB%93.md) + +
+ +**关于转载** + +转载的时候请注明一下出处就行啦。 + +另外我这个仓库的格式模仿的是@CyC2018 大佬的仓库 + +并且其中一篇LeetCode刷题指南也是fork这位大佬而来的。我只是自己刷了一遍然后稍微加了一些解析,站在大佬肩膀上。 + + diff --git a/ReadMe.md b/ReadMe.md deleted file mode 100644 index 84572da..0000000 --- a/ReadMe.md +++ /dev/null @@ -1,492 +0,0 @@ -
- - - -
- -

-

- - - - - - -
-

- -力求打造最完整最实用的Java工程师学习指南! - -这些文章和总结都是我近几年学习Java总结和整理出来的,非常实用,对于学习Java后端的朋友来说应该是最全面最完整的技术仓库。 -我靠着这些内容进行复习,拿到了BAT等大厂的offer,这个仓库也已经帮助了很多的Java学习者,如果对你有用,希望能给个star支持我,谢谢! - -为了更好地讲清楚每个知识模块,我们也参考了很多网上的优质博文,力求不漏掉每一个知识点,所有参考博文都将声明转载来源,如有侵权,请联系我。 - -点击关注[微信公众号](#微信公众号)及时获取笔主最新更新文章,并可免费领取Java工程师必备学习资源 - -

- - - -

- -

- -# Java基础 - -## 基础知识 -* [面向对象基础](docs/Java/basic/面向对象基础.md) -* [Java基本数据类型](docs/Java/basic/Java基本数据类型.md) -* [string和包装类](docs/Java/basic/string和包装类.md) -* [final关键字特性](docs/Java/basic/final关键字特性.md) -* [Java类和包](docs/Java/basic/Java类和包.md) -* [抽象类和接口](docs/Java/basic/抽象类和接口.md) -* [代码块和代码执行顺序](docs/Java/basic/代码块和代码执行顺序.md) -* [Java自动拆箱装箱里隐藏的秘密](docs/Java/basic/Java自动拆箱装箱里隐藏的秘密.md) -* [Java中的Class类和Object类](docs/Java/basic/Java中的Class类和Object类.md) -* [Java异常](docs/Java/basic/Java异常.md) -* [解读Java中的回调](docs/Java/basic/解读Java中的回调.md) -* [反射](docs/Java/basic/反射.md) -* [泛型](docs/Java/basic/泛型.md) -* [枚举类](docs/Java/basic/枚举类.md) -* [Java注解和最佳实践](docs/Java/basic/Java注解和最佳实践.md) -* [JavaIO流](docs/Java/basic/JavaIO流.md) -* [多线程](docs/Java/basic/多线程.md) -* [深入理解内部类](docs/Java/basic/深入理解内部类.md) -* [javac和javap](docs/Java/basic/javac和javap.md) -* [Java8新特性终极指南](docs/Java/basic/Java8新特性终极指南.md) -* [序列化和反序列化](docs/Java/basic/序列化和反序列化.md) -* [继承封装多态的实现原理](docs/Java/basic/继承封装多态的实现原理.md) - -## 集合类 - -* [Java集合类总结](docs/Java/collection/Java集合类总结.md) -* [Java集合详解:一文读懂ArrayList,Vector与Stack使用方法和实现原理](docs/Java/collection/Java集合详解:一文读懂ArrayList,Vector与Stack使用方法和实现原理.md) -* [Java集合详解:Queue和LinkedList](docs/Java/collection/Java集合详解:Queue和LinkedList.md) -* [Java集合详解:Iterator,fail-fast机制与比较器](docs/Java/collection/Java集合详解:Iterator,fail-fast机制与比较器.md) -* [Java集合详解:HashMap和HashTable](docs/Java/collection/Java集合详解:HashMap和HashTable.md) -* [Java集合详解:深入理解LinkedHashMap和LRU缓存](docs/Java/collection/Java集合详解:深入理解LinkedHashMap和LRU缓存.md) -* [Java集合详解:TreeMap和红黑树](docs/Java/collection/Java集合详解:TreeMap和红黑树.md) -* [Java集合详解:HashSet,TreeSet与LinkedHashSet](docs/Java/collection/Java集合详解:HashSet,TreeSet与LinkedHashSet.md) -* [Java集合详解:Java集合类细节精讲](docs/Java/collection/Java集合详解:Java集合类细节精讲.md) - -# JavaWeb - -* [走进JavaWeb技术世界:JavaWeb的由来和基础知识](docs/JavaWeb/走进JavaWeb技术世界:JavaWeb的由来和基础知识.md) -* [走进JavaWeb技术世界:JSP与Servlet的曾经与现在](docs/JavaWeb/走进JavaWeb技术世界:JSP与Servlet的曾经与现在.md) -* [走进JavaWeb技术世界:JDBC的进化与连接池技术](docs/JavaWeb/走进JavaWeb技术世界:JDBC的进化与连接池技术.md) -* [走进JavaWeb技术世界:Servlet工作原理详解](docs/JavaWeb/走进JavaWeb技术世界:Servlet工作原理详解.md) -* [走进JavaWeb技术世界:初探Tomcat的HTTP请求过程](docs/JavaWeb/走进JavaWeb技术世界:初探Tomcat的HTTP请求过程.md) -* [走进JavaWeb技术世界:Tomcat5总体架构剖析](docs/JavaWeb/走进JavaWeb技术世界:Tomcat5总体架构剖析.md) -* [走进JavaWeb技术世界:Tomcat和其他WEB容器的区别](docs/JavaWeb/走进JavaWeb技术世界:Tomcat和其他WEB容器的区别.md) -* [走进JavaWeb技术世界:浅析Tomcat9请求处理流程与启动部署过程](docs/JavaWeb/走进JavaWeb技术世界:浅析Tomcat9请求处理流程与启动部署过程.md) -* [走进JavaWeb技术世界:Java日志系统的诞生与发展](docs/JavaWeb/走进JavaWeb技术世界:Java日志系统的诞生与发展.md) -* [走进JavaWeb技术世界:从JavaBean讲到Spring](docs/JavaWeb/走进JavaWeb技术世界:从JavaBean讲到Spring.md) -* [走进JavaWeb技术世界:单元测试框架Junit](docs/JavaWeb/走进JavaWeb技术世界:单元测试框架Junit.md) -* [走进JavaWeb技术世界:从手动编译打包到项目构建工具Maven](docs/JavaWeb/走进JavaWeb技术世界:从手动编译打包到项目构建工具Maven.md) -* [走进JavaWeb技术世界:Hibernate入门经典与注解式开发](docs/JavaWeb/走进JavaWeb技术世界:Hibernate入门经典与注解式开发.md) -* [走进JavaWeb技术世界:Mybatis入门](docs/JavaWeb/走进JavaWeb技术世界:Mybatis入门.md) -* [走进JavaWeb技术世界:深入浅出Mybatis基本原理](docs/JavaWeb/走进JavaWeb技术世界:深入浅出Mybatis基本原理.md) -* [走进JavaWeb技术世界:极简配置的SpringBoot](docs/JavaWeb/走进JavaWeb技术世界:极简配置的SpringBoot.md) - -# Java进阶 - -## 并发编程 - -* [Java并发指南:并发基础与Java多线程](docs/Java/concurrency/Java并发指南:并发基础与Java多线程.md) -* [Java并发指南:深入理解Java内存模型JMM](docs/Java/concurrency/Java并发指南:深入理解Java内存模型JMM.md) -* [Java并发指南:并发三大问题与volatile关键字,CAS操作](docs/Java/concurrency/Java并发指南:并发三大问题与volatile关键字,CAS操作.md) -* [Java并发指南:Java中的锁Lock和synchronized](docs/Java/concurrency/Java并发指南:Java中的锁Lock和synchronized.md) -* [Java并发指南:JMM中的final关键字解析](docs/Java/concurrency/Java并发指南:JMM中的final关键字解析.md) -* [Java并发指南:Java内存模型JMM总结](docs/Java/concurrency/Java并发指南:Java内存模型JMM总结.md) -* [Java并发指南:JUC的核心类AQS详解](docs/Java/concurrency/Java并发指南:JUC的核心类AQS详解.md) -* [Java并发指南:AQS中的公平锁与非公平锁,Condtion](docs/Java/concurrency/Java并发指南:AQS中的公平锁与非公平锁,Condtion.md) -* [Java并发指南:AQS共享模式与并发工具类的实现](docs/Java/concurrency/Java并发指南:AQS共享模式与并发工具类的实现.md) -* [Java并发指南:Java读写锁ReentrantReadWriteLock源码分析](docs/Java/concurrency/Java并发指南:Java读写锁ReentrantReadWriteLock源码分析.md) -* [Java并发指南:解读Java阻塞队列BlockingQueue](docs/Java/concurrency/Java并发指南:解读Java阻塞队列BlockingQueue.md) -* [Java并发指南:深度解读java线程池设计思想及源码实现](docs/Java/concurrency/Java并发指南:深度解读Java线程池设计思想及源码实现.md) -* [Java并发指南:Java中的HashMap和ConcurrentHashMap全解析](docs/Java/concurrency/Java并发指南:Java中的HashMap和ConcurrentHashMap全解析.md) -* [Java并发指南:JUC中常用的Unsafe和Locksupport](docs/Java/concurrency/Java并发指南:JUC中常用的Unsafe和Locksupport.md) -* [Java并发指南:ForkJoin并发框架与工作窃取算法剖析](docs/Java/concurrency/Java并发指南:ForkJoin并发框架与工作窃取算法剖析.md) -* [Java并发编程学习总结](docs/Java/concurrency/Java并发编程学习总结.md) - -## JVM - -* [JVM总结](docs/Java/JVM/JVM总结.md) -* [深入理解JVM虚拟机:JVM内存的结构与消失的永久代](docs/Java/JVM/深入理解JVM虚拟机:JVM内存的结构与消失的永久代.md) -* [深入理解JVM虚拟机:JVM垃圾回收基本原理和算法](docs/Java/JVM/深入理解JVM虚拟机:JVM垃圾回收基本原理和算法.md) -* [深入理解JVM虚拟机:垃圾回收器详解](docs/Java/JVM/深入理解JVM虚拟机:垃圾回收器详解.md) -* [深入理解JVM虚拟机:Javaclass介绍与解析实践](docs/Java/JVM/深入理解JVM虚拟机:Java字节码介绍与解析实践.md) -* [深入理解JVM虚拟机:虚拟机字节码执行引擎](docs/Java/JVM/深入理解JVM虚拟机:虚拟机字节码执行引擎.md) -* [深入理解JVM虚拟机:深入理解JVM类加载机制](docs/Java/JVM/深入理解JVM虚拟机:深入理解JVM类加载机制.md) -* [深入理解JVM虚拟机:JNDI,OSGI,Tomcat类加载器实现](docs/Java/JVM/深入理解JVM虚拟机:JNDI,OSGI,Tomcat类加载器实现.md) -* [深入了解JVM虚拟机:Java的编译期优化与运行期优化](docs/Java/JVM/深入理解JVM虚拟机:Java的编译期优化与运行期优化.md) -* [深入理解JVM虚拟机:JVM监控工具与诊断实践](docs/Java/JVM/深入理解JVM虚拟机:JVM监控工具与诊断实践.md) -* [深入理解JVM虚拟机:JVM常用参数以及调优实践](docs/Java/JVM/深入理解JVM虚拟机:JVM常用参数以及调优实践.md) -* [深入理解JVM虚拟机:Java内存异常原理与实践](docs/Java/JVM/深入理解JVM虚拟机:Java内存异常原理与实践.md) -* [深入理解JVM虚拟机:JVM性能管理神器VisualVM介绍与实战](docs/Java/JVM/深入理解JVM虚拟机:JVM性能管理神器VisualVM介绍与实战.md) -* [深入理解JVM虚拟机:再谈四种引用及GC实践](docs/Java/JVM/深入理解JVM虚拟机:再谈四种引用及GC实践.md) -* [深入理解JVM虚拟机:GC调优思路与常用工具](docs/Java/JVM/深入理解JVM虚拟机:GC调优思路与常用工具.md) - -## Java网络编程 - -* [Java网络编程和NIO详解:JAVA 中原生的 socket 通信机制](docs/Java/network/Java网络编程与NIO详解:JAVA中原生的socket通信机制.md) -* [Java网络编程与NIO详解:JAVA NIO 一步步构建IO多路复用的请求模型](docs/Java/network/Java网络编程与NIO详解:JavaNIO一步步构建IO多路复用的请求模型.md) -* [Java网络编程和NIO详解:IO模型与Java网络编程模型](docs/Java/network/Java网络编程与NIO详解:IO模型与Java网络编程模型.md) -* [Java网络编程与NIO详解:浅析NIO包中的BufferChannel和Selector](docs/Java/network/Java网络编程与NIO详解:浅析NIO包中的BufferChannel和Selector.md) -* [Java网络编程和NIO详解:Java非阻塞IO和异步IO](docs/Java/network/Java网络编程与NIO详解:Java非阻塞IO和异步IO.md) -* [Java网络编程与NIO详解:LinuxEpoll实现原理详解](docs/Java/network/Java网络编程与NIO详解:LinuxEpoll实现原理详解.md.md) -* [Java网络编程与NIO详解:浅谈Linux中Selector的实现原理](docs/Java/network/Java网络编程与NIO详解:浅谈Linux中Selector的实现原理.md) -* [Java网络编程与NIO详解:浅析mmap和DirectBuffer](docs/Java/network/Java网络编程与NIO详解:浅析mmap和DirectBuffer.md) -* [Java网络编程与NIO详解:基于NIO的网络编程框架Netty](docs/Java/network/Java网络编程与NIO详解:基于NIO的网络编程框架Netty.md) -* [Java网络编程与NIO详解:Java网络编程与NIO详解](docs/Java/network/Java网络编程与NIO详解:深度解读Tomcat中的NIO模型.md) -* [Java网络编程与NIO详解:Tomcat中的Connector源码分析(NIO)](docs/Java/network/Java网络编程与NIO详解:Tomcat中的Connector源码分析(NIO).md) - -# Spring全家桶 - -## Spring - -* [SpringAOP的概念与作用](docs/Spring全家桶/Spring/Spring常见注解.md) -* [SpringBean的定义与管理(核心)](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring中对于数据库的访问](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring中对于校验功能的支持](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring中的Environment环境变量](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring中的事件处理机制](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring中的资源管理](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring中的配置元数据(管理配置的基本数据)](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring事务基本用法](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring合集](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring容器与IOC](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring常见注解](docs/Spring全家桶/Spring/Spring常见注解.md) -* [Spring概述](docs/Spring全家桶/Spring/Spring常见注解.md) -* [第一个Spring应用](docs/Spring全家桶/Spring/Spring常见注解.md) - -## Spring源码分析 - -### 综合 -* [Spring源码剖析:初探SpringIOC核心流程](docs/Spring全家桶/Spring源码分析/Spring源码剖析:初探SpringIOC核心流程.md) -* [Spring源码剖析:SpringIOC容器的加载过程 ](docs/Spring全家桶/Spring源码分析/Spring源码剖析:SpringIOC容器的加载过程.md) -* [Spring源码剖析:懒加载的单例Bean获取过程分析](docs/Spring全家桶/Spring源码分析/Spring源码剖析:懒加载的单例Bean获取过程分析.md) -* [Spring源码剖析:JDK和cglib动态代理原理详解 ](docs/Spring全家桶/Spring源码分析/Spring源码剖析:JDK和cglib动态代理原理详解.md) -* [Spring源码剖析:SpringAOP概述](docs/Spring全家桶/Spring源码分析/Spring源码剖析:SpringAOP概述.md) -* [Spring源码剖析:AOP实现原理详解 ](docs/Spring全家桶/Spring源码分析/Spring源码剖析:AOP实现原理详解.md) -* [Spring源码剖析:Spring事务概述](docs/Spring全家桶/Spring源码分析/Spring源码剖析:Spring事务概述.md) -* [Spring源码剖析:Spring事务源码剖析](docs/Spring全家桶/Spring源码分析/Spring源码剖析:Spring事务源码剖析.md) - -### AOP -* [AnnotationAwareAspectJAutoProxyCreator 分析(上)](docs/Spring全家桶/Spring源码分析/SpringAOP/AnnotationAwareAspectJAutoProxyCreator分析(上).md) -* [AnnotationAwareAspectJAutoProxyCreator 分析(下)](docs/Spring全家桶/Spring源码分析/SpringAOP/AnnotationAwareAspectJAutoProxyCreator分析(下).md) -* [AOP示例demo及@EnableAspectJAutoProxy](docs/Spring全家桶/Spring源码分析/SpringAOP/AOP示例demo及@EnableAspectJAutoProxy.md) -* [SpringAop(四):jdk 动态代理](docs/Spring全家桶/Spring源码分析/SpringAOP/SpringAop(四):jdk动态代理.md) -* [SpringAop(五):cglib 代理](docs/Spring全家桶/Spring源码分析/SpringAOP/SpringAop(五):cglib代理.md) -* [SpringAop(六):aop 总结](docs/Spring全家桶/Spring源码分析/SpringAOP/SpringAop(六):aop总结.md) - -### 事务 -* [spring 事务(一):认识事务组件](docs/Spring全家桶/Spring源码分析/Spring事务/Spring事务(一):认识事务组件.md) -* [spring 事务(二):事务的执行流程](docs/Spring全家桶/Spring源码分析/Spring事务/Spring事务(二):事务的执行流程.md) -* [spring 事务(三):事务的隔离级别与传播方式的处理](docs/Spring全家桶/Spring源码分析/Spring事务/Spring事务(三):事务的隔离级别与传播方式的处理01.md) -* [spring 事务(四):事务的隔离级别与传播方式的处理](docs/Spring全家桶/Spring源码分析/Spring事务/Spring事务(四):事务的隔离级别与传播方式的处理02.md) -* [spring 事务(五):事务的隔离级别与传播方式的处理](docs/Spring全家桶/Spring源码分析/Spring事务/Spring事务(五):事务的隔离级别与传播方式的处理03.md) -* [spring 事务(六):事务的隔离级别与传播方式的处理](docs/Spring全家桶/Spring源码分析/Spring事务/Spring事务(六):事务的隔离级别与传播方式的处理04.md) - -### 启动流程 -* [spring启动流程(一):启动流程概览](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(一):启动流程概览.md) -* [spring启动流程(二):ApplicationContext 的创建](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(二):ApplicationContext的创建.md) -* [spring启动流程(三):包的扫描流程](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(三):包的扫描流程.md) -* [spring启动流程(四):启动前的准备工作](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(四):启动前的准备工作.md) -* [spring启动流程(五):执行 BeanFactoryPostProcessor](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(五):执行BeanFactoryPostProcessor.md) -* [spring启动流程(六):注册 BeanPostProcessor](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(六):注册BeanPostProcessor.md) -* [spring启动流程(七):国际化与事件处理](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(七):国际化与事件处理.md) -* [spring启动流程(八):完成 BeanFactory 的初始化](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(八):完成BeanFactory的初始化.md) -* [spring启动流程(九):单例 bean 的创建](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(九):单例bean的创建.md) -* [spring启动流程(十):启动完成的处理](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(十):启动完成的处理.md) -* [spring启动流程(十一):启动流程总结](docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(十一):启动流程总结.md) - -### 组件分析 -* [spring 组件之 ApplicationContext](docs/Spring全家桶/Spring源码分析/Spring组件分析/Spring组件之ApplicationContext.md) -* [spring 组件之 BeanDefinition](docs/Spring全家桶/Spring源码分析/Spring组件分析/Spring组件之BeanDefinition.md) -* [Spring 组件之 BeanFactory](docs/Spring全家桶/Spring源码分析/Spring组件分析/Spring组件之BeanFactory.md) -* [spring 组件之 BeanFactoryPostProcessor](docs/Spring全家桶/Spring源码分析/Spring组件分析/Spring组件之BeanFactoryPostProcessor.md) -* [spring 组件之 BeanPostProcessor](docs/Spring全家桶/Spring源码分析/Spring组件分析/Spring组件之BeanPostProcessor.md) - -### 重要机制探秘 - -* [ConfigurationClassPostProcessor(一):处理 @ComponentScan 注解](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/ConfigurationClassPostProcessor(一):处理@ComponentScan注解.md) -* [ConfigurationClassPostProcessor(三):处理 @Import 注解](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/ConfigurationClassPostProcessor(三):处理@Import注解.md) -* [ConfigurationClassPostProcessor(二):处理 @Bean 注解](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/ConfigurationClassPostProcessor(二):处理@Bean注解.md) -* [ConfigurationClassPostProcessor(四):处理 @Conditional 注解](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/ConfigurationClassPostProcessor(四):处理@Conditional注解.md) -* [Spring 探秘之 AOP 的执行顺序](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/Spring探秘之AOP的执行顺序.md) -* [Spring 探秘之 Spring 事件机制](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/Spring探秘之Spring事件机制.md) -* [spring 探秘之循环依赖的解决(一):理论基石](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/Spring探秘之循环依赖的解决(一):理论基石.md) -* [spring 探秘之循环依赖的解决(二):源码分析](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/Spring探秘之循环依赖的解决(二):源码分析.md) -* [spring 探秘之监听器注解 @EventListener](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/spring探秘之监听器注解@EventListener.md) -* [spring 探秘之组合注解的处理](docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/Spring探秘之组合注解的处理.md) - -## SpringMVC - -* [SpringMVC中的国际化功能](docs/Spring全家桶/SpringMVC/SpringMVC中的国际化功能.md) -* [SpringMVC中的异常处理器](docs/Spring全家桶/SpringMVC/SpringMVC中的异常处理器.md) -* [SpringMVC中的拦截器](docs/Spring全家桶/SpringMVC/SpringMVC中的拦截器.md) -* [SpringMVC中的视图解析器](docs/Spring全家桶/SpringMVC/SpringMVC中的视图解析器.md) -* [SpringMVC中的过滤器Filter](docs/Spring全家桶/SpringMVC/SpringMVC中的过滤器Filter.md) -* [SpringMVC基本介绍与快速入门](docs/Spring全家桶/SpringMVC/SpringMVC基本介绍与快速入门.md) -* [SpringMVC如何实现文件上传](docs/Spring全家桶/SpringMVC/SpringMVC如何实现文件上传.md) -* [SpringMVC中的常用功能](docs/Spring全家桶/SpringMVC/SpringMVC中的常用功能.md) - -## SpringMVC源码分析 - -* [SpringMVC源码分析:SpringMVC概述](docs/Spring全家桶/SpringMVC源码分析/SpringMVC源码分析:SpringMVC概述.md) -* [SpringMVC源码分析:SpringMVC设计理念与DispatcherServlet](docs/Spring全家桶/SpringMVC源码分析/SpringMVC源码分析:SpringMVC设计理念与DispatcherServlet.md) -* [SpringMVC源码分析:DispatcherServlet的初始化与请求转发 ](docs/Spring全家桶/SpringMVC源码分析/SpringMVC源码分析:DispatcherServlet的初始化与请求转发.md) -* [SpringMVC源码分析:DispatcherServlet如何找到正确的Controller ](docs/Spring全家桶/SpringMVC源码分析/SpringMVC源码分析:DispatcherServlet如何找到正确的Controller.md) -* [SpringMVC源码剖析:消息转换器HttpMessageConverter与@ResponseBody注解](docs/Spring全家桶/SpringMVC/SpringMVC源码剖析:消息转换器HttpMessageConverter与@ResponseBody注解.md) -* [DispatcherServlet 初始化流程 ](docs/Spring全家桶/SpringMVC源码分析/DispatcherServlet初始化流程.md) -* [RequestMapping 初始化流程 ](docs/Spring全家桶/SpringMVC源码分析/RequestMapping初始化流程.md) -* [Spring 容器启动 Tomcat ](docs/Spring全家桶/SpringMVC源码分析/Spring容器启动Tomcat.md) -* [SpringMVC demo 与@EnableWebMvc 注解 ](docs/Spring全家桶/SpringMVC源码分析/SpringMVC的Demo与@EnableWebMvc注解.md) -* [SpringMVC 整体源码结构总结 ](docs/Spring全家桶/SpringMVC源码分析/SpringMVC整体源码结构总结.md) -* [请求执行流程(一)之获取 Handler ](docs/Spring全家桶/SpringMVC源码分析/请求执行流程(一)之获取Handler.md) -* [请求执行流程(二)之执行 Handler 方法 ](docs/Spring全家桶/SpringMVC源码分析/请求执行流程(二)之执行Handler方法.md) - -## SpringBoot - -* [SpringBoot系列:SpringBoot的前世今生](docs/Spring全家桶/SpringBoot/SpringBoot的前世今生.md) -* [给你一份SpringBoot知识清单.md](docs/Spring全家桶/SpringBoot/给你一份SpringBoot知识清单.md) -* [Spring常见注解使用指南(包含Spring+SpringMVC+SpringBoot)](docs/Spring全家桶/SpringBoot/Spring常见注解使用指南(包含Spring+SpringMVC+SpringBoot).md) -* [SpringBoot中的日志管理](docs/Spring全家桶/SpringBoot/SpringBoot中的日志管理.md) -* [SpringBoot常见注解](docs/Spring全家桶/SpringBoot/SpringBoot常见注解.md) -* [SpringBoot应用也可以部署到外部Tomcat](docs/Spring全家桶/SpringBoot/SpringBoot应用也可以部署到外部Tomcat.md) -* [SpringBoot生产环境工具Actuator](docs/Spring全家桶/SpringBoot/SpringBoot生产环境工具Actuator.md) -* [SpringBoot的Starter机制](docs/Spring全家桶/SpringBoot/SpringBoot的Starter机制.md) -* [SpringBoot的前世今生](docs/Spring全家桶/SpringBoot/SpringBoot的前世今生.md) -* [SpringBoot的基本使用](docs/Spring全家桶/SpringBoot/SpringBoot的基本使用.md) -* [SpringBoot的配置文件管理](docs/Spring全家桶/SpringBoot/SpringBoot的配置文件管理.md) -* [SpringBoot自带的热部署工具](docs/Spring全家桶/SpringBoot/SpringBoot自带的热部署工具.md) -* [SpringBoot中的任务调度与@Async](docs/Spring全家桶/SpringBoot/SpringBoot中的任务调度与@Async.md) -* [基于SpringBoot中的开源监控工具SpringBootAdmin](docs/Spring全家桶/SpringBoot/基于SpringBoot中的开源监控工具SpringBootAdmin.md) - -## SpringBoot源码分析 -* [@SpringBootApplication 注解](docs/Spring全家桶/SpringBoot源码解析/@SpringBootApplication注解.md) -* [springboot web应用(一):servlet 组件的注册流程](docs/Spring全家桶/SpringBoot源码解析/SpringBootWeb应用(一):servlet组件的注册流程.md) -* [springboot web应用(二):WebMvc 装配过程](docs/Spring全家桶/SpringBoot源码解析/SpringBootWeb应用(二):WebMvc装配过程.md) - -* [SpringBoot 启动流程(一):准备 SpringApplication](docs/Spring全家桶/SpringBoot源码解析/SpringBoot启动流程(一):准备SpringApplication.md) -* [SpringBoot 启动流程(二):准备运行环境](docs/Spring全家桶/SpringBoot源码解析/SpringBoot启动流程(二):准备运行环境.md) -* [SpringBoot 启动流程(三):准备IOC容器](docs/Spring全家桶/SpringBoot源码解析/SpringBoot启动流程(三):准备IOC容器.md) -* [springboot 启动流程(四):启动IOC容器](docs/Spring全家桶/SpringBoot源码解析/SpringBoot启动流程(四):启动IOC容器.md) -* [springboot 启动流程(五):完成启动](docs/Spring全家桶/SpringBoot源码解析/SpringBoot启动流程(五):完成启动.md) -* [springboot 启动流程(六):启动流程总结](docs/Spring全家桶/SpringBoot源码解析/SpringBoot启动流程(六):启动流程总结.md) - -* [springboot 自动装配(一):加载自动装配类](docs/Spring全家桶/SpringBoot源码解析/SpringBoot自动装配(一):加载自动装配类.md) -* [springboot 自动装配(二):条件注解](docs/Spring全家桶/SpringBoot源码解析/SpringBoot自动装配(二):条件注解.md) -* [springboot 自动装配(三):自动装配顺序](docs/Spring全家桶/SpringBoot源码解析/SpringBoot自动装配(三):自动装配顺序.md) - -## SpringCloud -* [SpringCloud概述](docs/Spring全家桶/SpringCloud/SpringCloud概述.md) -* [Spring Cloud Config](docs/Spring全家桶/SpringCloud/SpringCloudConfig.md) -* [Spring Cloud Consul](docs/Spring全家桶/SpringCloud/SpringCloudConsul.md) -* [Spring Cloud Eureka](docs/Spring全家桶/SpringCloud/SpringCloudEureka.md) -* [Spring Cloud Gateway](docs/Spring全家桶/SpringCloud/SpringCloudGateway.md) -* [Spring Cloud Hystrix](docs/Spring全家桶/SpringCloud/SpringCloudHystrix.md) -* [Spring Cloud LoadBalancer](docs/Spring全家桶/SpringCloud/SpringCloudLoadBalancer.md) -* [Spring Cloud OpenFeign](docs/Spring全家桶/SpringCloud/SpringCloudOpenFeign.md) -* [Spring Cloud Ribbon](docs/Spring全家桶/SpringCloud/SpringCloudRibbon.md) -* [Spring Cloud Sleuth](docs/Spring全家桶/SpringCloud/SpringCloudSleuth.md) -* [Spring Cloud Zuul](docs/Spring全家桶/SpringCloud/SpringCloudZuul.md) - -## SpringCloud 源码分析 -* [Spring Cloud Config源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudConfig源码分析.md) -* [Spring Cloud Eureka源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudEureka源码分析.md) -* [Spring Cloud Gateway源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudGateway源码分析.md) -* [Spring Cloud Hystrix源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudHystrix源码分析.md) -* [Spring Cloud LoadBalancer源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudLoadBalancer源码分析.md) -* [Spring Cloud OpenFeign源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudOpenFeign源码分析.md) -* [Spring Cloud Ribbon源码分析](docs/Spring全家桶/SpringCloud源码分析/SpringCloudRibbon源码分析.md) - -## SpringCloud Alibaba -* [SpringCloud Alibaba概览](docs/Spring全家桶/SpringCloudAlibaba/SpringCloudAlibaba概览.md) -* [SpringCloud Alibaba nacos](docs/Spring全家桶/SpringCloudAlibaba/SpringCloudAlibabaNacos.md) -* [SpringCloud Alibaba RocketMQ](docs/Spring全家桶/SpringCloudAlibaba/SpringCloudAlibabaRocketMQ.md) -* [SpringCloud Alibaba sentinel](docs/Spring全家桶/SpringCloudAlibaba/SpringCloudAlibabaSentinel.md) -* [SpringCloud Alibaba skywalking](docs/Spring全家桶/SpringCloudAlibaba/SpringCloudAlibabaSkywalking.md) -* [SpringCloud Alibaba seata](docs/Spring全家桶/SpringCloudAlibaba/SpringCloudAlibabaSeata.md) - -## SpringCloud Alibaba源码分析 -* [Spring Cloud Seata源码分析](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudSeata源码分析.md) -* [Spring Cloud Sentinel源码分析](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudSentinel源码分析.md) -* [SpringCloudAlibaba nacos源码分析:概览](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudAlibabaNacos源码分析:概览.md) -* [SpringCloudAlibaba nacos源码分析:服务发现](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudAlibabaNacos源码分析:服务发现.md) -* [SpringCloudAlibaba nacos源码分析:服务注册](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudAlibabaNacos源码分析:服务注册.md) -* [SpringCloudAlibaba nacos源码分析:配置中心](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudAlibabaNacos源码分析:配置中心.md) -* [Spring Cloud RocketMQ源码分析](docs/Spring全家桶/SpringCloudAlibaba源码分析/SpringCloudRocketMQ源码分析.md) - -# 设计模式 - -* [设计模式学习总结](docs/Java/design-parttern/设计模式学习总结.md) -* [初探Java设计模式:创建型模式(工厂,单例等).md](docs/Java/design-parttern/初探Java设计模式:创建型模式(工厂,单例等).md) -* [初探Java设计模式:结构型模式(代理模式,适配器模式等).md](docs/Java/design-parttern/初探Java设计模式:结构型模式(代理模式,适配器模式等).md) -* [初探Java设计模式:行为型模式(策略,观察者等).md](docs/Java/design-parttern/初探Java设计模式:行为型模式(策略,观察者等).md) -* [初探Java设计模式:JDK中的设计模式.md](docs/Java/design-parttern/初探Java设计模式:JDK中的设计模式.md) -* [初探Java设计模式:Spring涉及到的种设计模式.md](docs/Java/design-parttern/初探Java设计模式:Spring涉及到的种设计模式.md) - - -# 计算机基础 - -## 计算机网络 -todo - - -## 操作系统 -todo - -## Linux相关 -todo - - -## 数据结构与算法 -todo - -## 数据结构 -todo - -## 算法 -todo - -# 数据库 -todo - -## MySQL -* [Mysql原理与实践总结](docs/database/Mysql原理与实践总结.md) -* [重新学习Mysql数据库:无废话MySQL入门](docs/database/重新学习MySQL数据库:无废话MySQL入门.md) -* [重新学习Mysql数据库:『浅入浅出』MySQL和InnoDB](docs/database/重新学习MySQL数据库:『浅入浅出』MySQL和InnoDB.md) -* [重新学习Mysql数据库:Mysql存储引擎与数据存储原理](docs/database/重新学习MySQL数据库:Mysql存储引擎与数据存储原理.md) -* [重新学习Mysql数据库:Mysql索引实现原理和相关数据结构算法](docs/database/重新学习MySQL数据库:Mysql索引实现原理和相关数据结构算法.md) -* [重新学习Mysql数据库:根据MySQL索引原理进行分析与优化](docs/database/重新学习MySQL数据库:根据MySQL索引原理进行分析与优化.md) -* [重新学习MySQL数据库:浅谈MySQL的中事务与锁](docs/database/重新学习MySQL数据库:浅谈MySQL的中事务与锁.md) -* [重新学习Mysql数据库:详解MyIsam与InnoDB引擎的锁实现](docs/database/重新学习MySQL数据库:详解MyIsam与InnoDB引擎的锁实现.md) -* [重新学习Mysql数据库:MySQL的事务隔离级别实战](docs/database/重新学习MySQL数据库:MySQL的事务隔离级别实战.md) -* [重新学习MySQL数据库:Innodb中的事务隔离级别和锁的关系](docs/database/重新学习MySQL数据库:Innodb中的事务隔离级别和锁的关系.md) -* [重新学习MySQL数据库:MySQL里的那些日志们](docs/database/重新学习MySQL数据库:MySQL里的那些日志们.md) -* [重新学习MySQL数据库:以Java的视角来聊聊SQL注入](docs/database/重新学习MySQL数据库:以Java的视角来聊聊SQL注入.md) -* [重新学习MySQL数据库:从实践sql语句优化开始](docs/database/重新学习MySQL数据库:从实践sql语句优化开始.md) -* [重新学习Mysql数据库:Mysql主从复制,读写分离,分表分库策略与实践](docs/database/重新学习MySQL数据库:Mysql主从复制,读写分离,分表分库策略与实践.md) - - -# 缓存 - -## Redis -* [Redis原理与实践总结](docs/cache/Redis原理与实践总结.md) -* [探索Redis设计与实现开篇:什么是Redis](docs/cache/探索Redis设计与实现开篇:什么是Redis.md) -* [探索Redis设计与实现:Redis的基础数据结构概览](docs/cache/探索Redis设计与实现:Redis的基础数据结构概览.md) -* [探索Redis设计与实现:Redis内部数据结构详解——dict](docs/cache/探索Redis设计与实现:Redis内部数据结构详解——dict.md) -* [探索Redis设计与实现:Redis内部数据结构详解——sds](docs/cache/探索Redis设计与实现:Redis内部数据结构详解——sds.md) -* [探索Redis设计与实现:Redis内部数据结构详解——ziplist](docs/cache/探索Redis设计与实现:Redis内部数据结构详解——ziplist.md) -* [探索Redis设计与实现:Redis内部数据结构详解——quicklist](docs/cache/探索Redis设计与实现:Redis内部数据结构详解——quicklist.md) -* [探索Redis设计与实现:Redis内部数据结构详解——skiplist](docs/cache/探索Redis设计与实现:Redis内部数据结构详解——skiplist.md) -* [探索Redis设计与实现:Redis内部数据结构详解——intset](docs/cache/探索Redis设计与实现:Redis内部数据结构详解——intset.md) -* [探索Redis设计与实现:连接底层与表面的数据结构robj](docs/cache/探索Redis设计与实现:连接底层与表面的数据结构robj.md) -* [探索Redis设计与实现:数据库redisDb与键过期删除策略](docs/cache/探索Redis设计与实现:数据库redisDb与键过期删除策略.md) -* [探索Redis设计与实现:Redis的事件驱动模型与命令执行过程](docs/cache/探索Redis设计与实现:Redis的事件驱动模型与命令执行过程.md) -* [探索Redis设计与实现:使用快照和AOF将Redis数据持久化到硬盘中](docs/cache/探索Redis设计与实现:使用快照和AOF将Redis数据持久化到硬盘中.md) -* [探索Redis设计与实现:浅析Redis主从复制](docs/cache/探索Redis设计与实现:浅析Redis主从复制.md) -* [探索Redis设计与实现:Redis集群机制及一个Redis架构演进实例](docs/cache/探索Redis设计与实现:Redis集群机制及一个Redis架构演进实例.md) -* [探索Redis设计与实现:Redis事务浅析与ACID特性介绍](docs/cache/探索Redis设计与实现:Redis事务浅析与ACID特性介绍.md) -* [探索Redis设计与实现:Redis分布式锁进化史 ](docs/cache/探索Redis设计与实现:Redis分布式锁进化史.md ) - -# 消息队列 - -## Kafka -* [消息队列kafka详解:Kafka快速上手(Java版)](docs/mq/kafka/消息队列kafka详解:Kafka快速上手(Java版).md) -* [消息队列kafka详解:Kafka一条消息存到broker的过程](docs/mq/kafka/消息队列kafka详解:Kafka一条消息存到broker的过程.md) -* [消息队列kafka详解:消息队列kafka详解:Kafka介绍](docs/mq/kafka/消息队列kafka详解:Kafka介绍.md) -* [消息队列kafka详解:Kafka原理分析总结篇](docs/mq/kafka/消息队列kafka详解:Kafka原理分析总结篇.md) -* [消息队列kafka详解:Kafka常见命令及配置总结](docs/mq/kafka/消息队列kafka详解:Kafka常见命令及配置总结.md) -* [消息队列kafka详解:Kafka架构介绍](docs/mq/kafka/消息队列kafka详解:Kafka架构介绍.md) -* [消息队列kafka详解:Kafka的集群工作原理](docs/mq/kafka/消息队列kafka详解:Kafka的集群工作原理.md) -* [消息队列kafka详解:Kafka重要知识点+面试题大全](docs/mq/kafka/消息队列kafka详解:Kafka重要知识点+面试题大全.md) -* [消息队列kafka详解:如何实现延迟队列](docs/mq/kafka/消息队列kafka详解:如何实现延迟队列.md) -* [消息队列kafka详解:如何实现死信队列](docs/mq/kafka/消息队列kafka详解:如何实现死信队列.md) - -## RocketMQ -* [RocketMQ系列:事务消息(最终一致性)](docs/mq/RocketMQ/RocketMQ系列:事务消息(最终一致性).md) -* [RocketMQ系列:基本概念](docs/mq/RocketMQ/RocketMQ系列:基本概念.md) -* [RocketMQ系列:广播与延迟消息](docs/mq/RocketMQ/RocketMQ系列:广播与延迟消息.md) -* [RocketMQ系列:批量发送与过滤](docs/mq/RocketMQ/RocketMQ系列:批量发送与过滤.md) -* [RocketMQ系列:消息的生产与消费](docs/mq/RocketMQ/RocketMQ系列:消息的生产与消费.md) -* [RocketMQ系列:环境搭建](docs/mq/RocketMQ/RocketMQ系列:环境搭建.md) -* [RocketMQ系列:顺序消费](docs/mq/RocketMQ/RocketMQ系列:顺序消费.md) - -# 大后端 -* [后端技术杂谈开篇:云计算,大数据与AI的故事](docs/backend/后端技术杂谈开篇:云计算,大数据与AI的故事.md) -* [后端技术杂谈:搜索引擎基础倒排索引](docs/backend/后端技术杂谈:搜索引擎基础倒排索引.md) -* [后端技术杂谈:搜索引擎工作原理](docs/backend/后端技术杂谈:搜索引擎工作原理.md) -* [后端技术杂谈:Lucene基础原理与实践](docs/backend/后端技术杂谈:Lucene基础原理与实践.md) -* [后端技术杂谈:Elasticsearch与solr入门实践](docs/backend/后端技术杂谈:Elasticsearch与solr入门实践.md) -* [后端技术杂谈:云计算的前世今生](docs/backend/后端技术杂谈:云计算的前世今生.md) -* [后端技术杂谈:白话虚拟化技术](docs/backend/后端技术杂谈:白话虚拟化技术.md ) -* [后端技术杂谈:OpenStack的基石KVM](docs/backend/后端技术杂谈:OpenStack的基石KVM.md) -* [后端技术杂谈:OpenStack架构设计](docs/backend/后端技术杂谈:OpenStack架构设计.md) -* [后端技术杂谈:先搞懂Docker核心概念吧](docs/backend/后端技术杂谈:先搞懂Docker核心概念吧.md) -* [后端技术杂谈:Docker 核心技术与实现原理](docs/backend/后端技术杂谈:Docker%核心技术与实现原理.md) -* [后端技术杂谈:十分钟理解Kubernetes核心概念](docs/backend/后端技术杂谈:十分钟理解Kubernetes核心概念.md) -* [后端技术杂谈:捋一捋大数据研发的基本概念](docs/backend/后端技术杂谈:捋一捋大数据研发的基本概念.md) - -# 分布式 -## 分布式理论 -* [分布式系统理论基础:一致性PC和PC ](docs/distributed/basic/分布式系统理论基础:一致性PC和PC.md) -* [分布式系统理论基础:CAP ](docs/distributed/basic/分布式系统理论基础:CAP.md) -* [分布式系统理论基础:时间时钟和事件顺序](docs/distributed/basic/分布式系统理论基础:时间时钟和事件顺序.md) -* [分布式系统理论基础:Paxos](docs/distributed/basic/分布式系统理论基础:Paxos.md) -* [分布式系统理论基础:选举多数派和租约](docs/distributed/basic/分布式系统理论基础:选举多数派和租约.md) -* [分布式系统理论基础:RaftZab ](docs/distributed/basic/分布式系统理论基础:RaftZab.md) -* [分布式系统理论进阶:Paxos变种和优化 ](docs/distributed/basic/分布式系统理论进阶:Paxos变种和优化.md) -* [分布式系统理论基础:zookeeper分布式协调服务 ](docs/distributed/basic/分布式系统理论基础:zookeeper分布式协调服务.md) -* [分布式理论总结](docs/distributed/分布式技术实践总结.md) - -## 分布式技术 -* [搞懂分布式技术:分布式系统的一些基本概念](docs/distributed/practice/搞懂分布式技术:分布式系统的一些基本概念.md ) -* [搞懂分布式技术:分布式一致性协议与Paxos,Raft算法](docs/distributed/practice/搞懂分布式技术:分布式一致性协议与Paxos,Raft算法.md) -* [搞懂分布式技术:初探分布式协调服务zookeeper](docs/distributed/practice/搞懂分布式技术:初探分布式协调服务zookeeper.md ) -* [搞懂分布式技术:ZAB协议概述与选主流程详解](docs/distributed/practice/搞懂分布式技术:ZAB协议概述与选主流程详解.md ) -* [搞懂分布式技术:Zookeeper的配置与集群管理实战](docs/distributed/practice/搞懂分布式技术:Zookeeper的配置与集群管理实战.md) -* [搞懂分布式技术:Zookeeper典型应用场景及实践](docs/distributed/practice/搞懂分布式技术:Zookeeper典型应用场景及实践.md ) -* [搞懂分布式技术:LVS实现负载均衡的原理与实践 ](docs/distributed/practice/搞懂分布式技术:LVS实现负载均衡的原理与实践.md ) -* [搞懂分布式技术:分布式session解决方案与一致性hash](docs/distributed/practice/搞懂分布式技术:分布式session解决方案与一致性hash.md) -* [搞懂分布式技术:分布式ID生成方案 ](docs/distributed/practice/搞懂分布式技术:分布式ID生成方案.md ) -* [搞懂分布式技术:缓存的那些事](docs/distributed/practice/搞懂分布式技术:缓存的那些事.md) -* [搞懂分布式技术:SpringBoot使用注解集成Redis缓存](docs/distributed/practice/搞懂分布式技术:SpringBoot使用注解集成Redis缓存.md) -* [搞懂分布式技术:缓存更新的套路 ](docs/distributed/practice/搞懂分布式技术:缓存更新的套路.md ) -* [搞懂分布式技术:浅谈分布式锁的几种方案 ](docs/distributed/practice/搞懂分布式技术:浅谈分布式锁的几种方案.md ) -* [搞懂分布式技术:浅析分布式事务](docs/distributed/practice/搞懂分布式技术:浅析分布式事务.md ) -* [搞懂分布式技术:分布式事务常用解决方案 ](docs/distributed/practice/搞懂分布式技术:分布式事务常用解决方案.md ) -* [搞懂分布式技术:使用RocketMQ事务消息解决分布式事务 ](docs/distributed/practice/搞懂分布式技术:使用RocketMQ事务消息解决分布式事务.md ) -* [搞懂分布式技术:消息队列因何而生](docs/distributed/practice/搞懂分布式技术:消息队列因何而生.md) -* [搞懂分布式技术:浅谈分布式消息技术Kafka](docs/distributed/practice/搞懂分布式技术:浅谈分布式消息技术Kafka.md ) -* [分布式技术实践总结](docs/distributed/分布式理论总结.md) - -# 面试指南 - -todo -## 校招指南 -todo - -## 面经 -todo - -# 工具 -todo - -# 资料 -todo - -## 书单 -todo - -# 待办 -springboot和springcloud - -# 微信公众号 - -## Java技术江湖 -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】 -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/Javatutorial.jpeg) diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232GC\350\260\203\344\274\230\346\200\235\350\267\257\344\270\216\345\270\270\347\224\250\345\267\245\345\205\267.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232GC\350\260\203\344\274\230\346\200\235\350\267\257\344\270\216\345\270\270\347\224\250\345\267\245\345\205\267.md" deleted file mode 100644 index ceda305..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232GC\350\260\203\344\274\230\346\200\235\350\267\257\344\270\216\345\270\270\347\224\250\345\267\245\345\205\267.md" +++ /dev/null @@ -1,838 +0,0 @@ -# 目录 - -* [核心概念(Core Concepts)](#核心概念core-concepts) - * [Latency(延迟)](#latency延迟) - * [Throughput(吞吐量)](#throughput吞吐量) - * [Capacity(系统容量)](#capacity系统容量) -* [相关示例](#相关示例) - * [Tuning for Latency(调优延迟指标)](#tuning-for-latency调优延迟指标) - * [Tuning for Throughput(吞吐量调优)](#tuning-for-throughput吞吐量调优) - * [Tuning for Capacity(调优系统容量)](#tuning-for-capacity调优系统容量) - * [6\. GC 调优(工具篇) - GC参考手册](#6-gc-调优工具篇---gc参考手册) - * [JMX API](#jmx-api) - * [JVisualVM](#jvisualvm) - * [jstat](#jstat) - * [GC日志(GC logs)](#gc日志gc-logs) - * [GCViewer](#gcviewer) - * [分析器(Profilers)](#分析器profilers) - * [hprof](#hprof) - * [Java VisualVM](#java-visualvm) - * [AProf](#aprof) - * [参考文章](#参考文章) - - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -> **说明**: -> -> **Capacity**: 性能,能力,系统容量; 文中翻译为”**系统容量**“; 意为硬件配置。 - -您应该已经阅读了前面的章节: - -1. 垃圾收集简介 - GC参考手册 -2. Java中的垃圾收集 - GC参考手册 -3. GC 算法(基础篇) - GC参考手册 -4. GC 算法(实现篇) - GC参考手册 - -GC调优(Tuning Garbage Collection)和其他性能调优是同样的原理。初学者可能会被 200 多个 GC参数弄得一头雾水, 然后随便调整几个来试试结果,又或者修改几行代码来测试。其实只要参照下面的步骤,就能保证你的调优方向正确: - -1. 列出性能调优指标(State your performance goals) -2. 执行测试(Run tests) -3. 检查结果(Measure the results) -4. 与目标进行对比(Compare the results with the goals) -5. 如果达不到指标, 修改配置参数, 然后继续测试(go back to running tests) - -第一步, 我们需要做的事情就是: 制定明确的GC性能指标。对所有性能监控和管理来说, 有三个维度是通用的: - -* Latency(延迟) -* Throughput(吞吐量) -* Capacity(系统容量) - -我们先讲解基本概念,然后再演示如何使用这些指标。如果您对 延迟、吞吐量和系统容量等概念很熟悉, 可以跳过这一小节。 - -### 核心概念(Core Concepts) - -我们先来看一家工厂的装配流水线。工人在流水线将现成的组件按顺序拼接,组装成自行车。通过实地观测, 我们发现从组件进入生产线,到另一端组装成自行车需要4小时。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224347.png) -继续观察,我们还发现,此后每分钟就有1辆自行车完成组装, 每天24小时,一直如此。将这个模型简化, 并忽略维护窗口期后得出结论:**这条流水线每小时可以组装60辆自行车**。 - -> **说明**: 时间窗口/窗口期,请类比车站卖票的窗口,是一段规定/限定做某件事的时间段。 - -通过这两种测量方法, 就知道了生产线的相关性能信息:**延迟**与**吞吐量**: - -* 生产线的延迟:**4小时** -* 生产线的吞吐量:**60辆/小时** - -请注意, 衡量延迟的时间单位根据具体需要而确定 —— 从纳秒(nanosecond)到几千年(millennia)都有可能。系统的吞吐量是每个单位时间内完成的操作。操作(Operations)一般是特定系统相关的东西。在本例中,选择的时间单位是小时, 操作就是对自行车的组装。 - -掌握了延迟和吞吐量两个概念之后, 让我们对这个工厂来进行实际的调优。自行车的需求在一段时间内都很稳定, 生产线组装自行车有四个小时延迟, 而吞吐量在几个月以来都很稳定: 60辆/小时。假设某个销售团队突然业绩暴涨, 对自行车的需求增加了1倍。客户每天需要的自行车不再是 60 * 24 = 1440辆, 而是 2*1440 = 2880辆/天。老板对工厂的产能不满意,想要做些调整以提升产能。 - -看起来总经理很容易得出正确的判断, 系统的延迟没法子进行处理 —— 他关注的是每天的自行车生产总量。得出这个结论以后, 假若工厂资金充足, 那么应该立即采取措施, 改善吞吐量以增加产能。 - -我们很快会看到, 这家工厂有两条相同的生产线。每条生产线一分钟可以组装一辆成品自行车。 可以想象,每天生产的自行车数量会增加一倍。达到 2880辆/天。要注意的是, 不需要减少自行车的装配时间 —— 从开始到结束依然需要 4 小时。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224359.png) - -巧合的是,这样进行的性能优化,同时增加了吞吐量和产能。一般来说,我们会先测量当前的系统性能, 再设定新目标, 只优化系统的某个方面来满足性能指标。 - -在这里做了一个很重要的决定 —— 要增加吞吐量,而不是减小延迟。在增加吞吐量的同时, 也需要增加系统容量。比起原来的情况, 现在需要两条流水线来生产出所需的自行车。在这种情况下, 增加系统的吞吐量并不是免费的, 需要水平扩展, 以满足增加的吞吐量需求。 - -在处理性能问题时, 应该考虑到还有另一种看似不相关的解决办法。假如生产线的延迟从1分钟降低为30秒,那么吞吐量同样可以增长 1 倍。 - -或者是降低延迟, 或者是客户非常有钱。软件工程里有一种相似的说法 —— 每个性能问题背后,总有两种不同的解决办法。 可以用更多的机器, 或者是花精力来改善性能低下的代码。 - -#### Latency(延迟) - -GC的延迟指标由一般的延迟需求决定。延迟指标通常如下所述: - -* 所有交易必须在10秒内得到响应 -* 90%的订单付款操作必须在3秒以内处理完成 -* 推荐商品必须在 100 ms 内展示到用户面前 - -面对这类性能指标时, 需要确保在交易过程中, GC暂停不能占用太多时间,否则就满足不了指标。“不能占用太多” 的意思需要视具体情况而定, 还要考虑到其他因素, 比如外部数据源的交互时间(round-trips), 锁竞争(lock contention), 以及其他的安全点等等。 - -假设性能需求为:`90%`的交易要在`1000ms`以内完成, 每次交易最长不能超过`10秒`。 根据经验, 假设GC暂停时间比例不能超过10%。 也就是说, 90%的GC暂停必须在`100ms`内结束, 也不能有超过`1000ms`的GC暂停。为简单起见, 我们忽略在同一次交易过程中发生多次GC停顿的可能性。 - -有了正式的需求,下一步就是检查暂停时间。有许多工具可以使用, 在接下来的6\. GC 调优(工具篇) - - - -``` -2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics) - [PSYoungGen: 93677K->70109K(254976K)] - [ParOldGen: 499597K->511230K(761856K)] - 593275K->581339K(1016832K), - [Metaspace: 2936K->2936K(1056768K)] - , 0.0713174 secs] - [Times: user=0.21 sys=0.02, real=0.07 secs -``` - - - -这表示一次GC暂停, 在`2015-06-04T13:34:16`这个时刻触发. 对应于JVM启动之后的`2,578 ms`。 - -此事件将应用线程暂停了`0.0713174`秒。虽然花费的总时间为 210 ms, 但因为是多核CPU机器, 所以最重要的数字是应用线程被暂停的总时间, 这里使用的是并行GC, 所以暂停时间大约为`70ms`。 这次GC的暂停时间小于`100ms`的阈值,满足需求。 - -继续分析, 从所有GC日志中提取出暂停相关的数据, 汇总之后就可以得知是否满足需求。 - -#### Throughput(吞吐量) - -吞吐量和延迟指标有很大区别。当然两者都是根据一般吞吐量需求而得出的。一般吞吐量需求(Generic requirements for throughput) 类似这样: - -* 解决方案每天必须处理 100万个订单 -* 解决方案必须支持1000个登录用户,同时在5-10秒内执行某个操作: A、B或C -* 每周对所有客户进行统计, 时间不能超过6小时,时间窗口为每周日晚12点到次日6点之间。 - -可以看出,吞吐量需求不是针对单个操作的, 而是在给定的时间内, 系统必须完成多少个操作。和延迟需求类似, GC调优也需要确定GC行为所消耗的总时间。每个系统能接受的时间不同, 一般来说, GC占用的总时间比不能超过`10%`。 - -现在假设需求为: 每分钟处理 1000 笔交易。同时, 每分钟GC暂停的总时间不能超过6秒(即10%)。 - -有了正式的需求, 下一步就是获取相关的信息。依然是从GC日志中提取数据, 可以看到类似这样的信息: - - - -``` -2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics) - [PSYoungGen: 93677K->70109K(254976K)] - [ParOldGen: 499597K->511230K(761856K)] - 593275K->581339K(1016832K), - [Metaspace: 2936K->2936K(1056768K)], - 0.0713174 secs] - [Times: user=0.21 sys=0.02, real=0.07 secs -``` - - - -此时我们对 用户耗时(user)和系统耗时(sys)感兴趣, 而不关心实际耗时(real)。在这里, 我们关心的时间为`0.23s`(user + sys = 0.21 + 0.02 s), 这段时间内, GC暂停占用了 cpu 资源。 重要的是, 系统运行在多核机器上, 转换为实际的停顿时间(stop-the-world)为`0.0713174秒`, 下面的计算会用到这个数字。 - -提取出有用的信息后, 剩下要做的就是统计每分钟内GC暂停的总时间。看看是否满足需求: 每分钟内总的暂停时间不得超过6000毫秒(6秒)。 - -#### Capacity(系统容量) - -系统容量(Capacity)需求,是在达成吞吐量和延迟指标的情况下,对硬件环境的额外约束。这类需求大多是来源于计算资源或者预算方面的原因。例如: - -* 系统必须能部署到小于512 MB内存的Android设备上 -* 系统必须部署在Amazon**EC2**实例上, 配置不得超过**c3.xlarge(4核8GB)**。 -* 每月的 Amazon EC2 账单不得超过`$12,000` - -因此, 在满足延迟和吞吐量需求的基础上必须考虑系统容量。可以说, 假若有无限的计算资源可供挥霍, 那么任何 延迟和吞吐量指标 都不成问题, 但现实情况是, 预算(budget)和其他约束限制了可用的资源。 - -### 相关示例 - -介绍完性能调优的三个维度后, 我们来进行实际的操作以达成GC性能指标。 - -请看下面的代码: - - - -``` -//imports skipped for brevity -public class Producer implements Runnable { - - private static ScheduledExecutorService executorService - = Executors.newScheduledThreadPool(2); - - private Deque deque; - private int objectSize; - private int queueSize; - - public Producer(int objectSize, int ttl) { - this.deque = new ArrayDeque(); - this.objectSize = objectSize; - this.queueSize = ttl * 1000; - } - - @Override - public void run() { - for (int i = 0; i < 100; i++) { - deque.add(new byte[objectSize]); - if (deque.size() > queueSize) { - deque.poll(); - } - } - } - - public static void main(String[] args) - throws InterruptedException { - executorService.scheduleAtFixedRate( - new Producer(200 * 1024 * 1024 / 1000, 5), - 0, 100, TimeUnit.MILLISECONDS - ); - executorService.scheduleAtFixedRate( - new Producer(50 * 1024 * 1024 / 1000, 120), - 0, 100, TimeUnit.MILLISECONDS); - TimeUnit.MINUTES.sleep(10); - executorService.shutdownNow(); - } -} -``` - - - -这段程序代码, 每 100毫秒 提交两个作业(job)来。每个作业都模拟特定的生命周期: 创建对象, 然后在预定的时间释放, 接着就不管了, 由GC来自动回收占用的内存。 - -在运行这个示例程序时,通过以下JVM参数打开GC日志记录: - - - -``` --XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -``` - - - -还应该加上JVM参数`-Xloggc`以指定GC日志的存储位置,类似这样: - - - -``` --Xloggc:C:\\Producer_gc.log -``` - - - -* 1 -* 2 - -在日志文件中可以看到GC的行为, 类似下面这样: - - - -``` -2015-06-04T13:34:16.119-0200: 1.723: [GC (Allocation Failure) - [PSYoungGen: 114016K->73191K(234496K)] - 421540K->421269K(745984K), - 0.0858176 secs] - [Times: user=0.04 sys=0.06, real=0.09 secs] - -2015-06-04T13:34:16.738-0200: 2.342: [GC (Allocation Failure) - [PSYoungGen: 234462K->93677K(254976K)] - 582540K->593275K(766464K), - 0.2357086 secs] - [Times: user=0.11 sys=0.14, real=0.24 secs] - -2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics) - [PSYoungGen: 93677K->70109K(254976K)] - [ParOldGen: 499597K->511230K(761856K)] - 593275K->581339K(1016832K), - [Metaspace: 2936K->2936K(1056768K)], - 0.0713174 secs] - [Times: user=0.21 sys=0.02, real=0.07 secs] -``` - - - -基于日志中的信息, 可以通过三个优化目标来提升性能: - -1. 确保最坏情况下,GC暂停时间不超过预定阀值 -2. 确保线程暂停的总时间不超过预定阀值 -3. 在确保达到延迟和吞吐量指标的情况下, 降低硬件配置以及成本。 - -为此, 用三种不同的配置, 将代码运行10分钟, 得到了三种不同的结果, 汇总如下: - - -| **堆内存大小(Heap)** | **GC算法(GC Algorithm)** | **有效时间比(Useful work)** | **最长停顿时间(Longest pause)** | -| -Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | **560 ms** | -| -Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms | -| -Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms | - -使用不同的GC算法,和不同的内存配置,运行相同的代码, 以测量GC暂停时间与 延迟、吞吐量的关系。实验的细节和结果在后面章节详细介绍。 - -注意, 为了尽量简单, 示例中只改变了很少的输入参数, 此实验也没有在不同CPU数量或者不同的堆布局下进行测试。 - -#### Tuning for Latency(调优延迟指标) - -假设有一个需求,**每次作业必须在 1000ms 内处理完成**。我们知道, 实际的作业处理只需要100 ms,简化后, 两者相减就可以算出对 GC暂停的延迟要求。现在需求变成:**GC暂停不能超过900ms**。这个问题很容易找到答案, 只需要解析GC日志文件, 并找出GC暂停中最大的那个暂停时间即可。 - -再来看测试所用的三个配置: - - -| **堆内存大小(Heap)** | **GC算法(GC Algorithm)** | **有效时间比(Useful work)** | **最长停顿时间(Longest pause)** | -| -Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | **560 ms** | -| -Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms | -| -Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms | - -可以看到,其中有一个配置达到了要求。运行的参数为: - - - -``` -java -Xmx12g -XX:+UseConcMarkSweepGC Producer -``` - - - -对应的GC日志中,暂停时间最大为`560 ms`, 这达到了延迟指标`900 ms`的要求。如果还满足吞吐量和系统容量需求的话,就可以说成功达成了GC调优目标, 调优结束。 - -#### Tuning for Throughput(吞吐量调优) - -假定吞吐量指标为:**每小时完成 1300万次操作处理**。同样是上面的配置, 其中有一种配置满足了需求: - - -| **堆内存大小(Heap)** | **GC算法(GC Algorithm)** | **有效时间比(Useful work)** | **最长停顿时间(Longest pause)** | -| -Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | 560 ms | -| -Xmx12g | -XX:+UseParallelGC | **91.5%** | 1,104 ms | -| -Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms | - -此配置对应的命令行参数为: - - - -``` -java -Xmx12g -XX:+UseParallelGC Producer -``` - - - -* 可以看到,GC占用了 8.5%的CPU时间,剩下的`91.5%`是有效的计算时间。为简单起见, 忽略示例中的其他安全点。现在需要考虑: - -1. 每个CPU核心处理一次作业需要耗时`100ms` -2. 因此, 一分钟内每个核心可以执行 60,000 次操作(**每个job完成100次操作**) -3. 一小时内, 一个核心可以执行 360万次操作 -4. 有四个CPU内核, 则每小时可以执行: 4 x 3.6M = 1440万次操作 - -理论上,通过简单的计算就可以得出结论, 每小时可以执行的操作数为:`14.4 M * 91.5% = 13,176,000`次, 满足需求。 - -值得一提的是, 假若还要满足延迟指标, 那就有问题了, 最坏情况下, GC暂停时间为`1,104 ms`, 最大延迟时间是前一种配置的两倍。 - -#### Tuning for Capacity(调优系统容量) - -假设需要将软件部署到服务器上(commodity-class hardware), 配置为`4核10G`。这样的话, 系统容量的要求就变成: 最大的堆内存空间不能超过`8GB`。有了这个需求, 我们需要调整为第三套配置进行测试: - - -| **堆内存大小(Heap)** | **GC算法(GC Algorithm)** | **有效时间比(Useful work)** | **最长停顿时间(Longest pause)** | -| -Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | 560 ms | -| -Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms | -| **-Xmx8g** | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms | - -程序可以通过如下参数执行: - - - -``` -java -Xmx8g -XX:+UseConcMarkSweepGC Producer -``` - - - -* 测试结果是延迟大幅增长, 吞吐量同样大幅降低: -* 现在,GC占用了更多的CPU资源, 这个配置只有`66.3%`的有效CPU时间。因此,这个配置让吞吐量从最好的情况**13,176,000 操作/小时**下降到**不足 9,547,200次操作/小时**. -* 最坏情况下的延迟变成了**1,610 ms**, 而不再是**560ms**。 - -通过对这三个维度的介绍, 你应该了解, 不是简单的进行“性能(performance)”优化, 而是需要从三种不同的维度来进行考虑, 测量, 并调优延迟和吞吐量, 此外还需要考虑系统容量的约束。 - -请继续阅读下一章:6\. GC 调优(工具篇) - GC参考手册 - -原文链接:[GC Tuning: Basics](https://plumbr.eu/handbook/gc-tuning) - -翻译时间: 2016年02月06日 - -## 6\. GC 调优(工具篇) - GC参考手册 - -2017年02月23日 18:56:02 - -阅读数:6469 - -进行GC性能调优时, 需要明确了解, 当前的GC行为对系统和用户有多大的影响。有多种监控GC的工具和方法, 本章将逐一介绍常用的工具。 - -您应该已经阅读了前面的章节: - -1. 垃圾收集简介 - GC参考手册 -2. Java中的垃圾收集 - GC参考手册 -3. GC 算法(基础篇) - GC参考手册 -4. GC 算法(实现篇) - GC参考手册 -5. GC 调优(基础篇) - GC参考手册 - -JVM 在程序执行的过程中, 提供了GC行为的原生数据。那么, 我们就可以利用这些原生数据来生成各种报告。原生数据(_raw data_) 包括: - -* 各个内存池的当前使用情况, -* 各个内存池的总容量, -* 每次GC暂停的持续时间, -* GC暂停在各个阶段的持续时间。 - -可以通过这些数据算出各种指标, 例如: 程序的内存分配率, 提升率等等。本章主要介绍如何获取原生数据。 后续的章节将对重要的派生指标(derived metrics)展开讨论, 并引入GC性能相关的话题。 - -## JMX API - -从 JVM 运行时获取GC行为数据, 最简单的办法是使用标准[JMX API 接口](https://docs.oracle.com/javase/tutorial/jmx/index.html). JMX是获取 JVM内部运行时状态信息 的标准API. 可以编写程序代码, 通过 JMX API 来访问本程序所在的JVM,也可以通过JMX客户端执行(远程)访问。 - -最常见的 JMX客户端是[JConsole](http://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html)和[JVisualVM](http://docs.oracle.com/javase/7/docs/technotes/tools/share/jvisualvm.html)(可以安装各种插件,十分强大)。两个工具都是标准JDK的一部分, 而且很容易使用. 如果使用的是 JDK 7u40 及更高版本, 还可以使用另一个工具:[Java Mission Control](http://www.oracle.com/technetwork/java/javaseproducts/mission-control/java-mission-control-1998576.html)( 大致翻译为 Java控制中心,`jmc.exe`)。 - -> JVisualVM安装MBeans插件的步骤: 通过 工具(T) – 插件(G) – 可用插件 – 勾选VisualVM-MBeans – 安装 – 下一步 – 等待安装完成…… 其他插件的安装过程基本一致。 - -所有 JMX客户端都是独立的程序,可以连接到目标JVM上。目标JVM可以在本机, 也可能是远端JVM. 如果要连接远端JVM, 则目标JVM启动时必须指定特定的环境变量,以开启远程JMX连接/以及端口号。 示例如下: - - - -``` -java -Dcom.sun.management.jmxremote.port=5432 com.yourcompany.YourApp -``` - - - -在此处, JVM 打开端口`5432`以支持JMX连接。 - -通过 JVisualVM 连接到某个JVM以后, 切换到 MBeans 标签, 展开 “java.lang/GarbageCollector” . 就可以看到GC行为信息, 下图是 JVisualVM 中的截图: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224430.png) - -下图是Java Mission Control 中的截图: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224439.png) - -从以上截图中可以看到两款垃圾收集器。其中一款负责清理年轻代(**PS Scavenge**),另一款负责清理老年代(**PS MarkSweep**); 列表中显示的就是垃圾收集器的名称。可以看到 , jmc 的功能和展示数据的方式更强大。 - -对所有的垃圾收集器, 通过 JMX API 获取的信息包括: - -* **CollectionCount**: 垃圾收集器执行的GC总次数, -* **CollectionTime**: 收集器运行时间的累计。这个值等于所有GC事件持续时间的总和, -* **LastGcInfo**: 最近一次GC事件的详细信息。包括 GC事件的持续时间(duration), 开始时间(startTime) 和 结束时间(endTime), 以及各个内存池在最近一次GC之前和之后的使用情况, -* **MemoryPoolNames**: 各个内存池的名称, -* **Name**: 垃圾收集器的名称 -* **ObjectName**: 由JMX规范定义的 MBean的名字,, -* **Valid**: 此收集器是否有效。本人只见过 “`true`“的情况 (^_^) - -根据经验, 这些信息对GC的性能来说,不能得出什么结论. 只有编写程序, 获取GC相关的 JMX 信息来进行统计和分析。 在下文可以看到, 一般也不怎么关注 MBean , 但 MBean 对于理解GC的原理倒是挺有用的。 - -## JVisualVM - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224451.png) - -Visual GC 插件常用来监控本机运行的Java程序, 比如开发者和性能调优专家经常会使用此插件, 以快速获取程序运行时的GC信息。 - -![06_03_jvmsualvm-garbage-collection-monitoring.png](https://s4.51cto.com/images/blog/202106/25/eabd68ba262d004c4919475f00d8ec9c.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk= "06_03_jvmsualvm-garbage-collection-monitoring.png") - -左侧的图表展示了各个内存池的使用情况: Metaspace/永久代, 老年代, Eden区以及两个存活区。 - -在右边, 顶部的两个图表与 GC无关, 显示的是 JIT编译时间 和 类加载时间。下面的6个图显示的是内存池的历史记录, 每个内存池的GC次数,GC总时间, 以及最大值,峰值, 当前使用情况。 - -再下面是 HistoGram, 显示了年轻代对象的年龄分布。至于对象的年龄监控(objects tenuring monitoring), 本章不进行讲解。 - -与纯粹的JMX工具相比, VisualGC 插件提供了更友好的界面, 如果没有其他趁手的工具, 请选择VisualGC. 本章接下来会介绍其他工具, 这些工具可以提供更多的信息, 以及更好的视角. 当然, 在“Profilers(分析器)”一节中,也会介绍 JVisualVM 的适用场景 —— 如: 分配分析(allocation profiling), 所以我们绝不会贬低哪一款工具, 关键还得看实际情况。 - -## jstat - -[jstat](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html)也是标准JDK提供的一款监控工具(Java Virtual Machine statistics monitoring tool),可以统计各种指标。既可以连接到本地JVM,也可以连到远程JVM. 查看支持的指标和对应选项可以执行 “`jstat -options`” 。例如: - - - -``` -+-----------------+---------------------------------------------------------------+ -| Option | Displays... | -+-----------------+---------------------------------------------------------------+ -|class | Statistics on the behavior of the class loader | -|compiler | Statistics on the behavior of the HotSpot Just-In-Time com- | -| | piler | -|gc | Statistics on the behavior of the garbage collected heap | -|gccapacity | Statistics of the capacities of the generations and their | -| | corresponding spaces. | -|gccause | Summary of garbage collection statistics (same as -gcutil), | -| | with the cause of the last and current (if applicable) | -| | garbage collection events. | -|gcnew | Statistics of the behavior of the new generation. | -|gcnewcapacity | Statistics of the sizes of the new generations and its corre- | -| | sponding spaces. | -|gcold | Statistics of the behavior of the old and permanent genera- | -| | tions. | -|gcoldcapacity | Statistics of the sizes of the old generation. | -|gcpermcapacity | Statistics of the sizes of the permanent generation. | -|gcutil | Summary of garbage collection statistics. | -|printcompilation | Summary of garbage collection statistics. | -+-----------------+---------------------------------------------------------------+ -``` - - - -* jstat 对于快速确定GC行为是否健康非常有用。启动方式为: “`jstat -gc -t PID 1s`” , 其中,PID 就是要监视的Java进程ID。可以通过`jps`命令查看正在运行的Java进程列表。 - - - -``` -jps - -jstat -gc -t 2428 1s -``` - - - -以上命令的结果, 是 jstat 每秒向标准输出输出一行新内容, 比如: - - - -``` -Timestamp S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT -200.0 8448.0 8448.0 8448.0 0.0 67712.0 67712.0 169344.0 169344.0 21248.0 20534.3 3072.0 2807.7 34 0.720 658 133.684 134.404 -201.0 8448.0 8448.0 8448.0 0.0 67712.0 67712.0 169344.0 169343.2 21248.0 20534.3 3072.0 2807.7 34 0.720 662 134.712 135.432 -202.0 8448.0 8448.0 8102.5 0.0 67712.0 67598.5 169344.0 169343.6 21248.0 20534.3 3072.0 2807.7 34 0.720 667 135.840 136.559 -203.0 8448.0 8448.0 8126.3 0.0 67712.0 67702.2 169344.0 169343.6 21248.0 20547.2 3072.0 2807.7 34 0.720 669 136.178 136.898 -204.0 8448.0 8448.0 8126.3 0.0 67712.0 67702.2 169344.0 169343.6 21248.0 20547.2 3072.0 2807.7 34 0.720 669 136.178 136.898 -205.0 8448.0 8448.0 8134.6 0.0 67712.0 67712.0 169344.0 169343.5 21248.0 20547.2 3072.0 2807.7 34 0.720 671 136.234 136.954 -206.0 8448.0 8448.0 8134.6 0.0 67712.0 67712.0 169344.0 169343.5 21248.0 20547.2 3072.0 2807.7 34 0.720 671 136.234 136.954 -207.0 8448.0 8448.0 8154.8 0.0 67712.0 67712.0 169344.0 169343.5 21248.0 20547.2 3072.0 2807.7 34 0.720 673 136.289 137.009 -208.0 8448.0 8448.0 8154.8 0.0 67712.0 67712.0 169344.0 169343.5 21248.0 20547.2 3072.0 2807.7 34 0.720 673 136.289 137.009 -``` - - - -稍微解释一下上面的内容。参考[jstat manpage](http://www.manpagez.com/man/1/jstat/), 我们可以知道: - -* jstat 连接到 JVM 的时间, 是JVM启动后的 200秒。此信息从第一行的 “**Timestamp**” 列得知。继续看下一行, jstat 每秒钟从JVM 接收一次信息, 也就是命令行参数中 “`1s`” 的含义。 -* 从第一行的 “**YGC**” 列得知年轻代共执行了34次GC, 由 “**FGC**” 列得知整个堆内存已经执行了 658次 full GC。 -* 年轻代的GC耗时总共为`0.720 秒`, 显示在“**YGCT**” 这一列。 -* Full GC 的总计耗时为`133.684 秒`, 由“**FGCT**”列得知。 这立马就吸引了我们的目光, 总的JVM 运行时间只有 200 秒,**但其中有 66% 的部分被 Full GC 消耗了**。 - -再看下一行, 问题就更明显了。 - -* 在接下来的一秒内共执行了 4 次 Full GC。参见 “**FGC**” 列. -* 这4次 Full GC 暂停占用了差不多 1秒的时间(根据**FGCT**列的差得知)。与第一行相比, Full GC 耗费了`928 毫秒`, 即`92.8%`的时间。 -* 根据 “**OC**和 “**OU**” 列得知,**整个老年代的空间**为`169,344.0 KB`(“OC“), 在 4 次 Full GC 后依然占用了`169,344.2 KB`(“OU“)。用了`928ms`的时间却只释放了 800 字节的内存, 怎么看都觉得很不正常。 - -只看这两行的内容, 就知道程序出了很严重的问题。继续分析下一行, 可以确定问题依然存在,而且变得更糟。 - -JVM几乎完全卡住了(stalled), 因为GC占用了90%以上的计算资源。GC之后, 所有的老代空间仍然还在占用。事实上, 程序在一分钟以后就挂了, 抛出了 “[java.lang.OutOfMemoryError: GC overhead limit exceeded](https://plumbr.eu/outofmemoryerror/gc-overhead-limit-exceeded)” 错误。 - -可以看到, 通过 jstat 能很快发现对JVM健康极为不利的GC行为。一般来说, 只看 jstat 的输出就能快速发现以下问题: - -* 最后一列 “**GCT**”, 与JVM的总运行时间 “**Timestamp**” 的比值, 就是GC 的开销。如果每一秒内, “**GCT**” 的值都会明显增大, 与总运行时间相比, 就暴露出GC开销过大的问题. 不同系统对GC开销有不同的容忍度, 由性能需求决定, 一般来讲, 超过`10%`的GC开销都是有问题的。 -* “**YGC**” 和 “**FGC**” 列的快速变化往往也是有问题的征兆。频繁的GC暂停会累积,并导致更多的线程停顿(stop-the-world pauses), 进而影响吞吐量。 -* 如果看到 “**OU**” 列中,老年代的使用量约等于老年代的最大容量(**OC**), 并且不降低的话, 就表示虽然执行了老年代GC, 但基本上属于无效GC。 - -## GC日志(GC logs) - -通过日志内容也可以得到GC相关的信息。因为GC日志模块内置于JVM中, 所以日志中包含了对GC活动最全面的描述。 这就是事实上的标准, 可作为GC性能评估和优化的最真实数据来源。 - -GC日志一般输出到文件之中, 是纯 text 格式的, 当然也可以打印到控制台。有多个可以控制GC日志的JVM参数。例如,可以打印每次GC的持续时间, 以及程序暂停时间(`-XX:+PrintGCApplicationStoppedTime`), 还有GC清理了多少引用类型(`-XX:+PrintReferenceGC`)。 - -要打印GC日志, 需要在启动脚本中指定以下参数: - - - -``` --XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc: -``` - - - -以上参数指示JVM: 将所有GC事件打印到日志文件中, 输出每次GC的日期和时间戳。不同GC算法输出的内容略有不同. ParallelGC 输出的日志类似这样: - - - -``` -199.879: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1473386 secs] [Times: user=0.43 sys=0.01, real=0.15 secs] -200.027: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1567794 secs] [Times: user=0.41 sys=0.00, real=0.16 secs] -200.184: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1621946 secs] [Times: user=0.43 sys=0.00, real=0.16 secs] -200.346: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1547695 secs] [Times: user=0.41 sys=0.00, real=0.15 secs] -200.502: [Full GC (Ergonomics) [PSYoungGen: 64000K->63999K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1563071 secs] [Times: user=0.42 sys=0.01, real=0.16 secs] -200.659: [Full GC (Ergonomics) [PSYoungGen: 64000K->63999K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1538778 secs] [Times: user=0.42 sys=0.00, real=0.16 secs] -``` - - - -在 “04\. GC算法:实现篇” 中详细介绍了这些格式, 如果对此不了解, 可以先阅读该章节。 - -分析以上日志内容, 可以得知: - -* 这部分日志截取自JVM启动后200秒左右。 -* 日志片段中显示, 在`780毫秒`以内, 因为垃圾回收 导致了5次 Full GC 暂停(去掉第六次暂停,这样更精确一些)。 -* 这些暂停事件的总持续时间是`777毫秒`, 占总运行时间的**99.6%**。 -* 在GC完成之后, 几乎所有的老年代空间(`169,472 KB`)依然被占用(`169,318 KB`)。 - -通过日志信息可以确定, 该应用的GC情况非常糟糕。JVM几乎完全停滞, 因为GC占用了超过`99%`的CPU时间。 而GC的结果是, 老年代空间仍然被占满, 这进一步肯定了我们的结论。 示例程序和jstat 小节中的是同一个, 几分钟之后系统就挂了, 抛出 “[java.lang.OutOfMemoryError: GC overhead limit exceeded](https://plumbr.eu/outofmemoryerror/gc-overhead-limit-exceeded)” 错误, 不用说, 问题是很严重的. - -从此示例可以看出, GC日志对监控GC行为和JVM是否处于健康状态非常有用。一般情况下, 查看 GC 日志就可以快速确定以下症状: - -* GC开销太大。如果GC暂停的总时间很长, 就会损害系统的吞吐量。不同的系统允许不同比例的GC开销, 但一般认为, 正常范围在`10%`以内。 -* 极个别的GC事件暂停时间过长。当某次GC暂停时间太长, 就会影响系统的延迟指标. 如果延迟指标规定交易必须在`1,000 ms`内完成, 那就不能容忍任何超过`1000毫秒`的GC暂停。 -* 老年代的使用量超过限制。如果老年代空间在 Full GC 之后仍然接近全满, 那么GC就成为了性能瓶颈, 可能是内存太小, 也可能是存在内存泄漏。这种症状会让GC的开销暴增。 - -可以看到,GC日志中的信息非常详细。但除了这些简单的小程序, 生产系统一般都会生成大量的GC日志, 纯靠人工是很难阅读和进行解析的。 - -## GCViewer - -我们可以自己编写解析器, 来将庞大的GC日志解析为直观易读的图形信息。 但很多时候自己写程序也不是个好办法, 因为各种GC算法的复杂性, 导致日志信息格式互相之间不太兼容。那么神器来了:[GCViewer](https://github.com/chewiebug/GCViewer)。 - -[GCViewer](https://github.com/chewiebug/GCViewer)是一款开源的GC日志分析工具。项目的 GitHub 主页对各项指标进行了完整的描述. 下面我们介绍最常用的一些指标。 - -第一步是获取GC日志文件。这些日志文件要能够反映系统在性能调优时的具体场景. 假若运营部门(operational department)反馈: 每周五下午,系统就运行缓慢, 不管GC是不是主要原因, 分析周一早晨的日志是没有多少意义的。 - -获取到日志文件之后, 就可以用 GCViewer 进行分析, 大致会看到类似下面的图形界面: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224526.png) - -使用的命令行大致如下: - - - -``` -java -jar gcviewer_1.3.4.jar gc.log -``` - - - -当然, 如果不想打开程序界面,也可以在后面加上其他参数,直接将分析结果输出到文件。 - -命令大致如下: - - - -``` -java -jar gcviewer_1.3.4.jar gc.log summary.csv chart.png -``` - - - -以上命令将信息汇总到当前目录下的 Excel 文件`summary.csv`之中, 将图形信息保存为`chart.png`文件。 - -点击下载:gcviewer的jar包及使用示例 - -上图中, Chart 区域是对GC事件的图形化展示。包括各个内存池的大小和GC事件。上图中, 只有两个可视化指标: 蓝色线条表示堆内存的使用情况, 黑色的Bar则表示每次GC暂停时间的长短。 - -从图中可以看到, 内存使用量增长很快。一分钟左右就达到了堆内存的最大值. 堆内存几乎全部被消耗, 不能顺利分配新对象, 并引发频繁的 Full GC 事件. 这说明程序可能存在内存泄露, 或者启动时指定的内存空间不足。 - -从图中还可以看到 GC暂停的频率和持续时间。`30秒`之后, GC几乎不间断地运行,最长的暂停时间超过`1.4秒`。 - -在右边有三个选项卡。“`**Summary**`(摘要)” 中比较有用的是 “`Throughput`”(吞吐量百分比) 和 “`Number of GC pauses`”(GC暂停的次数), 以及“`Number of full GC pauses`”(Full GC 暂停的次数). 吞吐量显示了有效工作的时间比例, 剩下的部分就是GC的消耗。 - -以上示例中的吞吐量为`**6.28%**`。这意味着有`**93.72%**` - -下一个有意思的地方是“**Pause**”(暂停)选项卡: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224540.png) - -“`Pause`” 展示了GC暂停的总时间,平均值,最小值和最大值, 并且将 total 与minor/major 暂停分开统计。如果要优化程序的延迟指标, 这些统计可以很快判断出暂停时间是否过长。另外, 我们可以得出明确的信息: 累计暂停时间为`634.59 秒`, GC暂停的总次数为`3,938 次`, 这在`11分钟/660秒`的总运行时间里那不是一般的高。 - -更详细的GC暂停汇总信息, 请查看主界面中的 “**Event details**” 标签: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224553.png) - -从“**Event details**” 标签中, 可以看到日志中所有重要的GC事件汇总:`普通GC停顿`和`Full GC 停顿次数`, 以及`并发执行数`,`非 stop-the-world 事件`等。此示例中, 可以看到一个明显的地方, Full GC 暂停严重影响了吞吐量和延迟, 依据是:`3,928 次 Full GC`, 暂停了`634秒`。 - -可以看到, GCViewer 能用图形界面快速展现异常的GC行为。一般来说, 图像化信息能迅速揭示以下症状: - -* 低吞吐量。当应用的吞吐量下降到不能容忍的地步时, 有用工作的总时间就大量减少. 具体有多大的 “容忍度”(tolerable) 取决于具体场景。按照经验, 低于 90% 的有效时间就值得警惕了, 可能需要好好优化下GC。 -* 单次GC的暂停时间过长。只要有一次GC停顿时间过长,就会影响程序的延迟指标. 例如, 延迟需求规定必须在 1000 ms以内完成交易, 那就不能容忍任何一次GC暂停超过1000毫秒。 -* 堆内存使用率过高。如果老年代空间在 Full GC 之后仍然接近全满, 程序性能就会大幅降低, 可能是资源不足或者内存泄漏。这种症状会对吞吐量产生严重影响。 - -业界良心 —— 图形化展示的GC日志信息绝对是我们重磅推荐的。不用去阅读冗长而又复杂的GC日志,通过容易理解的图形, 也可以得到同样的信息。 - -## 分析器(Profilers) - -下面介绍分析器([profilers](http://zeroturnaround.com/rebellabs/developer-productivity-report-2015-java-performance-survey-results/3/), Oracle官方翻译是:`抽样器`)。相对于前面的工具, 分析器只关心GC中的一部分领域. 本节我们也只关注分析器相关的GC功能。 - -首先警告 —— 不要认为分析器适用于所有的场景。分析器有时确实作用很大, 比如检测代码中的CPU热点时。但某些情况使用分析器不一定是个好方案。 - -对GC调优来说也是一样的。要检测是否因为GC而引起延迟或吞吐量问题时, 不需要使用分析器. 前面提到的工具(`jstat`或 原生/可视化GC日志)就能更好更快地检测出是否存在GC问题. 特别是从生产环境中收集性能数据时, 最好不要使用分析器, 因为性能开销非常大。 - -如果确实需要对GC进行优化, 那么分析器就可以派上用场了, 可以对 Object 的创建信息一目了然. 换个角度看, 如果GC暂停的原因不在某个内存池中, 那就只会是因为创建对象太多了。 所有分析器都能够跟踪对象分配(via allocation profiling), 根据内存分配的轨迹, 让你知道**实际驻留在内存中的是哪些对象**。 - -分配分析能定位到在哪个地方创建了大量的对象. 使用分析器辅助进行GC调优的好处是, 能确定哪种类型的对象最占用内存, 以及哪些线程创建了最多的对象。 - -下面我们通过实例介绍3种分配分析器:`**hprof**`,`**JVisualV**`**M**和`**AProf**`。实际上还有很多分析器可供选择, 有商业产品,也有免费工具, 但其功能和应用基本上都是类似的。 - -### hprof - -[hprof 分析器](http://docs.oracle.com/javase/8/docs/technotes/samples/hprof.html)内置于JDK之中。 在各种环境下都可以使用, 一般优先使用这款工具。 - -要让`hprof`和程序一起运行, 需要修改启动脚本, 类似这样: - - - -``` -java -agentlib:hprof=heap=sites com.yourcompany.YourApplication -``` - - - -在程序退出时,会将分配信息dump(转储)到工作目录下的`java.hprof.txt`文件中。使用文本编辑器打开, 并搜索 “**SITES BEGIN**” 关键字, 可以看到: - - - -``` -SITES BEGIN (ordered by live bytes) Tue Dec 8 11:16:15 2015 - percent live alloc'ed stack class - rank self accum bytes objs bytes objs trace name - 1 64.43% 4.43% 8370336 20121 27513408 66138 302116 int[] - 2 3.26% 88.49% 482976 20124 1587696 66154 302104 java.util.ArrayList - 3 1.76% 88.74% 241704 20121 1587312 66138 302115 eu.plumbr.demo.largeheap.ClonableClass0006 - ... 部分省略 ... - -SITES END -``` - - - -从以上片段可以看到, allocations 是根据每次创建的对象数量来排序的。第一行显示所有对象中有`**64.43%**`的对象是整型数组(`int[]`), 在标识为`302116`的位置创建。搜索 “**TRACE 302116**” 可以看到: - - - -``` -TRACE 302116: - eu.plumbr.demo.largeheap.ClonableClass0006.(GeneratorClass.java:11) - sun.reflect.GeneratedConstructorAccessor7.newInstance(:Unknown line) - sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) - java.lang.reflect.Constructor.newInstance(Constructor.java:422) -``` - - - -现在, 知道有`64.43%`的对象是整数数组, 在`ClonableClass0006`类的构造函数中, 第11行的位置, 接下来就可以优化代码, 以减少GC的压力。 - -### Java VisualVM - -本章前面的第一部分, 在监控 JVM 的GC行为工具时介绍了 JVisualVM , 本节介绍其在分配分析上的应用。 - -JVisualVM 通过GUI的方式连接到正在运行的JVM。 连接上目标JVM之后 : - -1. 打开 “工具” –> “选项” 菜单, 点击**性能分析(Profiler)**标签, 新增配置, 选择 Profiler 内存, 确保勾选了 “Record allocations stack traces”(记录分配栈跟踪)。 -2. 勾选 “Settings”(设置) 复选框, 在内存设置标签下,修改预设配置。 -3. 点击 “Memory”(内存) 按钮开始进行内存分析。 -4. 让程序运行一段时间,以收集关于对象分配的足够信息。 -5. 单击下方的 “Snapshot”(快照) 按钮。可以获取收集到的快照信息。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224729.png) - -完成上面的步骤后, 可以得到类似这样的信息: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224743.png) - -上图按照每个类被创建的对象数量多少来排序。看第一行可以知道, 创建的最多的对象是`int[]`数组. 鼠标右键单击这行, 就可以看到这些对象都在哪些地方创建的: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404224753.png) - -与`hprof`相比, JVisualVM 更加容易使用 —— 比如上面的截图中, 在一个地方就可以看到所有`int[]`的分配信息, 所以多次在同一处代码进行分配的情况就很容易发现。 - -### AProf - -最重要的一款分析器,是由 Devexperts 开发的[**AProf**](https://code.devexperts.com/display/AProf/About+Aprof)。 内存分配分析器 AProf 也被打包为 Java agent 的形式。 - -用 AProf 分析应用程序, 需要修改 JVM 启动脚本,类似这样: - - - -``` -java -javaagent:/path-to/aprof.jar com.yourcompany.YourApplication -``` - - - -重启应用之后, 工作目录下会生成一个`aprof.txt`文件。此文件每分钟更新一次, 包含这样的信息: - - - -``` -======================================================================================================================== -TOTAL allocation dump for 91,289 ms (0h01m31s) -Allocated 1,769,670,584 bytes in 24,868,088 objects of 425 classes in 2,127 locations -======================================================================================================================== - -Top allocation-inducing locations with the data types allocated from them ------------------------------------------------------------------------------------------------------------------------- -eu.plumbr.demo.largeheap.ManyTargetsGarbageProducer.newRandomClassObject: 1,423,675,776 (80.44%) bytes in 17,113,721 (68.81%) objects (avg size 83 bytes) - int[]: 711,322,976 (40.19%) bytes in 1,709,911 (6.87%) objects (avg size 416 bytes) - char[]: 369,550,816 (20.88%) bytes in 5,132,759 (20.63%) objects (avg size 72 bytes) - java.lang.reflect.Constructor: 136,800,000 (7.73%) bytes in 1,710,000 (6.87%) objects (avg size 80 bytes) - java.lang.Object[]: 41,079,872 (2.32%) bytes in 1,710,712 (6.87%) objects (avg size 24 bytes) - java.lang.String: 41,063,496 (2.32%) bytes in 1,710,979 (6.88%) objects (avg size 24 bytes) - java.util.ArrayList: 41,050,680 (2.31%) bytes in 1,710,445 (6.87%) objects (avg size 24 bytes) - ... cut for brevity ... -``` - - - -上面的输出是按照`size`进行排序的。可以看出,`80.44%`的 bytes 和`68.81%`的 objects 是在`ManyTargetsGarbageProducer.newRandomClassObject()`方法中分配的。 其中,**int[]**数组占用了`40.19%`的内存, 是最大的一个。 - -继续往下看, 会发现`allocation traces`(分配痕迹)相关的内容, 也是以 allocation size 排序的: - - - -``` -Top allocated data types with reverse location traces ------------------------------------------------------------------------------------------------------------------------- -int[]: 725,306,304 (40.98%) bytes in 1,954,234 (7.85%) objects (avg size 371 bytes) - eu.plumbr.demo.largeheap.ClonableClass0006.: 38,357,696 (2.16%) bytes in 92,206 (0.37%) objects (avg size 416 bytes) - java.lang.reflect.Constructor.newInstance: 38,357,696 (2.16%) bytes in 92,206 (0.37%) objects (avg size 416 bytes) - eu.plumbr.demo.largeheap.ManyTargetsGarbageProducer.newRandomClassObject: 38,357,280 (2.16%) bytes in 92,205 (0.37%) objects (avg size 416 bytes) - java.lang.reflect.Constructor.newInstance: 416 (0.00%) bytes in 1 (0.00%) objects (avg size 416 bytes) -... cut for brevity ... -``` - - - -可以看到,`int[]`数组的分配, 在`ClonableClass0006`构造函数中继续增大。 - -和其他工具一样,`AProf`揭露了 分配的大小以及位置信息(`allocation size and locations`), 从而能够快速找到最耗内存的部分。在我们看来,**AProf**是最有用的分配分析器, 因为它只专注于内存分配, 所以做得最好。 当然, 这款工具是开源免费的, 资源开销也最小。 - -请继续阅读下一章:7\. GC 调优(实战篇) - GC参考手册 - -原文链接:[GC Tuning: Tooling](https://plumbr.eu/handbook/gc-tuning-measuring) - -翻译时间: 2016年02月06日 - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JNDI\357\274\214OSGI\357\274\214Tomcat\347\261\273\345\212\240\350\275\275\345\231\250\345\256\236\347\216\260.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JNDI\357\274\214OSGI\357\274\214Tomcat\347\261\273\345\212\240\350\275\275\345\231\250\345\256\236\347\216\260.md" deleted file mode 100644 index 6c1b066..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JNDI\357\274\214OSGI\357\274\214Tomcat\347\261\273\345\212\240\350\275\275\345\231\250\345\256\236\347\216\260.md" +++ /dev/null @@ -1,372 +0,0 @@ -# 目录 - * [打破双亲委派模型](#打破双亲委派模型) - * [JNDI](#jndi) - * [JNDI 的理解](#[jndi-的理解]) - * [OSGI](#osgi) - * [1.如何正确的理解和认识OSGI技术?](#1如何正确的理解和认识osgi技术?) - * [Tomcat类加载器以及应用间class隔离与共享](#tomcat类加载器以及应用间class隔离与共享) - * [类加载器](#类加载器) - * [参考文章](#参考文章) - - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 打破双亲委派模型 - -### JNDI - -### JNDI 的理解 - - -JNDI是 Java 命名与文件夹接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之中的一个,不少专家觉得,没有透彻理解JNDI的意义和作用,就没有真正掌握J2EE特别是EJB的知识。 - -那么,JNDI究竟起什么作用?//带着问题看文章是最有效的 - -要了解JNDI的作用,我们能够从“假设不用JNDI我们如何做?用了JNDI后我们又将如何做?”这个问题来探讨。 - -没有JNDI的做法: - -程序猿开发时,知道要开发訪问MySQL数据库的应用,于是将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到数据库。 -就像以下代码这样: - -```` - 1. Connectionconn=null; - 2. try{ - 3. Class.forName("com.mysql.jdbc.Driver", - 4. true,Thread.currentThread().getContextClassLoader()); - 5. conn=DriverManager. - 6. getConnection("jdbc:mysql://MyDBServer?user=qingfeng&password=mingyue"); - 7. ...... - 8. conn.close(); - 9. }catch(Exceptione){ - 10. e.printStackTrace(); - 11. }finally{ - 12. if(conn!=null){ - 13. try{ - 14. conn.close(); - 15. }catch(SQLExceptione){} - 16. } - 17. } -```` - - -这是传统的做法,也是曾经非Java程序猿(如Delphi、VB等)常见的做法。 - -这种做法一般在小规模的开发过程中不会产生问题,仅仅要程序猿熟悉Java语言、了解JDBC技术和MySQL,能够非常快开发出对应的应用程序。 - -没有JNDI的做法存在的问题: - - 1、数据库server名称MyDBServer 、username和口令都可能须要改变,由此引发JDBC URL须要改动; - 2、数据库可能改用别的产品,如改用DB2或者Oracle,引发JDBC驱动程序包和类名须要改动; - 3、随着实际使用终端的添加,原配置的连接池參数可能须要调整; - 4、...... - -解决的方法: - -程序猿应该不须要关心“详细的数据库后台是什么?JDBC驱动程序是什么?JDBC URL格式是什么?訪问数据库的username和口令是什么?”等等这些问题。 - -程序猿编写的程序应该没有对 JDBC驱动程序的引用,没有server名称,没实username称或口令 —— 甚至没有数据库池或连接管理。 - -而是把这些问题交给J2EE容器(比方weblogic)来配置和管理,程序猿仅仅须要对这些配置和管理进行引用就可以。 - -由此,就有了JNDI。 - -//看的出来。是为了一个最最核心的问题:是为了解耦,是为了开发出更加可维护、可扩展//的系统 - -用了JNDI之后的做法: -首先。在在J2EE容器中配置JNDI參数,定义一个数据源。也就是JDBC引用參数,给这个数据源设置一个名称;然后,在程序中,通过数据源名称引用数据源从而訪问后台数据库。 - -//红色的字能够看出。JNDI是由j2ee容器提供的功能 - -详细操作例如以下(以JBoss为例): -1、配置数据源 -在JBoss 的 D:\jboss420GA\docs\examples\jca 文件夹以下。有非常多不同数据库引用的数据源定义模板。 - -将当中的 mysql-ds.xml 文件Copy到你使用的server下,如 D:\jboss420GA\server\default\deploy。 -改动 mysql-ds.xml 文件的内容,使之能通过JDBC正确訪问你的MySQL数据库。例如以下: - - - -```` - - - -MySqlDS -jdbc:mysql://localhost:3306/lw -com.mysql.jdbc.Driver -root -rootpassword - -org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter - - -mySQL - - - -```` - - -这里,定义了一个名为MySqlDS的数据源。其參数包含JDBC的URL。驱动类名,username及密码等。 - -2、在程序中引用数据源: - -```` -1. Connectionconn=null; -2. try{ -3. Contextctx=newInitialContext(); -4. ObjectdatasourceRef=ctx.lookup("java:MySqlDS");//引用数据源 -5. DataSourceds=(Datasource)datasourceRef; -6. conn=ds.getConnection(); -7. ...... -8. c.close(); -9. }catch(Exceptione){ -10. e.printStackTrace(); -11. }finally{ -12. if(conn!=null){ -13. try{ -14. conn.close(); -15. }catch(SQLExceptione){} -16. } -17. } -```` - - -直接使用JDBC或者通过JNDI引用数据源的编程代码量相差无几,可是如今的程序能够不用关心详细JDBC參数了。 - -//解藕了。可扩展了 -在系统部署后。假设数据库的相关參数变更。仅仅须要又一次配置 mysql-ds.xml 改动当中的JDBC參数,仅仅要保证数据源的名称不变,那么程序源码就无需改动。 - -由此可见。JNDI避免了程序与数据库之间的紧耦合,使应用更加易于配置、易于部署。 - -JNDI的扩展: -JNDI在满足了数据源配置的要求的基础上。还进一步扩充了作用:全部与系统外部的资源的引用,都能够通过JNDI定义和引用。 - -//注意什么叫资源 - -所以,在J2EE规范中,J2EE 中的资源并不局限于 JDBC 数据源。 - -引用的类型有非常多,当中包含资源引用(已经讨论过)、环境实体和 EJB 引用。 - -特别是 EJB 引用,它暴露了 JNDI 在 J2EE 中的另外一项关键角色:查找其它应用程序组件。 - -EJB 的 JNDI 引用非常相似于 JDBC 资源的引用。在服务趋于转换的环境中,这是一种非常有效的方法。能够对应用程序架构中所得到的全部组件进行这类配置管理,从 EJB 组件到 JMS 队列和主题。再到简单配置字符串或其它对象。这能够降低随时间的推移服务变更所产生的维护成本,同一时候还能够简化部署,降低集成工作。外部资源”。 - -总结: - -J2EE 规范要求全部 J2EE 容器都要提供 JNDI 规范的实现。//sun 果然喜欢制定规范JNDI 在 J2EE 中的角色就是“交换机” —— J2EE 组件在执行时间接地查找其它组件、资源或服务的通用机制。在多数情况下,提供 JNDI 供应者的容器能够充当有限的数据存储。这样管理员就能够设置应用程序的执行属性,并让其它应用程序引用这些属性(Java 管理扩展(Java Management Extensions,JMX)也能够用作这个目的)。JNDI 在 J2EE 应用程序中的主要角色就是提供间接层,这样组件就能够发现所须要的资源,而不用了解这些间接性。 - -在 J2EE 中,JNDI 是把 J2EE 应用程序合在一起的粘合剂。JNDI 提供的间接寻址同意跨企业交付可伸缩的、功能强大且非常灵活的应用程序。 - -这是 J2EE 的承诺,并且经过一些计划和预先考虑。这个承诺是全然能够实现的。 - - 从上面的文章中能够看出: -1、JNDI 提出的目的是为了解藕,是为了开发更加easy维护,easy扩展。easy部署的应用。 -2、JNDI 是一个sun提出的一个规范(相似于jdbc),详细的实现是各个j2ee容器提供商。sun 仅仅是要求,j2ee容器必须有JNDI这种功能。 - -3、JNDI 在j2ee系统中的角色是“交换机”,是J2EE组件在执行时间接地查找其它组件、资源或服务的通用机制。 -4、JNDI 是通过资源的名字来查找的,资源的名字在整个j2ee应用中(j2ee容器中)是唯一的。 - - - 上文提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外。 - - 双亲委派模型的一次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办? - -这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码,因为启动类加载器的搜索范围中找不到用户应用程序类,那该怎么办? - - -为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。 - - 有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。 - - - -## OSGI - - - -目前,业内关于OSGI技术的学习资源或者技术文档还是很少的。我在某宝网搜索了一下“OSGI”的书籍,结果倒是有,但是种类少的可怜,而且几乎没有人购买。 -因为工作的原因我需要学习OSGI,所以我不得不想尽办法来主动学习OSGI。我将用文字记录学习OSGI的整个过程,通过整理书籍和视频教程,来让我更加了解这门技术,同时也让需要学习这门技术的同志们有一个清晰的学习路线。 - -我们需要解决一下几问题: - -### 1.如何正确的理解和认识OSGI技术? - -我们从外文资料上或者从翻译过来的资料上看到OSGi解释和定义,都是直译过来的,但是OSGI的真实意义未必是中文直译过来的意思。OSGI的解释就是Open Service Gateway Initiative,直译过来就是“开放的服务入口(网关)的初始化”,听起来非常费解,什么是服务入口初始化? - -所以我们不去直译这个OSGI,我们换一种说法来描述OSGI技术。 - -我们来回到我们以前的某些开发场景中去,假设我们使用SSH(struts+spring+hibernate)框架来开发我们的Web项目,我们做产品设计和开发的时候都是分模块的,我们分模块的目的就是实现模块之间的“解耦”,更进一步的目的是方便对一个项目的控制和管理。 -我们对一个项目进行模块化分解之后,我们就可以把不同模块交给不同的开发人员来完成开发,然后项目经理把大家完成的模块集中在一起,然后拼装成一个最终的产品。一般我们开发都是这样的基本情况。 - -那么我们开发的时候预计的是系统的功能,根据系统的功能来进行模块的划分,也就是说,这个产品的功能或客户的需求是划分的重要依据。 - -但是我们在开发过程中,我们模块之间还要彼此保持联系,比如A模块要从B模块拿到一些数据,而B模块可能要调用C模块中的一些方法(除了公共底层的工具类之外)。所以这些模块只是一种逻辑意义上的划分。 - -最重要的一点是,我们把最终的项目要去部署到tomcat或者jBoss的服务器中去部署。那么我们启动服务器的时候,能不能关闭项目的某个模块或功能呢?很明显是做不到的,一旦服务器启动,所有模块就要一起启动,都要占用服务器资源,所以关闭不了模块,假设能强制拿掉,就会影响其它的功能。 - -以上就是我们传统模块式开发的一些局限性。 - -我们做软件开发一直在追求一个境界,就是模块之间的真正“解耦”、“分离”,这样我们在软件的管理和开发上面就会更加的灵活,甚至包括给客户部署项目的时候都可以做到更加的灵活可控。但是我们以前使用SSH框架等架构模式进行产品开发的时候我们是达不到这种要求的。 - -所以我们“架构师”或顶尖的技术高手都在为模块化开发努力的摸索和尝试,然后我们的OSGI的技术规范就应运而生。 - -现在我们的OSGI技术就可以满足我们之前所说的境界:在不同的模块中做到彻底的分离,而不是逻辑意义上的分离,是物理上的分离,也就是说在运行部署之后都可以在不停止服务器的时候直接把某些模块拿下来,其他模块的功能也不受影响。 - -由此,OSGI技术将来会变得非常的重要,因为它在实现模块化解耦的路上,走得比现在大家经常所用的SSH框架走的更远。这个技术在未来大规模、高访问、高并发的Java模块化开发领域,或者是项目规范化管理中,会大大超过SSH等框架的地位。 - -现在主流的一些应用服务器,Oracle的weblogic服务器,IBM的WebSphere,JBoss,还有Sun公司的glassfish服务器,都对OSGI提供了强大的支持,都是在OSGI的技术基础上实现的。有那么多的大型厂商支持OSGI这门技术,我们既可以看到OSGI技术的重要性。所以将来OSGI是将来非常重要的技术。 - -但是OSGI仍然脱离不了框架的支持,因为OSGI本身也使用了很多spring等框架的基本控件(因为要实现AOP依赖注入等功能),但是哪个项目又不去依赖第三方jar呢? - - - 双亲委派模型的另一次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换(HotSwap)、模块热部署(HotDeployment)等,说白了就是希望应用程序能像我们的计算机外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用停机也不用重启。 - - 对于个人计算机来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是企业级软件开发者具有很大的吸引力。Sun公司所提出的JSR-294、JSR-277规范在与JCP组织的模块化规范之争中落败给JSR-291(即OSGi R4.2),虽然Sun不甘失去Java模块化的主导权,独立在发展Jigsaw项目,但目前OSGi已经成为了业界“事实上”的Java模块化标准,而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。 - - 每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。 - - 在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索: - - 1)将以java.*开头的类委派给父类加载器加载。 - - 2)否则,将委派列表名单内的类委派给父类加载器加载。 - - 3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。 - - 4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。 - - 5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。 - - 6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。 - - 7)否则,类查找失败。 - - 上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。 - - 只要有足够意义和理由,突破已有的原则就可认为是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器,并且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但在Java程序员中基本有一个共识:OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。 - - -## Tomcat类加载器以及应用间class隔离与共享 - - - -Tomcat的用户一定都使用过其应用部署功能,无论是直接拷贝文件到webapps目录,还是修改server.xml以目录的形式部署,或者是增加虚拟主机,指定新的appBase等等。 - -但部署应用时,不知道你是否曾注意过这几点: - -1. 如果在一个Tomcat内部署多个应用,甚至多个应用内使用了某个类似的几个不同版本,但它们之间却互不影响。这是如何做到的。 - -2. 如果多个应用都用到了某类似的相同版本,是否可以统一提供,不在各个应用内分别提供,占用内存呢。 - -3. 还有时候,在开发Web应用时,在pom.xml中添加了servlet-api的依赖,那实际应用的class加载时,会加载你的servlet-api 这个jar吗 - -以上提到的这几点,在Tomcat以及各类的应用服务器中,都是通过类加载器(ClasssLoader)来实现的。通过本文,你可以了解到Tomcat内部提供的各种类加载器,Web应用的class和资源等加载的方式,以及其内部的实现原理。在遇到类似问题时,更胸有成竹。 - -### 类加载器 - -Java语言本身,以及现在其它的一些基于JVM之上的语言(Groovy,Jython, Scala...),都是在将代码编译生成class文件,以实现跨多平台,write once, run anywhere。最终的这些class文件,在应用中,又被加载到JVM虚拟机中,开始工作。而把class文件加载到JVM的组件,就是我们所说的类加载器。而对于类加载器的抽象,能面对更多的class数据提供形式,例如网络、文件系统等。 - -Java中常见的那个ClassNotFoundException和NoClassDefFoundError就是类加载器告诉我们的。 - -Servlet规范指出,容器用于加载Web应用内Servlet的class loader, 允许加载位于Web应用内的资源。但不允许重写java.*, javax.*以及容器实现的类。同时 - -每个应用内使用Thread.currentThread.getContextClassLoader()获得的类加载器,都是该应用区别于其它应用的类加载器等等。 - -根据Servlet规范,各个应用服务器厂商自行实现。所以像其他的一些应用服务器一样, Tomcat也提供了多种的类加载器,以便应用服务器内的class以及部署的Web应用类文件运行在容器中时,可以使用不同的class repositories。 - -在Java中,类加载器是以一种父子关系树来组织的。除Bootstrap外,都会包含一个parent 类加载器。(这里写parent 类加载器,而不是父类加载器,不是为了装X,是为了避免和Java里的父类混淆)一般以类加载器需要加载一个class或者资源文件的时候,他会先委托给他的parent类加载器,让parent类加载器先来加载,如果没有,才再在自己的路径上加载。这就是人们常说的双亲委托,即把类加载的请求委托给parent。 - -但是...,这里需要注意一下 - -> 对于Web应用的类加载,和上面的双亲委托是有区别的。 - - 主流的Java Web服务器(也就是Web容器),如Tomcat、Jetty、WebLogic、WebSphere或其他笔者没有列举的服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web容器,要解决如下几个问题: - - 1)部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。 - - 2)部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用[spring](https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Flib.csdn.net%252Fbase%252Fjavaee "Java EE知识库")组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。 - - 3)Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web容器自身也是使用Java语言来实现的。因此,Web容器本身也有类库依赖的问题,一般来说,基于安全考虑,容器所使用的类库应该与应用程序的类库互相独立。 - - 4)支持JSP应用的Web容器,大多数都需要支持HotSwap功能。我们知道,JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件。而且ASP、[PHP](https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Flib.csdn.net%252Fbase%252Fphp "PHP知识库")和JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”的Web容器都会支持JSP生成类的热替换,当然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。 - - 由于存在上述问题,在部署Web应用时,单独的一个Class Path就无法满足需求了,所以各种Web容都“不约而同”地提供了好几个Class Path路径供用户存放第三方类库,这些路径一般都以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。现在,就以Tomcat容器为例,看一看Tomcat具体是如何规划用户类库结构和类加载器的。 - - 在Tomcat目录结构中,有3组目录(“/common/*”、“/server/*”和“/shared/*”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/*”,一共4组,把Java类库放置在这些目录中的含义分别如下: - - ①放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。 - - ②放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。 - - ③放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。 - - ④放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。 - - 为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如下图所示。 - - - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222020.png) - - - - - - - 上图中灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用已经介绍过了。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。 - - 从图中的委派关系中可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。 - - 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。 - -对于Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立CatalinaClassLoader和Shared ClassLoader的实例,否则在用到这两个类加载器的地方都会用Common ClassLoader的实例代替,而默认的配置文件中没有设置这两个loader项,所以Tomcat 6.x顺理成章地把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用。 - -这是Tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用Tomcat 5.x的加载器[架构](https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Flib.csdn.net%252Fbase%252Farchitecture "大型网站架构知识库")。 - - Tomcat加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。如果读者阅读完上面的案例后,能完全理解Tomcat设计团队这样布置加载器架构的用意,那说明已经大致掌握了类加载器“主流”的使用方式,那么笔者不妨再提一个问题让读者思考一下:前面曾经提到过一个场景,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到Common或Shared目录下让这些程序共享。 - - Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?如果研究过虚拟机类加载器机制中的双亲委派模型,相信读者可以很容易地回答这个问题。 - -分析:如果按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,上面在类加载器一节中提到过通过线程上下文方式传播类加载器。 - - 答案是使用线程上下文类加载器来实现的,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。 - - 看spring源码发现,spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。 - - - -本篇博文内容取材自《深入理解Java虚拟机:JVM高级特性与最佳实践》 - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\206\205\345\255\230\347\232\204\347\273\223\346\236\204\344\270\216\346\266\210\345\244\261\347\232\204\346\260\270\344\271\205\344\273\243.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\206\205\345\255\230\347\232\204\347\273\223\346\236\204\344\270\216\346\266\210\345\244\261\347\232\204\346\260\270\344\271\205\344\273\243.md" deleted file mode 100644 index 17431e9..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\206\205\345\255\230\347\232\204\347\273\223\346\236\204\344\270\216\346\266\210\345\244\261\347\232\204\346\260\270\344\271\205\344\273\243.md" +++ /dev/null @@ -1,407 +0,0 @@ -# 目录 - * [前言](#前言) - * [Java堆(Heap)](#java堆(heap)) - * [方法区(Method Area)](#方法区(method-area)) - * [程序计数器(Program Counter Register)](#程序计数器(program-counter-register)) - * [JVM栈(JVM Stacks)](#jvm栈(jvm-stacks)) - * [本地方法栈(Native Method Stacks)](#本地方法栈(native-method-stacks)) - * [哪儿的OutOfMemoryError](#哪儿的outofmemoryerror) - * [一、背景](#一、背景) - * [1.1 永久代(PermGen)在哪里?](#11-永久代(permgen)在哪里?) - * [1.2 JDK8永久代的废弃](#12-jdk8永久代的废弃) - * [二、为什么废弃永久代(PermGen)](#二、为什么废弃永久代(permgen)) - * [2.1 官方说明](#21-官方说明) - * [Motivation](#motivation) - * [2.2 现实使用中易出问题](#22-现实使用中易出问题) - * [三、深入理解元空间(Metaspace)](#三、深入理解元空间(metaspace)) - * [3.1元空间的内存大小](#31元空间的内存大小) - * [3.2常用配置参数](#32常用配置参数) - * [3.3测试并追踪元空间大小](#33测试并追踪元空间大小) - * [3.3.1.测试字符串常量](#331测试字符串常量) - * [3.3.2.测试元空间溢出](#332测试元空间溢出) - * [四、总结](#四、总结) - * [参考文章](#参考文章) - - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 前言 - -所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢? - -其实如果你经常解决服务器性能问题,那么这些问题就会变的非常常见,了解JVM内存也是为了服务器出现性能问题的时候可以快速的了解那块的内存区域出现问题,以便于快速的解决生产故障。 - -先看一张图,这张图能很清晰的说明JVM内存结构布局。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404214718.png) - -JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配; - -方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。 - -在通过一张图来了解如何通过参数来控制各区域的内存大小 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404214735.png) -控制参数 - -* -Xms设置堆的最小空间大小。 -* -Xmx设置堆的最大空间大小。 -* -XX:NewSize设置新生代最小空间大小。 -* -XX:MaxNewSize设置新生代最大空间大小。 -* -XX:PermSize设置永久代最小空间大小。 -* -XX:MaxPermSize设置永久代最大空间大小。 -* -Xss设置每个线程的堆栈大小。 - -没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。 - -> 老年代空间大小=堆空间大小-年轻代大空间大小 - -从更高的一个维度再次来看JVM和系统调用之间的关系 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404214754.png) - -方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。 - -下面我们详细介绍每个区域的作用 - -## Java堆(Heap) - -对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 - -Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 - -根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。 - -如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 - -## 方法区(Method Area) - -方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。 - -对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。 - -Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。 - -根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。 - -方法区有时被称为持久代(PermGen)。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404214839.png) - -所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:伊甸区(Eden),幸存者区域(Survivor Sapce),老年代(Old Generation Space)。 - -方法的执行都是伴随着线程的。原始类型的本地变量以及引用都存放在线程栈中。而引用关联的对象比如String,都存在在堆中。为了更好的理解上面这段话,我们可以看一个例子: - - - - -```` -import java.text.SimpleDateFormat;import java.util.Date;import org.apache.log4j.Logger; - public class HelloWorld { - private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName()); - public void sayHello(String message) { - SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY"); - String today = formatter.format(new Date()); - LOGGER.info(today + ": " + message); - }} - -```` - - - -这段程序的数据在内存中的存放如下: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404214906.png) -通过JConsole工具可以查看运行中的Java程序(比如Eclipse)的一些信息:堆内存的分配,线程的数量以及加载的类的个数; - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404214922.png) - -## 程序计数器(Program Counter Register) - -程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 - -由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 - -如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。 - -此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 - -## JVM栈(JVM Stacks) - -与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 - -局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。 - -其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 - -在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。 - -## 本地方法栈(Native Method Stacks) - -本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。 - -## 哪儿的OutOfMemoryError - -对内存结构清晰的认识同样可以帮助理解不同OutOfMemoryErrors: - - -Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space - - -原因:对象不能被分配到堆内存中 - - -Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space - - -原因:类或者方法不能被加载到持久代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库; - -Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit - - -原因:创建的数组大于堆内存的空间 - - -Exception in thread “main”: java.lang.OutOfMemoryError: request bytes for . Out of swap space? - - -原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。 - - -Exception in thread “main”: java.lang.OutOfMemoryError: (Native method) - - -原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现 - -关于永久代的废弃可以参考这篇文章 - -JDK8-废弃永久代(PermGen)迎来元空间(Metaspace) -(https://www.cnblogs.com/yulei126/p/6777323.html) - - -1.背景 - -2.为什么废弃永久代(PermGen) - -3.深入理解元空间(Metaspace) - -4.总结 - -========正文分割线===== - -## 一、背景 - -### 1.1 永久代(PermGen)在哪里? - -根据,hotspot jvm结构如下(虚拟机栈和本地方法栈合一起了): - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215109.png) -上图引自网络,但有个问题:方法区和heap堆都是线程共享的内存区域。 - -关于方法区和永久代: - -在HotSpot JVM中,这次讨论的永久代,就是上图的方法区(JVM规范中称为方法区)。《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。在其他JVM上不存在永久代。 - -### 1.2 JDK8永久代的废弃 - -JDK8 永久代变化如下图: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215123.png) -1.新生代:Eden+From Survivor+To Survivor - -2.老年代:OldGen - -3.永久代(方法区的实现) : PermGen----->替换为Metaspace(本地内存中) - -## 二、为什么废弃永久代(PermGen) - -### 2.1 官方说明 - -参照JEP122:http://openjdk.java.net/jeps/122,原文截取: - -## Motivation - -This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation. - -即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。 - -### 2.2 现实使用中易出问题 - -由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen - -其实在JDK7时就已经逐步把永久代的内容移动到其他区域了,比如移动到native区,移动到堆区等,而JDK8则是则是废除了永久代,改用元数据。 - -## 三、深入理解元空间(Metaspace) - -### 3.1元空间的内存大小 - -元空间是方法区的在HotSpot jvm 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。 - -元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。 - -### 3.2常用配置参数 - -1.MetaspaceSize - -初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用[Java](http://lib.csdn.net/base/javase "Java SE知识库")-XX:+PrintFlagsInitial命令查看本机的初始化参数 - -2.MaxMetaspaceSize - -限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。 - -3.MinMetaspaceFreeRatio - -当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。 - -4.MaxMetasaceFreeRatio - -当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。 - -5.MaxMetaspaceExpansion - -Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。 - -6.MinMetaspaceExpansion - -Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。 - -### 3.3测试并追踪元空间大小 - -#### 3.3.1.测试字符串常量 - -```` -public class StringOomMock { - static String base = "string"; - - public static void main(String[] args) { - List list = new ArrayList(); - for (int i=0;i< Integer.MAX_VALUE;i++){ - String str = base + base; - base = str; - list.add(str.intern()); - } - } -} -```` - -在eclipse中选中类--》run configuration-->java application--》new 参数如下: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215213.png) - -由于设定了最大内存20M,很快就溢出,如下图: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215254.png) - -可见在jdk8中: - -1.字符串常量由永久代转移到堆中。 - -2.持久代已不存在,PermSize MaxPermSize参数已移除。(看图中最后两行) - -#### 3.3.2.测试元空间溢出 - -根据定义,我们以加载类来测试元空间溢出,代码如下: -```` -package jdk8; - -import java.io.File; -import java.lang.management.ClassLoadingMXBean; -import java.lang.management.ManagementFactory; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.List; - -/** - * - * @ClassName:OOMTest - * @Description:模拟类加载溢出(元空间oom) - * @author diandian.zhang - * @date 2017年4月27日上午9:45:40 - */ -public class OOMTest { - public static void main(String[] args) { - try { - //准备url - URL url = new File("D:/58workplace/11study/src/main/java/jdk8").toURI().toURL(); - URL[] urls = {url}; - //获取有关类型加载的JMX接口 - ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean(); - //用于缓存类加载器 - List classLoaders = new ArrayList(); - while (true) { - //加载类型并缓存类加载器实例 - ClassLoader classLoader = new URLClassLoader(urls); - classLoaders.add(classLoader); - classLoader.loadClass("ClassA"); - //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目) - System.out.println("total: " + loadingBean.getTotalLoadedClassCount()); - System.out.println("active: " + loadingBean.getLoadedClassCount()); - System.out.println("unloaded: " + loadingBean.getUnloadedClassCount()); - } - } catch (Exception e) { - e.printStackTrace(); - } - } -} -```` - -为了快速溢出,设置参数:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=80m,运行结果如下: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215337.png) - -上图证实了,我们的JDK8中类加载(方法区的功能)已经不在永久代PerGem中了,而是Metaspace中。可以配合JVisualVM来看,更直观一些。 - -## 四、总结 - -本文讲解了元空间(Metaspace)的由来和本质,常用配置,以及监控测试。元空间的大小是动态变更的,但不是无限大的,最好也时常关注一下大小,以免影响服务器内存。 - - -## 参考文章 - -https://segmentfault.com/a/1190000009707894 - -https://www.cnblogs.com/hysum/p/7100874.html - -http://c.biancheng.net/view/939.html - -https://www.runoob.com - -https://blog.csdn.net/android_hl/article/details/53228348 - -## 微信公众号 - -### Java技术江湖 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源,关注公众号后,后台回复关键字 **“Java”** 即可免费无套路获取。 - -![我的公众号](https://img-blog.csdnimg.cn/20190805090108984.jpg) - -### 个人公众号:黄小斜 - -作者是 985 硕士,蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写作,相信终身学习的力量! - -**程序员3T技术学习资源:** 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 **“资料”** 即可免费无套路获取。 - -![](https://img-blog.csdnimg.cn/20190829222750556.jpg) diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\236\203\345\234\276\345\233\236\346\224\266\345\237\272\346\234\254\345\216\237\347\220\206\345\222\214\347\256\227\346\263\225.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\236\203\345\234\276\345\233\236\346\224\266\345\237\272\346\234\254\345\216\237\347\220\206\345\222\214\347\256\227\346\263\225.md" deleted file mode 100644 index 8048fb5..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\236\203\345\234\276\345\233\236\346\224\266\345\237\272\346\234\254\345\216\237\347\220\206\345\222\214\347\256\227\346\263\225.md" +++ /dev/null @@ -1,568 +0,0 @@ -# 目录 - * [JVM GC基本原理与GC算法](#jvm-gc基本原理与gc算法) - * [Java关键术语](#java关键术语) - * [Java HotSpot 虚拟机](#java-hotspot-虚拟机) - * [Java堆内存](#java堆内存) - * [启动Java垃圾回收](#启动java垃圾回收) - * [各种GC的触发时机(When)](#各种gc的触发时机when) - * [GC类型](#gc类型) - * [触发时机](#触发时机) - * [FULL GC触发条件详解](#full-gc触发条件详解) - * [总结](#总结) - * [什么是Stop the world](#什么是stop-the-world) - * [Java垃圾回收过程](#java垃圾回收过程) - * [垃圾回收中实例的终结](#垃圾回收中实例的终结) - * [对象什么时候符合垃圾回收的条件?](#对象什么时候符合垃圾回收的条件?) - * [GC Scope 示例程序](#gc-scope-示例程序) - * [JVM GC算法](#JVM GC算法) - * [JVM垃圾判定算法](#jvm垃圾判定算法) - * [引用计数算法(Reference Counting)](#引用计数算法reference-counting) - * [可达性分析算法(根搜索算法)](#可达性分析算法(根搜索算法)) - * [四种引用](#四种引用) - * [JVM垃圾回收算法](#jvm垃圾回收算法) - * [标记—清除算法(Mark-Sweep)](#标记清除算法(mark-sweep)) - * [复制算法(Copying)](#复制算法(copying)) - * [标记—整理算法(Mark-Compact)](#标记整理算法(mark-compact)) - * [分代收集算法(Generational Collection)](#分代收集算法generational-collection) - * [参考文章](#参考文章) - - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## JVM GC基本原理与GC算法 - -Java的内存分配与回收全部由JVM垃圾回收进程自动完成。与C语言不同,Java开发者不需要自己编写代码实现垃圾回收。这是Java深受大家欢迎的众多特性之一,能够帮助程序员更好地编写Java程序。 - -下面四篇教程是了解Java 垃圾回收(GC)的基础: - -1. [垃圾回收简介](http://www.importnew.com/13504.html) -2. [圾回收是如何工作的?](http://www.importnew.com/13493.html) -3. [垃圾回收的类别](http://www.importnew.com/13827.html) - -这篇教程是系列第一部分。首先会解释基本的术语,比如JDK、JVM、JRE和HotSpotVM。接着会介绍JVM结构和Java 堆内存结构。理解这些基础对于理解后面的垃圾回收知识很重要。 - -## Java关键术语 - -* JavaAPI:一系列帮助开发者创建Java应用程序的封装好的库。 -* Java 开发工具包 (JDK):一系列工具帮助开发者创建Java应用程序。JDK包含工具编译、运行、打包、分发和监视Java应用程序。 -* Java 虚拟机(JVM):JVM是一个抽象的计算机结构。Java程序根据JVM的特性编写。JVM针对特定于操作系统并且可以将Java指令翻译成底层系统的指令并执行。JVM确保了Java的平台无关性。 -* Java 运行环境(JRE):JRE包含JVM实现和Java API。 - -## Java HotSpot 虚拟机 - -每种JVM实现可能采用不同的方法实现垃圾回收机制。在收购SUN之前,Oracle使用的是JRockit JVM,收购之后使用HotSpot JVM。目前Oracle拥有两种JVM实现并且一段时间后两个JVM实现会合二为一。 - -HotSpot JVM是目前Oracle SE平台标准核心组件的一部分。在这篇垃圾回收教程中,我们将会了解基于HotSpot虚拟机的垃圾回收原则。 - -## Java堆内存 - -我们有必要了解堆内存在JVM内存模型的角色。在运行时,Java的实例被存放在堆内存区域。当一个对象不再被引用时,满足条件就会从堆内存移除。在垃圾回收进程中,这些对象将会从堆内存移除并且内存空间被回收。堆内存以下三个主要区域: - -1. 新生代(Young Generation) - * Eden空间(Eden space,任何实例都通过Eden空间进入运行时内存区域) - * S0 Survivor空间(S0 Survivor space,存在时间长的实例将会从Eden空间移动到S0 Survivor空间) - * S1 Survivor空间 (存在时间更长的实例将会从S0 Survivor空间移动到S1 Survivor空间) -2. 老年代(Old Generation)实例将从S1提升到Tenured(终身代) -3. 永久代(Permanent Generation)包含类、方法等细节的元信息 - - -永久代空间[在Java SE8特性](http://javapapers.com/java/java-8-features/)中已经被移除。 - -Java 垃圾回收是一项自动化的过程,用来管理程序所使用的运行时内存。通过这一自动化过程,JVM 解除了程序员在程序中分配和释放内存资源的开销。 - -## 启动Java垃圾回收 - -作为一个自动的过程,程序员不需要在代码中显示地启动垃圾回收过程。`System.gc()`和`Runtime.gc()`用来请求JVM启动垃圾回收。 - -虽然这个请求机制提供给程序员一个启动 GC 过程的机会,但是启动由 JVM负责。JVM可以拒绝这个请求,所以并不保证这些调用都将执行垃圾回收。启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用。JVM将这个选择留给了Java规范的实现,不同实现具体使用的算法不尽相同。 - -毋庸置疑,我们知道垃圾回收过程是不能被强制执行的。我刚刚发现了一个调用`System.gc()`有意义的场景。通过这篇文章了解一下[适合调用System.gc()](http://javapapers.com/core-java/system-gc-invocation-a-suitable-scenario/)这种极端情况。 - -## 各种GC的触发时机(When) - -### GC类型 - -说到GC类型,就更有意思了,为什么呢,因为业界没有统一的严格意义上的界限,也没有严格意义上的GC类型,都是左边一个教授一套名字,右边一个作者一套名字。为什么会有这个情况呢,因为GC类型是和收集器有关的,不同的收集器会有自己独特的一些收集类型。所以作者在这里引用**R大**关于GC类型的介绍,作者觉得还是比较妥当准确的。如下: - -* Partial GC:并不收集整个GC堆的模式 - * Young GC(Minor GC):只收集young gen的GC - * Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式 - * Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式 -* Full GC(Major GC):收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。 - -### 触发时机 - -上面大家也看到了,GC类型分分类是和收集器有关的,那么当然了,对于不同的收集器,GC触发时机也是不一样的,作者就针对默认的serial GC来说: - -* young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。 -* full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。 - -### FULL GC触发条件详解 - -除直接调用System.gc外,触发Full GC执行的情况有如下四种。 - -1.旧生代空间不足 - -旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: - -java.lang.OutOfMemoryError:Javaheapspace - -为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 - -2\. Permanet Generation空间满 - -Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: - -java.lang.OutOfMemoryError:PermGenspace - -为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 - -3\. CMS GC时出现promotion failed和concurrent mode failure - -对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。 - -promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 - -应对措施为:增大survivor space、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。 - -4.统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间 - -这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 - -例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 - -当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 - -除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.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可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 - -### 什么是Stop the world - -Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。 - -GC时的Stop the World(STW)是大家最大的敌人。但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象。 - -JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation,比如分派GC,thread dump等,这些任务,都需要整个Heap,以及所有线程的状态是静止的,一致的才能进行。所以JVM引入了安全点(Safe Point)的概念,想办法在需要进行VM Operation时,通知所有的线程进入一个静止的安全点。 - -除了GC,其他触发安全点的VM Operation包括: - -1\. JIT相关,比如Code deoptimization, Flushing code cache ; - -2\. Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation) ; - -3\. Biased lock revocation 取消偏向锁 ; - -4\. Various debug operation (e.g. thread dump or deadlock check); - -## Java垃圾回收过程 - -垃圾回收是一种回收无用内存空间并使其对未来实例可用的过程。 - -Eden 区:当一个实例被创建了,首先会被存储在堆内存年轻代的 Eden 区中。 - -注意:如果你不能理解这些词汇,我建议你阅读这篇[垃圾回收介绍](http://javapapers.com/java/java-garbage-collection-introduction/),这篇教程详细地介绍了内存模型、JVM 架构以及这些术语。 - -Survivor 区(S0 和 S1):作为年轻代 GC(Minor GC)周期的一部分,存活的对象(仍然被引用的)从 Eden 区被移动到 Survivor 区的 S0 中。类似的,垃圾回收器会扫描 S0 然后将存活的实例移动到 S1 中。 - -(译注:此处不应该是Eden和S0中存活的都移到S1么,为什么会先移到S0再从S0移到S1?) - -死亡的实例(不再被引用)被标记为垃圾回收。根据垃圾回收器(有四种常用的垃圾回收器,将在下一教程中介绍它们)选择的不同,要么被标记的实例都会不停地从内存中移除,要么回收过程会在一个单独的进程中完成。 - -老年代:老年代(Old or tenured generation)是堆内存中的第二块逻辑区。当垃圾回收器执行 Minor GC 周期时,在 S1 Survivor 区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收。 - -老年代 GC(Major GC):相对于 Java 垃圾回收过程,老年代是实例生命周期的最后阶段。Major GC 扫描老年代的垃圾回收过程。如果实例不再被引用,那么它们会被标记为回收,否则它们会继续留在老年代中。 - -内存碎片:一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配。这些空出的空间将会使整个内存区域碎片化。为了实例的快速分配,需要进行碎片整理。基于垃圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中完成。 - -## 垃圾回收中实例的终结 - -在释放一个实例和回收内存空间之前,Java 垃圾回收器会调用实例各自的`finalize()`方法,从而该实例有机会释放所持有的资源。虽然可以保证`finalize()`会在回收内存空间之前被调用,但是没有指定的顺序和时间。多个实例间的顺序是无法被预知,甚至可能会并行发生。程序不应该预先调整实例之间的顺序并使用`finalize()`方法回收资源。 - -* 任何在 finalize过程中未被捕获的异常会自动被忽略,然后该实例的 finalize 过程被取消。 -* JVM 规范中并没有讨论关于弱引用的垃圾回收机制,也没有很明确的要求。具体的实现都由实现方决定。 -* 垃圾回收是由一个守护线程完成的。 - -## 对象什么时候符合垃圾回收的条件? - -* 所有实例都没有活动线程访问。 -* 没有被其他任何实例访问的循环引用实例。 - -[Java 中有不同的引用类型](http://javapapers.com/core-java/java-weak-reference/)。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。 - -| 引用类型 | 垃圾收集 | -| --- | --- | -| 强引用(Strong Reference) | 不符合垃圾收集 | -| 软引用(Soft Reference) | 垃圾收集可能会执行,但会作为最后的选择 | -| 弱引用(Weak Reference) | 符合垃圾收集 | -| 虚引用(Phantom Reference) | 符合垃圾收集 | - -在编译过程中作为一种优化技术,Java 编译器能选择给实例赋`null`值,从而标记实例为可回收。 -```` - class Animal { - - public static void main(String[] args) { - - Animal lion = new Animal(); - - System.out.println("Main is completed."); - - } - - - - protected void finalize() { - - System.out.println("Rest in Peace!"); - - } - - } -```` -在上面的类中,`lion`对象在实例化行后从未被使用过。因此 Java 编译器作为一种优化措施可以直接在实例化行后赋值`lion = null`。因此,即使在 SOP 输出之前, finalize 函数也能够打印出`'Rest in Peace!'`。我们不能证明这确定会发生,因为它依赖JVM的实现方式和运行时使用的内存。然而,我们还能学习到一点:如果编译器看到该实例在未来再也不会被引用,能够选择并提早释放实例空间。 - -* 关于对象什么时候符合垃圾回收有一个更好的例子。实例的所有属性能被存储在寄存器中,随后寄存器将被访问并读取内容。无一例外,这些值将被写回到实例中。虽然这些值在将来能被使用,这个实例仍然能被标记为符合垃圾回收。这是一个很经典的例子,不是吗? -* 当被赋值为null时,这是很简单的一个符合垃圾回收的示例。当然,复杂的情况可以像上面的几点。这是由 JVM 实现者所做的选择。目的是留下尽可能小的内存占用,加快响应速度,提高吞吐量。为了实现这一目标, JVM 的实现者可以选择一个更好的方案或算法在垃圾回收过程中回收内存空间。 -* 当`finalize()`方法被调用时,JVM 会释放该线程上的所有同步锁。 - -### GC Scope 示例程序 -```` -Class GCScope { - - GCScope t; - - static int i = 1; - - - - public static void main(String args[]) { - - GCScope t1 = new GCScope(); - - GCScope t2 = new GCScope(); - - GCScope t3 = new GCScope(); - - - - // No Object Is Eligible for GC - - - - t1.t = t2; // No Object Is Eligible for GC - - t2.t = t3; // No Object Is Eligible for GC - - t3.t = t1; // No Object Is Eligible for GC - - - - t1 = null; - - // No Object Is Eligible for GC (t3.t still has a reference to t1) - - - - t2 = null; - - // No Object Is Eligible for GC (t3.t.t still has a reference to t2) - - - - t3 = null; - - // All the 3 Object Is Eligible for GC (None of them have a reference. - - // only the variable t of the objects are referring each other in a - - // rounded fashion forming the Island of objects with out any external - - // reference) - - } - - - - protected void finalize() { - - System.out.println("Garbage collected from object" + i); - - i++; - - } - - - -class GCScope { - - GCScope t; - - static int i = 1; - - - - public static void main(String args[]) { - - GCScope t1 = new GCScope(); - - GCScope t2 = new GCScope(); - - GCScope t3 = new GCScope(); - - - - // 没有对象符合GC - - t1.t = t2; // 没有对象符合GC - - t2.t = t3; // 没有对象符合GC - - t3.t = t1; // 没有对象符合GC - - - - t1 = null; - - // 没有对象符合GC (t3.t 仍然有一个到 t1 的引用) - - - - t2 = null; - - // 没有对象符合GC (t3.t.t 仍然有一个到 t2 的引用) - - - - t3 = null; - - // 所有三个对象都符合GC (它们中没有一个拥有引用。 - - // 只有各对象的变量 t 还指向了彼此, - - // 形成了一个由对象组成的环形的岛,而没有任何外部的引用。) - - } - - - - protected void finalize() { - - System.out.println("Garbage collected from object" + i); - - i++; - - } -} -```` -## JVM GC算法 - -在判断哪些内存需要回收和什么时候回收用到GC 算法,本文主要对GC 算法进行讲解。 - -## JVM垃圾判定算法 - -常见的JVM垃圾判定算法包括:引用计数算法、可达性分析算法。 - -### 引用计数算法(Reference Counting) - -引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。 - -给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 - -优点:简单,高效,现在的objective-c用的就是这种算法。 - -缺点:很难处理循环引用,相互引用的两个对象则无法释放。因此目前主流的Java虚拟机都摒弃掉了这种算法。 - -举个简单的例子,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象没有任何引用,实际上这两个对象已经不可能再被访问,但是因为互相引用,导致它们的引用计数都不为0,因此引用计数算法无法通知GC收集器回收它们。 - -``` - -public class ReferenceCountingGC { - public Object instance = null; - - public static void main(String[] args) { - ReferenceCountingGC objA = new ReferenceCountingGC(); - ReferenceCountingGC objB = new ReferenceCountingGC(); - objA.instance = objB; - objB.instance = objA; - - objA = null; - objB = null; - - System.gc();//GC - } -} -``` - -运行结果 - -``` -[GC (System.gc()) [PSYoungGen: 3329K->744K(38400K)] 3329K->752K(125952K), 0.0341414 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] -[Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->628K(87552K)] 752K->628K(125952K), [Metaspace: 3450K->3450K(1056768K)], 0.0060728 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] -Heap - PSYoungGen total 38400K, used 998K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000) - eden space 33280K, 3% used [0x00000000d5c00000,0x00000000d5cf9b20,0x00000000d7c80000) - from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000) - to space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000) - ParOldGen total 87552K, used 628K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000) - object space 87552K, 0% used [0x0000000081400000,0x000000008149d2c8,0x0000000086980000) - Metaspace used 3469K, capacity 4496K, committed 4864K, reserved 1056768K - class space used 381K, capacity 388K, committed 512K, reserved 1048576K - -Process finished with exit code 0 -``` - -从运行结果看,GC日志中包含“3329K->744K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,说明虚拟机不是通过引用技术算法来判断对象是否存活的。 - -### 可达性分析算法(根搜索算法) - -可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。 - -从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215732.png) - -在Java语言中,可以作为GC Roots的对象包括下面几种: - -* 虚拟机栈(栈帧中的本地变量表)中的引用对象。 -* 方法区中的类静态属性引用的对象。 -* 方法区中的常量引用的对象。 -* 本地方法栈中JNI(Native方法)的引用对象 - -真正标记以为对象为可回收状态至少要标记两次。 - -## 四种引用 - -强引用就是指在程序代码之中普遍存在的,类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 - -``` - -Object obj = new Object(); -``` - -软引用是用来描述一些还有用但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。 - -``` - -Object obj = new Object(); -SoftReference sf = new SoftReference(obj); -``` - -弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。 - -``` - -Object obj = new Object(); -WeakReference wf = new WeakReference(obj); -``` - -虚引用也成为幽灵引用或者幻影引用,它是最弱的一中引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供给了PhantomReference类来实现虚引用。 - -``` - -Object obj = new Object(); -PhantomReference pf = new PhantomReference(obj); -``` - -## JVM垃圾回收算法 - -常见的垃圾回收算法包括:标记-清除算法,复制算法,标记-整理算法,分代收集算法。 - -在介绍JVM垃圾回收算法前,先介绍一个概念。 - -Stop-the-World - -Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。 - -## 标记—清除算法(Mark-Sweep) - -之所以说标记/清除算法是几种GC算法中最基础的算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。标记/清除算法的基本思想就跟它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。 - -标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象; - -清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收。 - -不足: - -* 标记和清除过程效率都不高 -* 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215805.png) -## 复制算法(Copying) - -将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 - -现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。 - -不足: - -* 将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 -* 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215817.png) - -## 标记—整理算法(Mark-Compact) - -标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,因此其不会产生内存碎片。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。 - -不足: - -效率不高,不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404215845.png) - -## 分代收集算法(Generational Collection) - -分代回收算法实际上是把复制算法和标记整理法的结合,并不是真正一个新的算法,一般分为:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要进行回收的,新生代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。 - -新生代:由于新生代产生很多临时对象,大量对象需要进行回收,所以采用复制算法是最高效的。 - -老年代:回收的对象很少,都是经过几次标记后都不是可回收的状态转移到老年代的,所以仅有少量对象需要回收,故采用标记清除或者标记整理算法。 - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\270\270\347\224\250\345\217\202\346\225\260\344\273\245\345\217\212\350\260\203\344\274\230\345\256\236\350\267\265.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\270\270\347\224\250\345\217\202\346\225\260\344\273\245\345\217\212\350\260\203\344\274\230\345\256\236\350\267\265.md" deleted file mode 100644 index caac027..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\345\270\270\347\224\250\345\217\202\346\225\260\344\273\245\345\217\212\350\260\203\344\274\230\345\256\236\350\267\265.md" +++ /dev/null @@ -1,659 +0,0 @@ -# 目录 - - * [JVM优化的必要性](#jvm优化的必要性) - * [JVM调优原则](#jvm调优原则) - * [JVM运行参数设置](#jvm运行参数设置) - * [JVM性能调优工具](#jvm性能调优工具) - * [常用调优策略](#常用调优策略) - * [六、JVM调优实例](#六、jvm调优实例) - * [七、一个具体的实战案例分析](#七、一个具体的实战案例分析) - * [参考资料](#参考资料) - * [参考文章](#参考文章) - - - - -![](https://pic.rmb.bdstatic.com/bjh/down/97522789c423e19931adafa65bd5424d.png) - -## JVM优化的必要性 - -**1.1: 项目上线后,什么原因使得我们需要进行jvm调优** - -1)、垃圾太多(java线程,对象占满内存),内存占满了,程序跑不动了!! -2)、垃圾回收线程太多,频繁地回收垃圾(垃圾回收线程本身也会占用资源: 占用内存,cpu资源),导致程序性能下降 -3)、回收垃圾频繁导致STW - -因此基于以上的原因,程序上线后,必须进行调优,否则程序性能就无法提升;也就是程序上线后,必须设置合理的垃圾回收策略; - -**1.2: jvm调优的本质是什么??** - -答案: 回收垃圾,及时回收没有用垃圾对象,及时释放掉内存空间 - -**1.3: 基于服务器环境,jvm堆内存到底应用设置多少内存?** - -1、32位的操作系统 --- 寻址能力 2^32 = 4GB ,最大的能支持4gb; jvm可以分配 2g+ - -2、64位的操作系统 --- 寻址能力 2^64 = 16384PB , 高性能计算机(IBM Z unix 128G 200+) - -![](https://pic.rmb.bdstatic.com/bjh/down/aae367a929e2d1361975942091ffadfe.png) - -Jvm堆内存不能设置太大,否则会导致寻址垃圾的时间过长,也就是导致整个程序STW, 也不能设置太小,否则会导致回收垃圾过于频繁; - -**1.4:总结** - -如果你遇到以下情况,就需要考虑进行JVM调优了: - -* Heap内存(老年代)持续上涨达到设置的最大内存值; - -* Full GC 次数频繁; - -* GC 停顿时间过长(超过1秒); - -* 应用出现OutOfMemory 等内存异常; - -* 应用中有使用本地缓存且占用大量内存空间; - -* 系统吞吐量与响应性能不高或下降。 - -## JVM调优原则 - -![](https://pic.rmb.bdstatic.com/bjh/down/7cd4f5c19ab4f5f5d3087422097ee931.png) - -JVM调优是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,因此,在进行JVM调优时,我们要遵循一些原则: - -* 大多数的Java应用不需要进行JVM优化; - -* 大多数导致GC问题的原因是代码层面的问题导致的(代码层面); - -* 上线之前,应先考虑将机器的JVM参数设置到最优; - -* 减少创建对象的数量(代码层面); - -* 减少使用全局变量和大对象(代码层面); - -* 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面); - -* 分析GC情况优化代码比优化JVM参数更好(代码层面); - -通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。 - -**2.1 JVM调优目标** - -调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。 - -* 延迟:GC低停顿和GC低频率; - -* 低内存占用; - -* 高吞吐量; - -其中,任何一个属性性能的提高,几乎都是以牺牲其他属性性能的损为代价的,不可兼得。具体根据在业务中的重要性确定。 - -**2.2 JVM调优量化目标** - -下面展示了一些JVM调优的量化目标参考实例: - -* Heap 内存使用率 <= 70%; - -* Old generation内存使用率<= 70%; - -* avgpause <= 1秒; - -* Full gc 次数0 或 avg pause interval >= 24小时 ; - -注意:不同应用的JVM调优量化目标是不一样的。 - -**2.3 JVM调优的步骤** - -一般情况下,JVM调优可通过以下步骤进行: - -* 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点; - -* 确定JVM调优量化目标; - -* 确定JVM调优参数(根据历史JVM参数来调整); - -* 依次调优内存、延迟、吞吐量等指标; - -* 对比观察调优前后的差异; - -* 不断的分析和调整,直到找到合适的JVM参数配置; - -* 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。 - -以上操作步骤中,某些步骤是需要多次不断迭代完成的。一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求,要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。 - -![](https://pic.rmb.bdstatic.com/bjh/down/c271890a3714d538ed3362073c66893d.png) - -**调优原则总结** - -JVM的自动内存管理本来就是为了将开发人员从内存管理的泥潭里拉出来。JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的选择才是进行JVM调优。 - -即使不得不进行JVM调优,也绝对不能拍脑门就去调整参数,一定要全面监控,详细分析性能数据。 - -**附录:系统性能优化指导** - -![](https://pic.rmb.bdstatic.com/bjh/down/00d90cef8369568f8581a9850dcd42e6.png) - -### JVM运行参数设置 - -**3.1、堆参数设置** - -**-XX:+PrintGC**使用这个参数,虚拟机启动后,只要遇到GC就会打印日志 - -**-XX:+UseSerialGC**配置串行回收器 - -**-XX:+PrintGCDetails**可以查看详细信息,包括各个区的情况 - -**-Xms**设置Java程序启动时初始化堆大小 - -**-Xmx**设置Java程序能获得最大的堆大小 - -**-Xmx20m -Xms5m -XX:+PrintCommandLineFlags**可以将隐式或者显示传给虚拟机的参数输出 - -**3.2、新生代参数配置** - -**-Xmn**可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右 - -**-XX:SurvivorRatio**用来设置新生代中eden空间和from/to空间的比例。含义:-XX:SurvivorRatio=eden/from**/**eden/to - -不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略:尽可能将对象预留在新生代,减少老年代的GC次数 - -除了可以设置新生代的绝对大小(-Xmn),还可以使用(-XX:NewRatio)设置新生代和老年代的比例:-XX:NewRatio=老年代/新生代 - -**配置运行时参数:** - --Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC - -**3.3、堆溢出参数配置** - -在Java程序在运行过程中,如果对空间不足,则会抛出内存溢出的错误(Out Of Memory)OOM,一旦这类问题发生在生产环境,则可能引起严重的业务中断,Java虚拟机提供了**-XX:+ -HeapDumpOnOutOfMemoryError**,使用该参数可以在内存溢出时导出整个堆信息,与之配合使用的还有参数**-XX:HeapDumpPath**,可以设置导出堆的存放路径 - -内存分析工具:Memory Analyzer - -**配置运行时参数**-Xms1m -Xmx1m -XX:+ -HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Demo3.dump - -**3.4、栈参数配置** - -Java虚拟机提供了参数**-Xss**来指定线程的最大栈空间,整个参数也直接决定了函数可调用的最大深度。 - -**配置运行时参数:**-Xss1m - -3.5、方法区参数配置 - -和Java堆一样,方法区是一块所有线程共享的内存区域,它用于保存系统的类信息,方法区(永久区)可以保存多少信息可以对其进行配置,在默认情况下,**-XX:MaxPermSize**为64M,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以免出现永久区内存溢出的问题 - --XX:PermSize=64M -XX:MaxPermSize=64M - -**3.6、直接内存参数配置** - -直接内存也是Java程序中非常重要的组成部分,特别是广泛用在NIO中,直接内存跳过了Java堆,使用Java程序可以直接访问原生堆空间,因此在一定程度上加快了内存空间的访问速度 - -但是说直接内存一定就可以提高内存访问速度也不见得,具体情况具体分析 - -**相关配置参数:-XX:MaxDirectMemorySize**,如果不设置,默认值为最大堆空间,即-Xmx。直接内存使用达到上限时,就会触发垃圾回收,如果不能有效的释放空间,就会引起系统的OOM - -**3.7、对象进入老年代的参数配置** - -一般而言,对象首次创建会被放置在新生代的eden区,如果没有GC介入,则对象不会离开eden区,那么eden区的对象如何进入老年代呢? - -通常情况下,只要对象的年龄达到一定的大小,就会自动离开年轻代进入老年代,对象年龄是由对象经历数次GC决定的,在新生代每次GC之后如果对象没有被回收,则年龄加1 - -虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升老年代 - -**-XX:MaxTenuringThreshold**,默认情况下为15 - -**配置运行时参数:**-Xmx64M -Xms64M -XX:+PrintGCDetails - -**结论**:对象首次创建会被放置在新生代的eden区,因此输出结果中from和to区都为0% - -根据设置MaxTenuringThreshold参数,可以指定新生代对象经过多少次回收后进入老年代。另外,大对象新生代eden区无法装入时,也会直接进入老年代。 - -JVM里有个参数可以设置对象的大小超过在指定的大小之后,直接晋升老年代 **-XX:PretenureSizeThreshold=15** - -参数:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails - -使用PretenureSizeThreshold可以进行指定进入老年代的对象大小,但是要注意TLAB区域优先分配空间。虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会 - -参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails --XX:PretenureSizeThreshold=1000 -XX:-UseTLAB - -**3.8、TLAB参数配置** - -TLAB全称是Thread Local Allocation Buffer,即线程本地分配缓存,从名字上看是一个线程专用的内存分配区域,是为了加速对象分配对象而生的。每一个线程都会产生一个TLAB,该线程独享的工作区域,Java虚拟机使用这种TLAB区来避免多线程冲突问题,提高了对象分配的效率 - -TLAB空间一般不会太大,当大对象无法在TLAB分配时,则会直接分配到堆上 - -**-XX:+UseTLAB**使用TLAB - -**-XX:+TLABSize**设置TLAB大小 - -**-XX:TLABRefillWasteFraction**设置维护进入TLAB空间的单个对象大小,它是一个比例值,默认为64,即如果对象大于整个空间的1/64,则在堆创建对象 - -**-XX:+PrintTLAB**查看TLAB信息 - -**-XX:ResizeTLAB**自调整TLABRefillWasteFraction阈值 - -参数:-XX:+UseTLAB -XX:+PrintTLAB -XX:+PrintGC -XX:TLABSize=102400 -XX:-ResizeTLAB --XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis -server - -内存参数 - - - - - -![](https://pics7.baidu.com/feed/bba1cd11728b47102f9e1e7af46fe7f6fd03237b.png@f_auto?token=ca9d5541411861cfb09e792849a82a06) - - - - - -## JVM性能调优工具 - -这个篇幅在这里就不过多介绍了,可以参照: - -深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全 - -## 常用调优策略 - -这里还是要提一下,及时确定要进行JVM调优,也不要陷入“知见障”,进行分析之后,发现可以通过优化程序提升性能,仍然首选优化程序。 - -**5.1、选择合适的垃圾回收器** - -CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。 - -CPU多核,关注吞吐量 ,那么选择PS+PO组合。 - -CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。 - -CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。 - -参数配置: - -> //设置Serial垃圾收集器(新生代) -> -> 开启:-XX:+UseSerialGC -> -> //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 -> -> 开启 -XX:+UseParallelOldGC -> -> //CMS垃圾收集器(老年代) -> -> 开启 -XX:+UseConcMarkSweepGC -> -> //设置G1垃圾收集器 -> -> 开启 -XX:+UseG1GC - -**5.2、调整内存大小** - -现象:垃圾收集频率非常频繁。 - -原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。 - -注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。 - -参数配置: - -> //设置堆初始值 -> -> 指令1:-Xms2g -> -> 指令2:-XX:InitialHeapSize=2048m -> -> //设置堆区最大值 -> -> 指令1:`-Xmx2g` -> -> 指令2: -XX:MaxHeapSize=2048m -> -> //新生代内存配置 -> -> 指令1:-Xmn512m -> -> 指令2:-XX:MaxNewSize=512m - -**5.3、设置符合预期的停顿时间** - -**现象**:程序间接性的卡顿 - -**原因**:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。 - -**注意**:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾. - -**参数配置**: - -> //GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间 -> -> -XX:MaxGCPauseMillis - -**5.4、调整内存区域大小比率** - -现象:某一个区域的GC频繁,其他都正常。 - -原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。 - -注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。 - -参数配置: - -> //survivor区和Eden区大小比率 -> -> 指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6 -> -> //新生代和老年代的占比 -> -> -XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2 - -**5.5、调整对象升老年代的年龄** - -**现象**:老年代频繁GC,每次回收的对象很多。 - -**原因**:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。 - -**注意**:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。 - -配置参数: - -> //进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7 -> -> -XX:InitialTenuringThreshol=7 - -**5.6、调整大对象的标准** - -**现象**:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。 - -**原因**:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。 - -**注意**:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。 - -配置参数: - -> //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。 -> -> -XX:PretenureSizeThreshold=1000000 - -**5.7、调整GC的触发时机** - -**现象**:CMS,G1 经常 Full GC,程序卡顿严重。 - -**原因**:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。 - -**注意**:提早触发GC会增加老年代GC的频率。 - -配置参数: - -> //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小 -> -> -XX:CMSInitiatingOccupancyFraction -> -> //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65% -> -> -XX:G1MixedGCLiveThresholdPercent=65 - -5.8、调整 JVM本地内存大小 - -**现象**:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM - -**原因**: JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。 - -**注意**: 本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。 - -配置参数: - -> XX:MaxDirectMemorySize - -## 六、JVM调优实例 - -整理的一些JVM调优实例: - -**6.1、网站流量浏览量暴增后,网站反应页面响很慢** - -> 1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。 -> -> 2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。 -> -> 3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。 -> -> 4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。 -> -> 5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。 -> -> 6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。 -> -> 7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。 - -**6.2、后台导出数据引发的OOM** - -**问题描述:**公司的后台系统,偶发性的引发OOM异常,堆内存溢出。 - -> 1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。 -> -> 2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+ -> HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。 -> -> 3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。 -> -> 4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。 -> -> 5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。 -> -> 6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。 -> -> 7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。 - -**6.3、单个缓存数据过大导致的系统CPU飚高** - -> 1、系统发布后发现CPU一直飚高到600%,发现这个问题后首先要做的是定位到是哪个应用占用CPU高,通过top 找到了对应的一个java应用占用CPU资源600%。 -> -> 2、如果是应用的CPU飚高,那么基本上可以定位可能是锁资源竞争,或者是频繁GC造成的。 -> -> 3、所以准备首先从GC的情况排查,如果GC正常的话再从线程的角度排查,首先使用jstat -gc PID 指令打印出GC的信息,结果得到得到的GC 统计信息有明显的异常,应用在运行了才几分钟的情况下GC的时间就占用了482秒,那么问这很明显就是频繁GC导致的CPU飚高。 -> -> 4、定位到了是GC的问题,那么下一步就是找到频繁GC的原因了,所以可以从两方面定位了,可能是哪个地方频繁创建对象,或者就是有内存泄露导致内存回收不掉。 -> -> 5、根据这个思路决定把堆内存信息dump下来看一下,使用jmap -dump 指令把堆内存信息dump下来(堆内存空间大的慎用这个指令否则容易导致会影响应用,因为我们的堆内存空间才2G所以也就没考虑这个问题了)。 -> -> 6、把堆内存信息dump下来后,就使用visualVM进行离线分析了,首先从占用内存最多的对象中查找,结果排名第三看到一个业务VO占用堆内存约10%的空间,很明显这个对象是有问题的。 -> -> 7、通过业务对象找到了对应的业务代码,通过代码的分析找到了一个可疑之处,这个业务对象是查看新闻资讯信息生成的对象,由于想提升查询的效率,所以把新闻资讯保存到了redis缓存里面,每次调用资讯接口都是从缓存里面获取。 -> -> 8、把新闻保存到redis缓存里面这个方式是没有问题的,有问题的是新闻的50000多条数据都是保存在一个key里面,这样就导致每次调用查询新闻接口都会从redis里面把50000多条数据都拿出来,再做筛选分页拿出10条返回给前端。50000多条数据也就意味着会产生50000多个对象,每个对象280个字节左右,50000个对象就有13.3M,这就意味着只要查看一次新闻信息就会产生至少13.3M的对象,那么并发请求量只要到10,那么每秒钟都会产生133M的对象,而这种大对象会被直接分配到老年代,这样的话一个2G大小的老年代内存,只需要几秒就会塞满,从而触发GC。 -> -> 9、知道了问题所在后那么就容易解决了,问题是因为单个缓存过大造成的,那么只需要把缓存减小就行了,这里只需要把缓存以页的粒度进行缓存就行了,每个key缓存10条作为返回给前端1页的数据,这样的话每次查询新闻信息只会从缓存拿出10条数据,就避免了此问题的 产生。 - -**6.4、CPU经常100% 问题定位** - -问题分析:CPU高一定是某个程序长期占用了CPU资源。 - -1、所以先需要找出那个进行占用CPU高。 - -> top 列出系统各个进程的资源占用情况。 - -2、然后根据找到对应进行里哪个线程占用CPU高。 - -> top -Hp 进程ID 列出对应进程里面的线程占用资源情况 - -3、找到对应线程ID后,再打印出对应线程的堆栈信息 - -> printf "%x\n" PID 把线程ID转换为16进制。 -> -> jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。 - -4、最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。 - -> 查看是否有线程长时间的watting 或blocked -> -> 如果线程长期处于watting状态下, 关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。 - -**6.5、内存飚高问题定位** - -分析: 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。 - -1、先观察垃圾回收的情况 - -> jstat -gcPID 1000查看GC次数,时间等信息,每隔一秒打印一次。 -> -> jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。 - -如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。 - -2、导出堆内存文件快照 - -> jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到文件。 - -3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。 - -**6.6、数据分析平台系统频繁 Full GC** - -平台主要对用户在 App 中行为进行定时分析统计,并支持报表导出,使用 CMS GC 算法。 - -数据分析师在使用中发现系统页面打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后大约有 10% 的存活对象进入老年代。 - -原来是因为 Survivor 区空间设置过小,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进入老年代。 - -通过调大 Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进入老年代。 - -调整之后每次 Young GC 后进入老年代的存活对象稳定运行时仅几百 Kb,Full GC 频率大大降低。 - -**6.7、业务对接网关 OOM** - -网关主要消费 Kafka 数据,进行数据处理计算然后转发到另外的 Kafka 队列,系统运行几个小时候出现 OOM,重启系统几个小时之后又 OOM。 - -通过 jmap 导出堆内存,在 eclipse MAT 工具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致 OOM。 - -**6.8、鉴权系统频繁长时间 Full GC** - -系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过 Zabbix 的监控平台监控发现系统频繁发生长时间 Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了 - -## 七、一个具体的实战案例分析 - -**7.1 典型调优参数设置** - -> 服务器配置: 4cpu,8GB内存 ---- jvm调优实际上是设置一个合理大小的jvm堆内存(既不能太大,也不能太小) - -> -Xmx3550m 设置jvm堆内存最大值 (经验值设置: 根据压力测试,根据线上程序运行效果情况) -> -> -Xms3550m 设置jvm堆内存初始化大小,一般情况下必须设置此值和最大的最大的堆内存空间保持一致,防止内存抖动,消耗性能 -> -> -Xmn2g 设置年轻代占用的空间大小 -> -> -Xss256k 设置线程堆栈的大小;jdk5.0以后默认线程堆栈大小为1MB; 在相同的内存情况下,减小堆栈大小,可以使得操作系统创建更多的业务线程; - -jvm堆内存设置: - -> nohup java -Xmx3550m -Xms3550m -Xmn2g -Xss256k -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1 >< - -TPS性能曲线: - -![](https://pic.rmb.bdstatic.com/bjh/down/ee3949fc51c360330ece8f6729cd0560.png) - -**7.2 分析gc日志** - -如果需要分析gc日志,就必须使得服务gc输入gc详情到log日志文件中,然后使用相应gc日志分析工具来对日志进行分析即可; - -把gc详情输出到一个gc.log日志文件中,便于gc分析 - -> -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log - -Throughput: 业务线程执行时间 / (gc时间+业务线程时间) - -![](https://pic.rmb.bdstatic.com/bjh/down/25529b786b50b8b30067e6f721101369.png) - -分析gc日志,发现,一开始就发生了3次fullgc,很明显jvm优化参数的设置是有问题的; - -![](https://pic.rmb.bdstatic.com/bjh/down/e70f3ffedc03bc0b387ff860cb8c948b.png) - -查看fullgc发生问题原因: jstat -gcutil pid - -![](https://pic.rmb.bdstatic.com/bjh/down/0e135e59c1e140aaded9223edd2f0177.png) - -Metaspace持久代: 初始化分配大小20m , 当metaspace被占满后,必须对持久代进行扩容,如果metaspace每进行一次扩容,fullgc就需要执行一次;(fullgc回收整个堆空间,非常占用时间) - -调整gc配置: 修改永久代空间初始化大小: - -> nohup java -Xmx3550m -Xms3550m -Xmn2g -Xss256k -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1 >< - -经过调优后,fullgc现象已经消失了: - -![](https://pic.rmb.bdstatic.com/bjh/down/0f8e8b90a8f9d810759ae43b3b0ed1b9.png) - -**7.3 Young&Old比例** - -年轻代和老年代比例:1:2 参数:-XX:NewRetio = 4 , 表示年轻代(eden,s0,s1)和老年代所占比值为1:4 - -1) -XX:NewRetio = 4 - -![](https://pic.rmb.bdstatic.com/bjh/down/f8353c852a2f65626b5b3a23f9616208.png) - -年轻代分配的内存大小变小了,这样YGC次数变多了,虽然fullgc不发生了,但是YGC花费的时间更多了! - -2) -XX:NewRetio = 2 YGC发生的次数必然会减少;因为eden区域的大小变大了,因此YGC就会变少; - -![](https://pic.rmb.bdstatic.com/bjh/down/11ca145ee5a93f511feda69e749582cb.png) - -7.4 Eden&S0S1 - -为了进一步减少YGC, 可以设置 enden ,s 区域的比值大小; 设置方式: -XX:SurvivorRatio=8 - -1) 设置比值:8:1:1 - -![](https://pic.rmb.bdstatic.com/bjh/down/6330cbdd9acd6a5c18e91c5bea13cca2.png) - -2) Xmn2g 8:1:1 - -nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1 >< - -根据gc调优,垃圾回收次数,时间,吞吐量都是一个比较优的一个配置; - -![](https://pic.rmb.bdstatic.com/bjh/down/68edd8bfceb7d5e85d18a53e150cad2a.png) - -**7.5 吞吐量优先** - -使用并行的垃圾回收器,可以充分利用多核心cpu来帮助进行垃圾回收;这样的gc方式,就叫做吞吐量优先的调优方式 - -垃圾回收器组合: ps(parallel scavenge) + po (parallel old) 此垃圾回收器是Jdk1.8 默认的垃圾回收器组合; - -> nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:+UseParallelGC -XX:UseParallelOldGC -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1 >< - -**7.6 响应时间优先** - -使用cms垃圾回收器,就是一个响应时间优先的组合; cms垃圾回收器(垃圾回收和业务线程交叉执行,不会让业务线程进行停顿stw)尽可能的减少stw的时间,因此使用cms垃圾回收器组合,是响应时间优先组合 - -> nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:+UseParNewGC -XX:UseConcMarkSweepGC -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1 >< - -可以发现,cms垃圾回收器时间变长; - -**7.7 g1** - -配置方式如下所示: - -> nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1 >< - -![](https://pic.rmb.bdstatic.com/bjh/down/4146281952d45f65334e7ba97c6ba873.png) - - -## 参考资料 - -* [Java HotSpot™ Virtual Machine Performance Enhancements](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/performance-enhancements-7.html) -* [Java HotSpot Virtual Machine Garbage Collection Tuning Guide](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html) -* [[HotSpot VM] JVM调优的”标准参数”的各种陷阱](http://hllvm.group.iteye.com/group/topic/27945) - - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\346\200\247\350\203\275\347\256\241\347\220\206\347\245\236\345\231\250VisualVM\344\273\213\347\273\215\344\270\216\345\256\236\346\210\230.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\346\200\247\350\203\275\347\256\241\347\220\206\347\245\236\345\231\250VisualVM\344\273\213\347\273\215\344\270\216\345\256\236\346\210\230.md" deleted file mode 100644 index e2547b3..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\346\200\247\350\203\275\347\256\241\347\220\206\347\245\236\345\231\250VisualVM\344\273\213\347\273\215\344\270\216\345\256\236\346\210\230.md" +++ /dev/null @@ -1,247 +0,0 @@ -# 目录 - * [一、VisualVM是什么?](#一、visualvm是什么?) - * [二、如何获取VisualVM?](#二、如何获取visualvm?) - * [三、获取那个版本?](#三、获取那个版本?) - * [四、VisualVM能做什么?](#四、visualvm能做什么?) - * [监控远程主机上的JAVA应用程序](#监控远程主机上的java应用程序) - * [排查JAVA应用程序内存泄漏](#排查java应用程序内存泄漏) - * [查找JAVA应用程序耗时的方法函数](#查找java应用程序耗时的方法函数) - * [排查JAVA应用程序线程锁](#排查java应用程序线程锁) - * [参考文章](#参考文章) - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 一、VisualVM是什么? - VisualVM是一款免费的JAVA虚拟机图形化监控分析工具。 - 1. 拥有图形化的监控界面。 - 2. 提供本地、远程的JVM监控分析功能。 - 3. 是一款免费的JAVA工具。 - 4. VisualVM拥有丰富的插件支持。 -## 二、如何获取VisualVM? - VisualVM官方网站:http://visualvm.java.net/ - - VisualVM各版本下载页面: http://visualvm.java.net/releases.html - - 下载VisualVM时也应该注意,不同的JDK版本对应不同版本的VisualVM,具体根据安装的JDK版本来下载第一的VisualVM。 -## 三、获取那个版本? - - 下载版本参考:Java虚拟机性能管理神器 - VisualVM(4) - JDK版本与VisualVM版本对应关系 - -备注:下列表中显示1.3.6版本只适合JDK7和JDK8,可是我用1.3.6版还是可以监控JDK1.6_45的版本。 - -## 四、VisualVM能做什么? - -1. 显示JAVA应用程序配置和运行时环境。 -显示JAVA应用程序JVM参数,系统属性,JVM的信息和运行环境。 - - -2. 显示本地和远程JAVA应用程序运行状态。 -可以连接到远程服务器上运行的JAVA应用程序,监控应用程序的运行状态。 - -3. 监控应用程序的性能消耗。 -可以监控到应用程序热点方法的执行单次时间、总耗时、耗时占比。 - - -4. 显示应用程序内存分配,显示分析堆信息。 -显示应用程序在运行时的编译时间、加载时间、垃圾回收时间、内存区域的回收状态等。 - - -5. 监控应用程序线程状态和生命周期。 -监控应用程序线程的运行、休眠、等待、锁定状态。 - - -6. 显示、分析线程堆信息。 -显示线程当前运行状态和关联类信息。 - - -7. 支持第三方插件来分析JAVA应用程序。 -另外还提供更多更强大、方便的第三方插件。 - - -### 监控远程主机上的JAVA应用程序 - - 使用VisualVM监控远程主机上JAVA应用程序时,需要开启远程主机上的远程监控访问,或者在远程JAVA应用程序启动时,开启远程监控选项,两种方法,选择其中一种就可以开启远程监控功能,配置完成后就可以在本地对远程主机上的JAVA应用程序进行监控。 - -1.远程服务器、应用程序配置 - 1.1配合jstatd工具提供监控数据 - 1.1.1创建安全访问文件 - 在JAVA_HOME/bin目录中,创建名称为jstatdAllPolicy文件(这个文件名称也可以顺便起,不过要与jstatd启动时指定名称相同),将以下内容拷贝到文件中。并保证文件的权限和用户都正确。 - - grant codebase"file:${java.home}/../lib/tools.jar"{ permission java.security.AllPermission; }; - - - -1.1.2启动jstatd服务 - 在JAVA_HOME/bin目录中,执行以下命令: - - ./jstatd -J-Djava.security.policy=jstatdAllPolicy-p 1099 -J-Djava.rmi.server.hostname=192.168.xxx.xxx - - - - jstatd命令描述以及参数说明: - - jstatd是一个基于RMI(Remove Method Invocation)的服务程序,它用于监控基于HotSpot的JVM中资源的创建及销毁,并且提供了一个远程接口允许远程的监控工具连接到本地的JVM执行命令。 - - - - -J-Djava.security.policy=jstatdAllPolicy 指定安全策略文件名称 - - -p 1099 指定启动端口 - - -J-Djava.rmi.server.hostname=192.168.xxx.xxx 指定本机IP地址,在hosts文件配置不正常时使用,最好加上。 - - -1.2JVM启动时配置远程监控选项 - 在需要远程监控的JVM启动时,开启远程监控选项 - - -Dcom.sun.management.jmxremote.port=1099 - -Dcom.sun.management.jmxremote.ssl=false - -Dcom.sun.management.jmxremote.authenticate=false - -Djava.rmi.server.hostname=192.168.xxx.xxx - - - -2.本地VisualVM配置 - 在本地VisualVM的应用程序窗口,右键单击【远程】》【添加远程主机】》【主机名】中输入远程主机的IP地址,点击【高级设置】输入远程主机开启的监控端口,点击【确定】完成配置。 - - - - 如果一切正常,就可以看到远程主机上的JAVA应用程序了。 - - - - - -## 排查JAVA应用程序内存泄漏 - -1. 发现问题 - 线上应用部署完成后,运行1~2天左右就会出现假死,或者某天早上8~10点高峰期间突然不处理数据了。由于在测试环境的压力测试没有做完全,也没有遇到相关问题。情况出现后对客户的使用造成很大影响,领导要求赶紧排查出问题原因! - -2. 排查原因 - 排查原因前,与运维沟通,了解线上服务器的运行状态,通过ganglila观察网络、CPU、内存、磁盘的运行历史状态,发现程序故障前,都有一波很高的负载,排查线上日志,负载来源在8~9点平台接入数据量成倍增加,通过与产品和市场人员分析,此时段是用户集中上班、接入平台的高峰时段,访问日志也显示,业务场景正常,无网络攻击和安全问题。属于产品业务正常的场景。 - - 排除了网络安全因素后,就从程序的运行内部进行排查,首先想到的获取JVM的dmp文件。获取JVM的dmp文件有两中方式: - - 1. JVM启动时增加两个参数,出现 OOME 时生成堆 dump: - - -XX:+HeapDumpOnOutOfMemoryError - - 生成堆文件地址: - - -XX:HeapDumpPath=/home/test/jvmlogs/ - - 2. 发现程序异常前通过执行指令,直接生成当前JVM的dmp文件,15434是指JVM的进程号 - - jmap -dump:format=b,file=serviceDump.dat 15434 - - 由于第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dmp文件,实时性不高,第二种方式在执行时,JVM是暂停服务的,所以对线上的运行会产生影响。所以建议第一种方式。 - -3. 解决方案 - 获取到dmp文件后,就开始进行分析。将服务器上的dmp文件拷贝到本地,然后启动本地的VisualVM,点击菜单栏【文件】选项,装入dmp文件 - - - - 打开dmp文件后,查看类标签,就能看到占用内存的一个排行。 - - - - 然后通过检查中查找最大的对象,排查到具体线程和对象。 - - - - - - 上列中的com.ctfo.trackservice.handler.TrackHandleThread#4就是重点排查对象。 - - 通过代码的比对,在此线程中,有调用DAO接口,负责将数据存储到数据库中。而存储到数据库中时,由于存储速度较慢,导致此线程中的数据队列满了,数据积压,无法回收导致了队列锁定,结果就是程序假死,不处理数据。 - - - - 通过进一步分析,发现数据库存储时有瓶颈,虽然当前是批量提交,速度也不快。平均8000/秒的存储速度。而数据库有一个DG(备份)节点,采用的是同步备份方式,即主库事务要等DG的事务也完成后才能返回成功,这样就会因为网络因素、DG性能因素等原因导致性能下降。通过与DBA、产品、沟通,将同步备份改为异步备份,实时同步改为异步(异步可能会导致主备有10分钟以内的数据延迟)。速度达到30000/秒。问题解决。 - - 至此,通过VisualVM分析java程序内存泄漏到此结束。不过还有几个问题:1. 如果dmp文件较大,VisualVM分析时间可能很久;另外,VisualVM对堆的分析显示功能还不算全面。如果需要更全面的显示,就可以使用另外一个专业的dmp文件分析工具【Memory Analyzer (MAT)】,此工具可以作为eclipse的插件进行安装,也可以单独下载使用。如果有感兴趣的朋友,我个人建议还是单独下载使用。下载地址:http://www.eclipse.org/mat/ - - - -## 查找JAVA应用程序耗时的方法函数 - -1.为什么要监控? - JAVA程序在开发前,根据设计文档的性能需求,是要对程序的性能指标进行测试的。比如接口每秒响应次数要求1000次/秒,就需要平均每次请求处理的时间在1ms以内,如果需要满足这个指标,就需要在开发阶段对接口执行函数进行监控,也可以通过打印日志进行监控,从而统计对应的性能指标,然后可以根据性能指标的要求进行相应优化。 - -2. 那些方法函数需要监控? - 根据具体业务的场景和需求,主要集中在IO通讯、文件读写、数据库操作、业务逻辑处理上,这些都是制约性能的重要因素,所以需要重点关注。 - - - -3. 如何排查 - 在研发环境,大部分会使用syso的方式或者日志方式打印性能损耗,如果代码没有加在运行时才想起来,或者想关注突然想起的函数,换做以前,是需要重启服务的,如果有VisualVM就可以直接查看耗时以及调用次数等情况。而不用打印、输出日志来查看性能损耗。 - - - -4. 如何处理 - 对于性能损耗的函数,根据业务逻辑可以进行相应的优化,例如字符串处理、文件读写方式、SQL语句优化、多线程处理等等方式。 - - 由于性能优化涉及的内容很多,这里就不深入了。主要是告诉大家通过VisualVM来排查问题的具体位置。 - - - - -## 排查JAVA应用程序线程锁 - - - -1. JAVA应用程序线程锁原因 - JAVA线程锁的例子和原因网上一大堆,我也不在这里深入说明,这里主要是否讲如何使用VisualVM进行排查。至于例子可以看这里:http://blog.csdn.net/fengzhe0411/article/details/6953370 - -这个例子比较极端,一般情况下,出现锁竞争激烈是比较常见的。 - -2. 排查JAVA应用程序线程锁 - 启动 VisualVM,在应用程序窗口,选择对应的JAVA应用,在详情窗口》线程标签(勾选线程可视化),查看线程生命周期状态,主要留意线程生命周期中红色部分。 - - - -(1)绿色:代表运行状态。一般属于正常情况。如果是多线程环境,生产者消费者模式下,消费者一直处于运行状态,说明消费者处理性能低,跟不上生产者的节奏,需要优化对应的代码,如果不处理,就可能导致消费者队列阻塞的现象。对应线程的【RUNNABLE】状态。 - -(2)蓝色:代表线程休眠。线程中调用Thread.sleep()函数的线程状态时,就是蓝色。对应线程的【TIMED_WAITING】状态。 - -(3)黄色:代表线程等待。调用线程的wait()函数就会出现黄色状态。对应线程的【WAITING】状态。 - -(4)红色:代码线程锁定。对应线程的【BLOCKED】状态。 - - - -3. 分析解决JAVA应用程序线程锁 - 发生线程锁的原因有很多,我所遇到比较多的情况是多线程同时访问同一资源,且此资源使用synchronized关键字,导致一个线程要等另外一个线程使用完资源后才能运行。例如再没有连接池的情况下,同时访问数据库接口。这种情况会导致性能的极具下降,解决的方案是增加连接池,或者修改访问方式。或者将资源粒度细化,类似ConCurrentHashMap中的处理方式,将资源分为多个更小粒度的资源,在更小粒度资源上来处理锁,就可以解决资源竞争激烈的问题。] - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\347\233\221\346\216\247\345\267\245\345\205\267\344\270\216\350\257\212\346\226\255\345\256\236\350\267\265.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\347\233\221\346\216\247\345\267\245\345\205\267\344\270\216\350\257\212\346\226\255\345\256\236\350\267\265.md" deleted file mode 100644 index 1d57b95..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232JVM\347\233\221\346\216\247\345\267\245\345\205\267\344\270\216\350\257\212\346\226\255\345\256\236\350\267\265.md" +++ /dev/null @@ -1,392 +0,0 @@ -# 目录 - * [一、jvm常见监控工具&指令](#一、jvm常见监控工具指令) - * [1、 jps:jvm进程状况工具](#1、-jpsjvm进程状况工具) - * [2、jstat: jvm统计信息监控工具](#2、jstat-jvm统计信息监控工具) - * [3、jinfo: java配置信息](#3、jinfo:-java配置信息) - * [4、jmap: java 内存映射工具](#4、jmap-java-内存映射工具) - * [5、jhat:jvm堆快照分析工具](#5、jhatjvm堆快照分析工具) - * [6、jstack:java堆栈跟踪工具](#6、jstackjava堆栈跟踪工具) - * [二、可视化工具](#二、可视化工具) - * [三、应用](#三、应用) - * [1、cpu飙升](#1、cpu飙升) - * [2、线程死锁](#2、线程死锁) - * [2.查看java进程的线程快照信息](#2查看java进程的线程快照信息) - * [3、OOM内存泄露](#3、oom内存泄露) - * [参考文章](#参考文章) - - - -本文转自:https://juejin.im/post/59e6c1f26fb9a0451c397a8c - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -在常见的线上问题时候,我们多数会遇到以下问题: - -> * 内存泄露 -> * 某个进程突然cpu飙升 -> * 线程死锁 -> * 响应变慢...等等其他问题。 - -如果遇到了以上这种问题,在线下可以有各种本地工具支持查看,但到线上了,就没有这么多的本地调试工具支持,我们该如何基于监控工具来进行定位问题? - -我们一般会基于数据收集来定位,而数据的收集离不开监控工具的处理,比如:运行日志、异常堆栈、GC日志、线程快照、堆快照等。经常使用恰当的分析和监控工具可以加快我们的分析数据、定位解决问题的速度。以下我们将会详细介绍。 - -## 一、jvm常见监控工具&指令 - -### 1、 jps:jvm进程状况工具 - - - -``` -jps [options] [hostid] -``` - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222505.png) - -如果不指定hostid就默认为当前主机或服务器。 - -命令行参数选项说明如下: - - - -``` --q 不输出类名、Jar名和传入main方法的参数 - -- l 输出main类或Jar的全限名 - --m 输出传入main方法的参数 - -- v 输出传入JVM的参数复制代码 -``` - - - -### 2、jstat: jvm统计信息监控工具 - -jstat 是用于见识虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、jit编译等运行数据,它是线上定位jvm性能的首选工具。 - -命令格式: - - - -``` -jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ] - -generalOption - 单个的常用的命令行选项,如-help, -options, 或 -version。 - -outputOptions -一个或多个输出选项,由单个的statOption选项组成,可以和-t, -h, and -J等选项配合使用。复制代码 -``` -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222543.png) - -参数选项: - -| Option | Displays | Ex | -| --- | --- | --- | -| [class](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#class_option) | 用于查看类加载情况的统计 | jstat -class pid:显示加载class的数量,及所占空间等信息。 | -| [compiler](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#compiler_option) | 查看HotSpot中即时编译器编译情况的统计 | jstat -compiler pid:显示VM实时编译的数量等信息。 | -| [gc](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gc_option) | 查看JVM中堆的垃圾收集情况的统计 | jstat -gc pid:可以显示gc的信息,查看gc的次数,及时间。其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。 | -| [gccapacity](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gccapacity_option) | 查看新生代、老生代及持久代的存储容量情况 | jstat -gccapacity:可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小 | -| [gccause](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gccause_option) | 查看垃圾收集的统计情况(这个和-gcutil选项一样),如果有发生垃圾收集,它还会显示最后一次及当前正在发生垃圾收集的原因。 | jstat -gccause:显示gc原因 | -| [gcnew](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gcnew_option) | 查看新生代垃圾收集的情况 | jstat -gcnew pid:new对象的信息 | -| [gcnewcapacity](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gcnewcapacity_option) | 用于查看新生代的存储容量情况 | jstat -gcnewcapacity pid:new对象的信息及其占用量 | -| [gcold](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gcold_option) | 用于查看老生代及持久代发生GC的情况 | jstat -gcold pid:old对象的信息 | -| [gcoldcapacity](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gcoldcapacity_option) | 用于查看老生代的容量 | jstat -gcoldcapacity pid:old对象的信息及其占用量 | -| [gcpermcapacity](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gcpermcapacity_option) | 用于查看持久代的容量 | jstat -gcpermcapacity pid: perm对象的信息及其占用量 | -| [gcutil](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#gcutil_option) | 查看新生代、老生代及持代垃圾收集的情况 | jstat -util pid:统计gc信息统计 | -| [printcompilation](http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html#printcompilation_option) | HotSpot编译方法的统计 | jstat -printcompilation pid:当前VM执行的信息 | - -**例如**: - -查看gc 情况执行:jstat-gcutil 27777 - - -### 3、jinfo: java配置信息 - -命令格式: - - - -``` -jinfo[option] pid复制代码 -``` - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222602.png) - -比如:获取一些当前进程的jvm运行和启动信息。 - - -### 4、jmap: java 内存映射工具 - -jmap命令用于生产堆转存快照。打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)。 - -命令格式: - - - -``` -jmap [ option ] pid - -jmap [ option ] executable core - -jmap [ option ] [server-id@]remote-hostname-or-IP复制代码 -``` -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222621.png) - - -参数选项: - - - -``` --dump:[live,]format=b,file= 使用hprof二进制形式,输出jvm的heap内容到文件=. live子选项是可选的,假如指定live选项,那么只输出活的对象到文件. - --finalizerinfo 打印正等候回收的对象的信息. - --heap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况. - --histo[:live] 打印每个class的实例数目,内存占用,类全名信息. VM的内部类名字开头会加上前缀”*”. 如果live子参数加上后,只统计活的对象数量. - --permstat 打印classload和jvm heap长久层的信息. 包含每个classloader的名字,活泼性,地址,父classloader和加载的class数量. 另外,内部String的数量和占用内存数也会打印出来. - --F 强迫.在pid没有相应的时候使用-dump或者-histo参数. 在这个模式下,live子参数无效. - --h | -help 打印辅助信息 - --J 传递参数给jmap启动的jvm. 复制代码 -``` - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222638.png) -### 5、jhat:jvm堆快照分析工具 - -jhat 命令与jamp搭配使用,用来分析map生产的堆快存储快照。jhat内置了一个微型http/Html服务器,可以在浏览器找那个查看。不过建议尽量不用,既然有dumpt文件,可以从生产环境拉取下来,然后通过本地可视化工具来分析,这样既减轻了线上服务器压力,有可以分析的足够详尽(比如 MAT/jprofile/visualVm)等。 - - -### 6、jstack:java堆栈跟踪工具 - -jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 - -命令格式: - - - -``` -jstack [ option ] pid - -jstack [ option ] executable core - -jstack [ option ] [server-id@]remote-hostname-or-IP复制代码 -``` - - - -参数: - - - -``` --F当’jstack [-l] pid’没有相应的时候强制打印栈信息 - --l长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表. - --m打印java和native c/c++框架的所有栈信息. - --h | -help打印帮助信息 - -pid 需要被打印配置信息的java进程id,可以用jps查询.复制代码 -``` - - - -后续的查找耗费最高cpu例子会用到。 - -## 二、可视化工具 - -对jvm监控的常见可视化工具,除了jdk本身提供的Jconsole和visualVm以外,还有第三方提供的jprofilter,perfino,Yourkit,Perf4j,JProbe,MAT等。这些工具都极大的丰富了我们定位以及优化jvm方式。 - -这些工具的使用,网上有很多教程提供,这里就不再过多介绍了。对于VisualVm来说,比较推荐使用,它除了对jvm的侵入性比较低以外,还是jdk团队自己开发的,相信以后功能会更加丰富和完善。jprofilter对于第三方监控工具,提供的功能和可视化最为完善,目前多数ide都支持其插件,对于上线前的调试以及性能调优可以配合使用。 - -另外对于线上dump的heap信息,应该尽量拉去到线下用于可视化工具来分析,这样分析更详细。如果对于一些紧急的问题,必须需要通过线上监控,可以采用 VisualVm的远程功能来进行,这需要使用tool.jar下的MAT功能。 - -## 三、应用 - -### 1、cpu飙升 - -在线上有时候某个时刻,可能会出现应用某个时刻突然cpu飙升的问题。对此我们应该熟悉一些指令,快速排查对应代码。 - -**_1.找到最耗CPU的进程_** - - - -``` -指令:top复制代码 -``` - - - - -**_2.找到该进程下最耗费cpu的线程_** - - - -``` -指令:top -Hp pid复制代码 -``` - - - - - -**_3.转换进制_** - - - -``` -printf “%x\n” 15332 // 转换16进制(转换后为0x3be4) 复制代码 -``` - - -**_4.过滤指定线程,打印堆栈信息_** - - - -``` -指令: -jstack pid |grep 'threadPid' -C5 --color - -jstack 13525 |grep '0x3be4' -C5 --color // 打印进程堆栈 并通过线程id,过滤得到线程堆栈信息。复制代码 -``` - -可以看到是一个上报程序,占用过多cpu了(以上例子只为示例,本身耗费cpu并不高) - -### 2、线程死锁 - -有时候部署场景会有线程死锁的问题发生,但又不常见。此时我们采用jstack查看下一下。比如说我们现在已经有一个线程死锁的程序,导致某些操作waiting中。 - -**_1.查找java进程id_** - - - -``` -指令:top 或者 jps 复制代码 -``` - - -### 2.查看java进程的线程快照信息 - - - -``` -指令:jstack -l pid复制代码 -``` - -从输出信息可以看到,有一个线程死锁发生,并且指出了那行代码出现的。如此可以快速排查问题。 - -### 3、OOM内存泄露 - -java堆内的OOM异常是实际应用中常见的内存溢出异常。一般我们都是先通过内存映射分析工具(比如MAT)对dump出来的堆转存快照进行分析,确认内存中对象是否出现问题。 - -当然了出现OOM的原因有很多,并非是堆中申请资源不足一种情况。还有可能是申请太多资源没有释放,或者是频繁频繁申请,系统资源耗尽。针对这三种情况我需要一一排查。 - -OOM的三种情况: - -> 1.申请资源(内存)过小,不够用。 -> -> 2.申请资源太多,没有释放。 -> -> 3.申请资源过多,资源耗尽。比如:线程过多,线程内存过大等。 - -**1.排查申请申请资源问题。** - - - -``` -指令:jmap -heap 11869 复制代码 -``` - -查看新生代,老生代堆内存的分配大小以及使用情况,看是否本身分配过小。 - - -从上述排查,发现程序申请的内存没有问题。 - -**2.排查gc** - -特别是fgc情况下,各个分代内存情况。 - - - -``` -指令:jstat -gcutil 11938 1000 每秒输出一次gc的分代内存分配情况,以及gc时间复制代码 -``` - - -**3.查找最费内存的对象** - - - -``` -指令: jmap -histo:live 11869 | more复制代码 -``` - -上述输出信息中,最大内存对象才161kb,属于正常范围。如果某个对象占用空间很大,比如超过了100Mb,应该着重分析,为何没有释放。 - -注意,上述指令: - - - -``` -jmap -histo:live 11869 | more - -执行之后,会造成jvm强制执行一次fgc,在线上不推荐使用,可以采取dump内存快照,线下采用可视化工具进行分析,更加详尽。 - -jmap -dump:format=b,file=/tmp/dump.dat 11869 - -或者采用线上运维工具,自动化处理,方便快速定位,遗失出错时间。复制代码 -``` - - -**4.确认资源是否耗尽** - -> * pstree 查看进程线程数量 -> * netstat 查看网络连接数量 - -或者采用: - -> * ll /proc/${PID}/fd | wc -l // 打开的句柄数 -> * ll /proc/${PID}/task | wc -l (效果等同pstree -p | wc -l) //打开的线程数 - -以上就是一些常见的jvm命令应用。 - -一种工具的应用并非是万能钥匙,包治百病,问题的解决往往是需要多种工具的结合才能更好的定位问题,无论使用何种分析工具,最重要的是熟悉每种工具的优势和劣势。这样才能取长补短,配合使用。 - - - - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\345\206\205\345\255\230\345\274\202\345\270\270\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\345\206\205\345\255\230\345\274\202\345\270\270\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265.md" deleted file mode 100644 index a999de2..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\345\206\205\345\255\230\345\274\202\345\270\270\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265.md" +++ /dev/null @@ -1,459 +0,0 @@ -# 目录 - * [实战内存溢出异常](#实战内存溢出异常) - * [1 . 对象的创建过程](#1--对象的创建过程) - * [2 . 对象的内存布局](#2--对象的内存布局) - * [3 . 对象的访问定位](#3--对象的访问定位) - * [4 .实战内存异常](#4-实战内存异常) - * [Java堆内存异常](#java堆内存异常) - * [Java栈内存异常](#java栈内存异常) - * [方法区内存异常](#方法区内存异常) - * [方法区与运行时常量池OOM](#方法区与运行时常量池oom) - * [附加-直接内存异常](#附加-直接内存异常) - * [Java内存泄漏](#java内存泄漏) - * [Java是如何管理内存?](#java是如何管理内存?) - * [什么是Java中的内存泄露?](#什么是java中的内存泄露?) - * [其他常见内存泄漏](#其他常见内存泄漏) - * [1、静态集合类引起内存泄露:](#1、静态集合类引起内存泄露:) - * [2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。](#2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。) - * [3、监听器](#3、监听器) - * [4、各种连接](#4、各种连接) - * [5、内部类和外部模块等的引用](#5、内部类和外部模块等的引用) - * [6、单例模式](#6、单例模式) - * [如何检测内存泄漏](#如何检测内存泄漏) - * [参考文章](#参考文章) - - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 实战内存溢出异常 - -大家好,相信大部分Javaer在code时经常会遇到本地代码运行正常,但在生产环境偶尔会莫名其妙的报一些关于内存的异常,StackOverFlowError,OutOfMemoryError异常是最常见的。今天就基于上篇文章JVM系列之Java内存结构详解讲解的各个内存区域重点实战分析下内存溢出的情况。在此之前,我还是想多余累赘一些其他关于对象的问题,具体内容如下: - -> 文章结构: -> 对象的创建过程 -> 对象的内存布局 -> 对象的访问定位 -> 实战内存异常 - -## 1 . 对象的创建过程 - -关于对象的创建,第一反应是new关键字,那么本文就主要讲解new关键字创建对象的过程。 - -``` -Student stu =new Student("张三","18"); - -``` - -就拿上面这句代码来说,虚拟机首先会去检查Student这个类有没有被加载,如果没有,首先去加载这个类到方法区,然后根据加载的Class类对象创建stu实例对象,需要注意的是,stu对象所需的内存大小在Student类加载完成后便可完全确定。内存分配完成后,虚拟机需要将分配到的内存空间的实例数据部分初始化为零值,这也就是为什么我们在编写Java代码时创建一个变量不需要初始化。紧接着,虚拟机会对对象的对象头进行必要的设置,如这个对象属于哪个类,如何找到类的元数据(Class对象),对象的锁信息,GC分代年龄等。设置完对象头信息后,调用类的构造函数。 -其实讲实话,虚拟机创建对象的过程远不止这么简单,我这里只是把大致的脉络讲解了一下,方便大家理解。 - -## 2 . 对象的内存布局 - -刚刚提到的实例数据,对象头,有些小伙伴也许有点陌生,这一小节就详细讲解一下对象的内存布局,对象创建完成后大致可以分为以下几个部分: - -* 对象头 -* 实例数据 -* 对齐填充 - -**对象头:**对象头中包含了对象运行时一些必要的信息,如GC分代信息,锁信息,哈希码,指向Class类元信息的指针等,其中对Javaer比较有用的是**锁信息与指向Class对象的指针**,关于锁信息,后期有机会讲解并发编程JUC时再扩展,关于指向Class对象的指针其实很好理解。比如上面那个Student的例子,当我们拿到stu对象时,调用Class stuClass=stu.getClass();的时候,其实就是根据这个指针去拿到了stu对象所属的Student类在方法区存放的Class类对象。虽然说的有点拗口,但这句话我反复琢磨了好几遍,应该是说清楚了。 - -**实例数据:**实例数据部分是对象真正存储的有效信息,就是程序代码中所定义的各种类型的字段内容。 - -**对齐填充:**虚拟机规范要求对象大小必须是8字节的整数倍。对齐填充其实就是来补全对象大小的。 - -## 3 . 对象的访问定位 - -谈到对象的访问,还拿上面学生的例子来说,当我们拿到stu对象时,直接调用stu.getName();时,其实就完成了对对象的访问。但这里要累赘说一下的是,stu虽然通常被认为是一个对象,其实准确来说是不准确的,stu只是一个变量,变量里存储的是指向对象的指针,(如果干过C或者C++的小伙伴应该比较清楚指针这个概念),当我们调用stu.getName()时,虚拟机会根据指针找到堆里面的对象然后拿到实例数据name.需要注意的是,当我们调用stu.getClass()时,虚拟机会首先根据stu指针定位到堆里面的对象,然后根据对象头里面存储的指向Class类元信息的指针再次到方法区拿到Class对象,进行了两次指针寻找。具体讲解图如下: -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223333.png) - -## 4 .实战内存异常 - -内存异常是我们工作当中经常会遇到问题,但如果仅仅会通过加大内存参数来解决问题显然是不够的,应该通过一定的手段定位问题,到底是因为参数问题,还是程序问题(无限创建,内存泄露)。定位问题后才能采取合适的解决方案,而不是一内存溢出就查找相关参数加大。 - -> 概念 -> 内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。关于GCRoot概念,下一篇文章讲解。 -> 内存溢出: 虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。 - -在分析问题之前先给大家讲一讲排查内存溢出问题的方法,内存溢出时JVM虚拟机会退出,**那么我们怎么知道JVM运行时的各种信息呢,Dump机制会帮助我们,可以通过加上VM参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出异常时生成dump文件,然后通过外部工具(作者使用的是VisualVM)来具体分析异常的原因。** - -下面从以下几个方面来配合代码实战演示内存溢出及如何定位: - -* Java堆内存异常 -* Java栈内存异常 -* 方法区内存异常 - -### Java堆内存异常 - -``` -/** - VM Args: - //这两个参数保证了堆中的可分配内存固定为20M - -Xms20m - -Xmx20m - //文件生成的位置,作则生成在桌面的一个目录 - -XX:+HeapDumpOnOutOfMemoryError //文件生成的位置,作则生成在桌面的一个目录 - //文件生成的位置,作则生成在桌面的一个目录 - -XX:HeapDumpPath=/Users/zdy/Desktop/dump/ - */ -public class HeapOOM { - //创建一个内部类用于创建对象使用 - static class OOMObject { - } - public static void main(String[] args) { - List list = new ArrayList(); - //无限创建对象,在堆中 - while (true) { - list.add(new OOMObject()); - } - } -} - -``` - -Run起来代码后爆出异常如下: - -java.lang.OutOfMemoryError: Java heap space -Dumping heap to /Users/zdy/Desktop/dump/java_pid1099.hprof … - -可以看到生成了dump文件到指定目录。并且爆出了OutOfMemoryError,还告诉了你是哪一片区域出的问题:heap space - -打开VisualVM工具导入对应的heapDump文件(如何使用请读者自行查阅相关资料),相应的说明见图: -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223348.png) - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223403.png) - -分析dump文件后,我们可以知道,OOMObject这个类创建了810326个实例。所以它能不溢出吗?接下来就在代码里找这个类在哪new的。排查问题。(我们的样例代码就不用排查了,While循环太凶猛了)分析dump文件后,我们可以知道,OOMObject这个类创建了810326个实例。所以它能不溢出吗?接下来就在代码里找这个类在哪new的。排查问题。(我们的样例代码就不用排查了,While循环太凶猛了) - -### Java栈内存异常 - -老实说,在栈中出现异常(StackOverFlowError)的概率小到和去苹果专卖店买手机,买回来后发现是Android系统的概率是一样的。因为作者确实没有在生产环境中遇到过,除了自己作死写样例代码测试。先说一下异常出现的情况,前面讲到过,方法调用的过程就是方法帧进虚拟机栈和出虚拟机栈的过程,那么有两种情况可以导致StackOverFlowError,当一个方法帧(比如需要2M内存)进入到虚拟机栈(比如还剩下1M内存)的时候,就会报出StackOverFlow.这里先说一个概念,栈深度:指目前虚拟机栈中没有出栈的方法帧。虚拟机栈容量通过参数-Xss来控制,下面通过一段代码,把栈容量人为的调小一点,然后通过递归调用触发异常。 - -``` -/** - * VM Args: - //设置栈容量为160K,默认1M - -Xss160k - */ -public class JavaVMStackSOF { - private int stackLength = 1; - public void stackLeak() { - stackLength++; - //递归调用,触发异常 - stackLeak(); - } - - public static void main(String[] args) throws Throwable { - JavaVMStackSOF oom = new JavaVMStackSOF(); - try { - oom.stackLeak(); - } catch (Throwable e) { - System.out.println("stack length:" + oom.stackLength); - throw e; - } - } -} - -``` - -> 结果如下: -> stack length:751 Exception in thread “main” -> java.lang.StackOverflowError - -可以看到,递归调用了751次,栈容量不够用了。 -默认的栈容量在正常的方法调用时,栈深度可以达到1000-2000深度,所以,一般的递归是可以承受的住的。如果你的代码出现了StackOverflowError,首先检查代码,而不是改参数。 - -这里顺带提一下,很多人在做多线程开发时,当创建很多线程时,**容易出现OOM(OutOfMemoryError),**这时可以通过具体情况,减少最大堆容量,或者栈容量来解决问题,这是为什么呢。请看下面的公式: - -线程数*(最大栈容量)+最大堆值+其他内存(忽略不计或者一般不改动)=机器最大内存 - -当线程数比较多时,且无法通过业务上削减线程数,那么再不换机器的情况下,**你只能把最大栈容量设置小一点,或者把最大堆值设置小一点。** - -### 方法区内存异常 - -写到这里时,作者本来想写一个无限创建动态代理对象的例子来演示方法区溢出,避开谈论JDK7与JDK8的内存区域变更的过渡,但细想一想,还是把这一块从始致终的说清楚。在上一篇文章中JVM系列之Java内存结构详解讲到方法区时提到,JDK7环境下方法区包括了(运行时常量池),其实这么说是不准确的。因为从JDK7开始,HotSpot团队就想到开始去"永久代",大家首先明确一个概念,**方法区和"永久代"(PermGen space)是两个概念,方法区是JVM虚拟机规范,任何虚拟机实现(J9等)都不能少这个区间,而"永久代"只是HotSpot对方法区的一个实现。**为了把知识点列清楚,我还是才用列表的形式: - -* [ ] JDK7之前(包括JDK7)拥有"永久代"(PermGen space),用来实现方法区。但在JDK7中已经逐渐在实现中把永久代中把很多东西移了出来,比如:符号引用(Symbols)转移到了native heap,运行时常量池(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap. -* [ ] 所以这就是为什么我说上一篇文章中说方法区中包含运行时常量池是不正确的,因为已经移动到了java heap; - **在JDK7之前(包括7)可以通过-XX:PermSize -XX:MaxPermSize来控制永久代的大小. - JDK8正式去除"永久代",换成Metaspace(元空间)作为JVM虚拟机规范中方法区的实现。** - 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但仍可以通过参数控制:-XX:MetaspaceSize与-XX:MaxMetaspaceSize来控制大小。 - -### 方法区与运行时常量池OOM - -Java 永久代是非堆内存的组成部分,用来存放类名、访问修饰符、常量池、字段描述、方法描述等,因运行时常量池是方法区的一部分,所以这里也包含运行时常量池。我们可以通过 jvm 参数 -XX:PermSize=10M -XX:MaxPermSize=10M 来指定该区域的内存大小,-XX:PermSize 默认为物理内存的 1/64 ,-XX:MaxPermSize 默认为物理内存的 1/4 。String.intern() 方法是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。在 JDK 1.6 及之前的版本中,由于常量池分配在永久代内,我们可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制方法区大小,从而间接限制其中常量池的容量,通过运行 java -XX:PermSize=8M -XX:MaxPermSize=8M RuntimeConstantPoolOom 下面的代码我们可以模仿一个运行时常量池内存溢出的情况: - -``` -import java.util.ArrayList; -import java.util.List; - -public class RuntimeConstantPoolOom { - public static void main(String[] args) { - List list = new ArrayList(); - int i = 0; - while (true) { - list.add(String.valueOf(i++).intern()); - } - } -} - -``` - -运行结果如下: - -``` -[root@9683817ada51 oom]# ../jdk1.6.0_45/bin/java -XX:PermSize=8m -XX:MaxPermSize=8m RuntimeConstantPoolOom -Exception in thread "main" java.lang.OutOfMemoryError: PermGen space - at java.lang.String.intern(Native Method) - at RuntimeConstantPoolOom.main(RuntimeConstantPoolOom.java:9) - -``` - -还有一种情况就是我们可以通过不停的加载class来模拟方法区内存溢出,《深入理解java虚拟机》中借助 CGLIB 这类字节码技术模拟了这个异常,我们这里使用不同的 classloader 来实现(同一个类在不同的 classloader 中是不同的),代码如下 - -``` -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.HashSet; -import java.util.Set; - -public class MethodAreaOom { - public static void main(String[] args) throws MalformedURLException, ClassNotFoundException { - Set> classes = new HashSet>(); - URL url = new File("").toURI().toURL(); - URL[] urls = new URL[]{url}; - while (true) { - ClassLoader loader = new URLClassLoader(urls); - Class loadClass = loader.loadClass(Object.class.getName()); - classes.add(loadClass); - } - } -} - -``` - -``` -[root@9683817ada51 oom]# ../jdk1.6.0_45/bin/java -XX:PermSize=2m -XX:MaxPermSize=2m MethodAreaOom -Error occurred during initialization of VM -java.lang.OutOfMemoryError: PermGen space - at sun.net.www.ParseUtil.(ParseUtil.java:31) - at sun.misc.Launcher.getFileURL(Launcher.java:476) - at sun.misc.Launcher$ExtClassLoader.getExtURLs(Launcher.java:187) - at sun.misc.Launcher$ExtClassLoader.(Launcher.java:158) - at sun.misc.Launcher$ExtClassLoader$1.run(Launcher.java:142) - at java.security.AccessController.doPrivileged(Native Method) - at sun.misc.Launcher$ExtClassLoader.getExtClassLoader(Launcher.java:135) - at sun.misc.Launcher.(Launcher.java:55) - at sun.misc.Launcher.(Launcher.java:43) - at java.lang.ClassLoader.initSystemClassLoader(ClassLoader.java:1337) - at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1319) - -``` - -在 jdk1.8 上运行上面的代码将不会出现异常,因为 jdk1.8 已结去掉了永久代,当然 -XX:PermSize=2m -XX:MaxPermSize=2m 也将被忽略,如下 - -``` -[root@9683817ada51 oom]# java -XX:PermSize=2m -XX:MaxPermSize=2m MethodAreaOom -Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=2m; support was removed in 8.0 -Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=2m; support was removed in 8.0 - -``` - -jdk1.8 使用元空间( Metaspace )替代了永久代( PermSize ),因此我们可以在 1.8 中指定 Metaspace 的大小模拟上述情况 - -``` -[root@9683817ada51 oom]# java -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m RuntimeConstantPoolOom -Error occurred during initialization of VM -java.lang.OutOfMemoryError: Metaspace - <> - -``` - -在JDK8的环境下将报出异常: -Exception in thread “main” java.lang.OutOfMemoryError: Metaspace -这是因为在调用CGLib的创建代理时会生成动态代理类,即Class对象到Metaspace,所以While一下就出异常了。 -**提醒一下:虽然我们日常叫"堆Dump",但是dump技术不仅仅是对于"堆"区域才有效,而是针对OOM的,也就是说不管什么区域,凡是能够报出OOM错误的,都可以使用dump技术生成dump文件来分析。** - -在经常动态生成大量Class的应用中,需要特别注意类的回收状况,这类场景除了例子中的CGLib技术,常见的还有,大量JSP,反射,OSGI等。需要特别注意,当出现此类异常,应该知道是哪里出了问题,然后看是调整参数,还是在代码层面优化。 - -### 附加-直接内存异常 - -直接内存异常非常少见,而且机制很特殊,因为直接内存不是直接向操作系统分配内存,而且通过计算得到的内存不够而手动抛出异常,所以当你发现你的dump文件很小,而且没有明显异常,只是告诉你OOM,你就可以考虑下你代码里面是不是直接或者间接使用了NIO而导致直接内存溢出。 - -## Java内存泄漏 - -Java的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄漏问题,或者认为即使有内存泄漏也不是程序的责任,而是GC或JVM的问题。其实,这种想法是不正确的,因为Java也存在内存泄露,但它的表现与C++不同。 - -随着越来越多的服务器程序采用Java技术,例如JSP,Servlet, EJB等,服务器程序往往长期运行。另外,在很多嵌入式系统中,内存的总量非常有限。内存泄露问题也就变得十分关键,即使每次运行少量泄漏,长期运行之后,系统也是面临崩溃的危险。 - -## Java是如何管理内存? - -为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。 - -监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。 - -为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。 - -以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。 -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223428.png) - -Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。 - -## 什么是Java中的内存泄露? - -下面,我们就可以描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。 - -在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。 - -通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。 -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223441.png) - -因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。 - -对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。 - -下面给出了一个简单的内存泄露的例子。在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个Vector中,如果我们仅仅释放引用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。 - -``` -Vector v=new Vector(10); -for (int i=1;i<100; i++) -{ - Object o=new Object(); - v.add(o); - o=null; -} -//此时,所有的Object对象都没有被释放,因为变量v引用这些对象 - -``` - -### 其他常见内存泄漏 - -#### 1、静态集合类引起内存泄露: - -像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。 -例: - -``` -Static Vector v = new Vector(10); -for (int i = 1; i<100; i++) { - Object o = new Object(); - v.add(o); - o = null; -}// -在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。 - -``` - -#### 2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。 - -例: - -``` -public static void main(String[] args) { - Set set = new HashSet(); - Person p1 = new Person("唐僧","pwd1",25); - Person p2 = new Person("孙悟空","pwd2",26); - Person p3 = new Person("猪八戒","pwd3",27); - set.add(p1); - set.add(p2); - set.add(p3); - System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素! - p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 - - set.remove(p3); //此时remove不掉,造成内存泄漏 - set.add(p3); //重新添加,居然添加成功 - System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! - for (Person person : set) { - System.out.println(person); - } -} - -``` - -#### 3、监听器 - -在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。 - -#### 4、各种连接 - -比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。 - -#### 5、内部类和外部模块等的引用 - -内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: -public void registerMsg(Object b); -这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。 - -#### 6、单例模式 - -不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子: - -``` -class A{ - public A(){ - B.getInstance().setA(this); - } -.... -} -//B类采用单例模式 -class B{ - private A a; - private static B instance=new B(); - public B(){} - public static B getInstance(){ - return instance; - } - public void setA(A a){ - this.a=a; - } - //getter... -} - -``` - -**显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况。** - -## 如何检测内存泄漏 - -最后一个重要的问题,就是如何检测Java的内存泄漏。目前,我们通常使用一些工具来检查Java程序的内存泄漏问题。市场上已有几种专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。 - -下面,我们将简单介绍Optimizeit的基本功能和工作原理。 - -Optimizeit Profiler版本4.11支持Application,Applet,Servlet和Romote Application四类应用,并且可以支持大多数类型的JVM,包括SUN JDK系列,IBM的JDK系列,和Jbuilder的JVM等。并且,该软件是由Java编写,因此它支持多种操作系统。Optimizeit系列还包括Thread Debugger和Code Coverage两个工具,分别用于监测运行时的线程状态和代码覆盖面。 - -当设置好所有的参数了,我们就可以在OptimizeIt环境下运行被测程序,在程序运行过程中,Optimizeit可以监视内存的使用曲线(如下图),包括JVM申请的堆(heap)的大小,和实际使用的内存大小。另外,在运行过程中,我们可以随时暂停程序的运行,甚至强行调用GC,让GC进行内存回收。通过内存使用曲线,我们可以整体了解程序使用内存的情况。这种监测对于长期运行的应用程序非常有必要,也很容易发现内存泄露。 - - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\345\255\227\350\212\202\347\240\201\344\273\213\347\273\215\344\270\216\350\247\243\346\236\220\345\256\236\350\267\265.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\345\255\227\350\212\202\347\240\201\344\273\213\347\273\215\344\270\216\350\247\243\346\236\220\345\256\236\350\267\265.md" deleted file mode 100644 index cf18680..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\345\255\227\350\212\202\347\240\201\344\273\213\347\273\215\344\270\216\350\247\243\346\236\220\345\256\236\350\267\265.md" +++ /dev/null @@ -1,359 +0,0 @@ -# 目录 - * [前言](#前言) - * [Class文件](#class文件) - * [什么是Class文件?](#什么是class文件?) - * [基本结构](#基本结构) - * [解析](#解析) - * [字段类型](#字段类型) - * [常量池](#常量池) - * [字节码指令](#字节码指令) - * [运行](#运行) - * [总结](#总结) - * [参考:](#参考:) - - -本文转自:https://juejin.im/post/589834a20ce4630056097a56 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - -## 前言 - -身为一个java程序员,怎么能不了解JVM呢,倘若想学习JVM,那就又必须要了解Class文件,Class之于虚拟机,就如鱼之于水,虚拟机因为Class而有了生命。《深入理解java虚拟机》中花了一整个章节来讲解Class文件,可是看完后,一直都还是迷迷糊糊,似懂非懂。正好前段时间看见一本书很不错:《自己动手写Java虚拟机》,作者利用go语言实现了一个简单的JVM,虽然没有完整实现JVM的所有功能,但是对于一些对JVM稍感兴趣的人来说,可读性还是很高的。作者讲解的很详细,每个过程都分为了一章,其中一部分就是讲解如何解析Class文件。 - -这本书不太厚,很快就读完了,读完后,收获颇丰。但是纸上得来终觉浅,绝知此事要躬行,我便尝试着自己解析Class文件。go语言虽然很优秀,但是终究不熟练,尤其是不太习惯其把类型放在变量之后的语法,还是老老实实用java吧。 - -**话不多说,先贴出项目地址:[github.com/HalfStackDe…](https://github.com/HalfStackDeveloper/ClassReader)** - -## Class文件 - -### 什么是Class文件? - -java之所以能够实现跨平台,便在于其编译阶段不是将代码直接编译为平台相关的机器语言,而是先编译成二进制形式的java字节码,放在Class文件之中,虚拟机再加载Class文件,解析出程序运行所需的内容。每个类都会被编译成一个单独的class文件,内部类也会作为一个独立的类,生成自己的class。 - -### 基本结构 - -随便找到一个class文件,用Sublime Text打开是这样的: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404220426.png) - -是不是一脸懵逼,不过java虚拟机规范中给出了class文件的基本格式,只要按照这个格式去解析就可以了: - -``` -ClassFile { - u4 magic; - u2 minor_version; - u2 major_version; - u2 constant_pool_count; - cp_info constant_pool[constant_pool_count-1]; - u2 access_flags; - u2 this_class; - u2 super_class; - u2 interfaces_count; - u2 interfaces[interfaces_count]; - u2 fields_count; - field_info fields[fields_count]; - u2 methods_count; - method_info methods[methods_count]; - u2 attributes_count; - attribute_info attributes[attributes_count]; -} -``` - -ClassFile中的字段类型有u1、u2、u4,这是什么类型呢?其实很简单,就是分别表示1个字节,2个字节和4个字节。 - -开头四个字节为:magic,是用来唯一标识文件格式的,一般被称作magic number(魔数),这样虚拟机才能识别出所加载的文件是否是class格式,class文件的魔数为cafebabe。不只是class文件,基本上大部分文件都有魔数,用来标识自己的格式。 - -接下来的部分主要是class文件的一些信息,如常量池、类访问标志、父类、接口信息、字段、方法等,具体的信息可参考《Java虚拟机规范》。 - -## 解析 - -### 字段类型 - -上面说到ClassFile中的字段类型有u1、u2、u4,分别表示1个字节,2个字节和4个字节的无符号整数。java中short、int、long分别为2、4、8个字节的有符号整数,去掉符号位,刚好可以用来表示u1、u2、u4。 - -``` -public class U1 { - public static short read(InputStream inputStream) { - byte[] bytes = new byte[1]; - try { - inputStream.read(bytes); - } catch (IOException e) { - e.printStackTrace(); - } - short value = (short) (bytes[0] & 0xFF); - return value; - } -} - -public class U2 { - public static int read(InputStream inputStream) { - byte[] bytes = new byte[2]; - try { - inputStream.read(bytes); - } catch (IOException e) { - e.printStackTrace(); - } - int num = 0; - for (int i= 0; i < bytes.length; i++) { - num <<= 8; - num |= (bytes[i] & 0xff); - } - return num; - } -} - -public class U4 { - public static long read(InputStream inputStream) { - byte[] bytes = new byte[4]; - try { - inputStream.read(bytes); - } catch (IOException e) { - e.printStackTrace(); - } - long num = 0; - for (int i= 0; i < bytes.length; i++) { - num <<= 8; - num |= (bytes[i] & 0xff); - } - return num; - } -} -``` - -### 常量池 - -定义好字段类型后,我们就可以读取class文件了,首先是读取魔数之类的基本信息,这部分很简单: - -``` -FileInputStream inputStream = new FileInputStream(file); -ClassFile classFile = new ClassFile(); -classFile.magic = U4.read(inputStream); -classFile.minorVersion = U2.read(inputStream); -classFile.majorVersion = U2.read(inputStream); -``` - -这部分只是热热身,接下来的大头在于常量池。解析常量池之前,我们先来解释一下常量池是什么。 - -常量池,顾名思义,存放常量的资源池,这里的常量指的是字面量和符号引用。字面量指的是一些字符串资源,而符号引用分为三类:类符号引用、方法符号引用和字段符号引用。通过将资源放在常量池中,其他项就可以直接定义成常量池中的索引了,避免了空间的浪费,不只是class文件,Android可执行文件dex也是同样如此,将字符串资源等放在DexData中,其他项通过索引定位资源。java虚拟机规范给出了常量池中每一项的格式: - -``` -cp_info { - u1 tag; - u1 info[]; -} -``` - - -由于格式太多,文章中只挑选一部分讲解: - -这里首先读取常量池的大小,初始化常量池: - -``` -//解析常量池 -int constant_pool_count = U2.read(inputStream); -ConstantPool constantPool = new ConstantPool(constant_pool_count); -constantPool.read(inputStream); -``` - -接下来再逐个读取每项内容,并存储到数组cpInfo中,这里需要注意的是,cpInfo[]下标从1开始,0无效,且真正的常量池大小为constant_pool_count-1。 - -``` -public class ConstantPool { - public int constant_pool_count; - public ConstantInfo[] cpInfo; - - public ConstantPool(int count) { - constant_pool_count = count; - cpInfo = new ConstantInfo[constant_pool_count]; - } - - public void read(InputStream inputStream) { - for (int i = 1; i < constant_pool_count; i++) { - short tag = U1.read(inputStream); - ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag); - constantInfo.read(inputStream); - cpInfo[i] = constantInfo; - if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) { - i++; - } - } - } -} -``` - -我们先来看看CONSTANT_Utf8格式,这一项里面存放的是MUTF-8编码的字符串: - -``` -CONSTANT_Utf8_info { - u1 tag; - u2 length; - u1 bytes[length]; -} -``` - -那么如何读取这一项呢? - -``` -public class ConstantUtf8 extends ConstantInfo { - public String value; - - @Override - public void read(InputStream inputStream) { - int length = U2.read(inputStream); - byte[] bytes = new byte[length]; - try { - inputStream.read(bytes); - } catch (IOException e) { - e.printStackTrace(); - } - try { - value = readUtf8(bytes); - } catch (UTFDataFormatException e) { - e.printStackTrace(); - } - } - - private String readUtf8(byte[] bytearr) throws UTFDataFormatException { - //copy from java.io.DataInputStream.readUTF() - } -} -``` - -很简单,首先读取这一项的字节数组长度,接着调用readUtf8(),将字节数组转化为String字符串。 - -再来看看CONSTANT_Class这一项,这一项存储的是类或者接口的符号引用: - -``` -CONSTANT_Class_info { - u1 tag; - u2 name_index; -} -``` - -注意这里的name_index并不是直接的字符串,而是指向常量池中cpInfo数组的name_index项,且cpInfo[name_index]一定是CONSTANT_Utf8格式。 - -``` -public class ConstantClass extends ConstantInfo { - public int nameIndex; - - @Override - public void read(InputStream inputStream) { - nameIndex = U2.read(inputStream); - } -} -``` - -常量池解析完毕后,就可以供后面的数据使用了,比方说ClassFile中的this_class指向的就是常量池中格式为CONSTANT_Class的某一项,那么我们就可以读取出类名: - -``` -int classIndex = U2.read(inputStream); -ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex]; -ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex]; -classFile.className = className.value; -System.out.print("classname:" + classFile.className + "\n"); -``` - -### 字节码指令 - -解析常量池之后还需要接着解析一些类信息,如父类、接口类、字段等,但是相信大家最好奇的还是java指令的存储,大家都知道,我们平时写的java代码会被编译成java字节码,那么这些字节码到底存储在哪呢?别急,讲解指令之前,我们先来了解下ClassFile中的method_info,其格式如下: - -``` -method_info { - u2 access_flags; - u2 name_index; - u2 descriptor_index; - u2 attributes_count; - attribute_info attributes[attributes_count]; -} -``` - -method_info里主要是一些方法信息:如访问标志、方法名索引、方法描述符索引及属性数组。这里要强调的是属性数组,因为字节码指令就存储在这个属性数组里。属性有很多种,比如说异常表就是一个属性,而存储字节码指令的属性为CODE属性,看这名字也知道是用来存储代码的了。属性的通用格式为: - -``` -attribute_info { - u2 attribute_name_index; - u4 attribute_length; - u1 info[attribute_length]; -} -``` - -根据attribute_name_index可以从常量池中拿到属性名,再根据属性名就可以判断属性种类了。 - -Code属性的具体格式为: - -``` -Code_attribute { - u2 attribute_name_index; u4 attribute_length; - u2 max_stack; - u2 max_locals; - u4 code_length; - u1 code[code_length]; - u2 exception_table_length; - { - u2 start_pc; - u2 end_pc; - u2 handler_pc; - u2 catch_type; - } exception_table[exception_table_length]; - u2 attributes_count; - attribute_info attributes[attributes_count]; -} -``` - -其中code数组里存储就是字节码指令,那么如何解析呢?每条指令在code[]中都是一个字节,我们平时javap命令反编译看到的指令其实是助记符,只是方便阅读字节码使用的,jvm有一张字节码与助记符的对照表,根据对照表,就可以将指令翻译为可读的助记符了。这里我也是在网上随便找了一个对照表,保存到本地txt文件中,并在使用时解析成HashMap。代码很简单,就不贴了,可以参考我代码中InstructionTable.java。 - -接下来我们就可以解析字节码了: - -``` -for (int j = 0; j < methodInfo.attributesCount; j++) { - if (methodInfo.attributes[j] instanceof CodeAttribute) { - CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j]; - for (int m = 0; m < codeAttribute.codeLength; m++) { - short code = codeAttribute.code[m]; - System.out.print(InstructionTable.getInstruction(code) + "\n"); - } - } -} -``` - -## 运行 - -整个项目终于写完了,接下来就来看看效果如何,随便找一个class文件解析运行: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404220510.png) - -哈哈,是不是很赞! - -**最后再贴一下项目地址:[github.com/HalfStackDe…](https://github.com/HalfStackDeveloper/ClassReader),欢迎Fork And Star!** - -## 总结 - -Class文件看起来很复杂,其实真正解析起来,也没有那么难,关键是要自己动手试试,才能彻底理解,希望各位看完后也能觉知此事要躬行! - -## 参考: - -[1\. 周志明《java虚拟机规范(JavaSE7)》](https://book.douban.com/subject/25792515/) - -[2\. 张秀宏《自己动手写Java虚拟机》](https://book.douban.com/subject/26802084/) - -[3\. 周志明《深入理解Java虚拟机(第2版)》](https://book.douban.com/subject/26802084/) - -**(如有错误,欢迎指正!)** - -**(转载请标明ID:半栈工程师,个人博客:[halfstackdeveloper.github.io](https://halfstackdeveloper.github.io/))** - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\347\232\204\347\274\226\350\257\221\346\234\237\344\274\230\345\214\226\344\270\216\350\277\220\350\241\214\346\234\237\344\274\230\345\214\226.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\347\232\204\347\274\226\350\257\221\346\234\237\344\274\230\345\214\226\344\270\216\350\277\220\350\241\214\346\234\237\344\274\230\345\214\226.md" deleted file mode 100644 index 58b8166..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232Java\347\232\204\347\274\226\350\257\221\346\234\237\344\274\230\345\214\226\344\270\216\350\277\220\350\241\214\346\234\237\344\274\230\345\214\226.md" +++ /dev/null @@ -1,222 +0,0 @@ -# 目录 - * [java编译期优化](#[java编译期优化]) - * [早期(编译期)优化](#早期(编译期)优化) - * [泛型与类型擦除](#泛型与类型擦除) - * [自动装箱、拆箱与遍历循环](#自动装箱、拆箱与遍历循环) - * [条件编译](#条件编译) - * [晚期(运行期)优化](#晚期(运行期)优化) - * [解释器与编译器](#解释器与编译器) - * [分层编译策略](#分层编译策略) - * [热点代码探测](#热点代码探测) - * [编译优化技术](#编译优化技术) - * [java与C/C++编译器对比](#java与cc编译器对比) - * [参考文章](#参考文章) - - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## java编译期优化 - -java语言的编译期其实是一段不确定的操作过程,因为它可以分为三类编译过程: -1.前端编译:把_.java文件转变为_.class文件 -2.后端编译:把字节码转变为机器码 -3.静态提前编译:直接把*.java文件编译成本地机器代码 -从JDK1.3开始,虚拟机设计团队就把对性能的优化集中到了后端的即时编译中,这样可以让那些不是由Javac产生的Class文件(如JRuby、Groovy等语言的Class文件)也能享受到编译期优化所带来的好处 -**Java中即时编译在运行期的优化过程对于程序运行来说更重要,而前端编译期在编译期的优化过程对于程序编码来说关系更加密切** - -### 早期(编译期)优化 - -``` -早期编译过程主要分为3个部分:1.解析与填充符号表过程:词法、语法分析;填充符号表 2.插入式注解处理器的注解处理过程 3.语义分析与字节码生成过程:标注检查、数据与控制流分析、解语法糖、字节码生成 -``` - -##### 泛型与类型擦除 - -Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换成原来的原生类型了,并且在相应的地方插入了强制转型代码 - -``` -泛型擦除前的例子 -public static void main( String[] args ) -{ - Map map = new HashMap(); - map.put("hello","你好"); - System.out.println(map.get("hello")); -} - -泛型擦除后的例子 -public static void main( String[] args ) -{ - Map map = new HashMap(); - map.put("hello","你好"); - System.out.println((String)map.get("hello")); -} -``` - -##### 自动装箱、拆箱与遍历循环 - -自动装箱、拆箱在编译之后会被转化成对应的包装和还原方法,如Integer.valueOf()与Integer.intValue(),而遍历循环则把代码还原成了迭代器的实现,变长参数会变成数组类型的参数。 -**然而包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们的equals()方法不处理数据转型的关系。** - -##### 条件编译 - -Java语言也可以进行条件编译,方法就是使用条件为常量的if语句,它在编译阶段就会被“运行”: - -``` -public static void main(String[] args) { - if(true){ - System.out.println("block 1"); - } - else{ - System.out.println("block 2"); - } -} - -编译后Class文件的反编译结果: -public static void main(String[] args) { - System.out.println("block 1"); -} -``` - -**只能是条件为常量的if语句,这也是Java语言的语法糖,根据布尔常量值的真假,编译器会把分支中不成立的代码块消除掉** - -### 晚期(运行期)优化 - -##### 解释器与编译器 - -Java程序最初是通过解释器进行解释执行的,当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译时间,立即执行;当程序运行后,随着时间的推移,编译期逐渐发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。**解释执行节约内存,编译执行提升效率。**同时,解释器可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,则通过逆优化退回到解释状态继续执行。 - -HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler(C1编译器)和Server Compiler(C2编译器),默认采用解释器与其中一个编译器直接配合的方式工作,使用哪个编译器取决于虚拟机运行的模式,也可以自己去指定。若强制虚拟机运行与“解释模式”,编译器完全不介入工作,若强制虚拟机运行于“编译模式”,则优先采用编译方式执行程序,解释器仍然要在编译无法进行的情况下介入执行过程。 -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222130.png) -##### 分层编译策略 - -``` -分层编译策略作为默认编译策略在JDK1.7的Server模式虚拟机中被开启,其中包括: -第0层:程序解释执行,解释器不开启性能监控功能,可触发第1层编译; -第1层:C1编译,将字节码编译成本地代码,进行简单可靠的优化,如有必要将加入性能监控的逻辑; -第2层:C2编译,也是将字节码编译成本地代码,但是会启动一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。 -实施分层编译后,C1和C2将会同时工作,C1获取更高的编译速度,C2获取更好的编译质量,在解释执行的时候也无须再承担性能监控信息的任务。 -``` - -##### 热点代码探测 - -``` -在运行过程中会被即时编译器编译的“热点代码”有两类: -1.被多次调用的方法:由方法调用触发的编译,属于JIT编译方式 -2.被多次执行的循环体:也以整个方法作为编译对象,因为编译发生在方法执行过程中,因此成为栈上替换(OSR编译) - -热点探测判定方式有两种: -1.基于采样的热点探测:虚拟机周期性的检查各个线程的栈顶,如果某个方法经常出现在栈顶,则判定为“热点方法”。(简单高效,可以获取方法的调用关系,但容易受线程阻塞或别的外界因素影响扰乱热点探测) -2.基于计数的热点探测:虚拟机为每个方法建立一个计数器,统计方法的执行次数,超过一定阈值就是“热点方法”。(需要为每个方法维护计数器,不能直接获取方法的调用关系,但是统计结果精确严谨) -``` - -HotSpot虚拟机使用的是第二种,它为每个方法准备了两类计数器:方法调用计数器和回边计数器,下图表示方法调用计数器触发即时编译: -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222225.png) -如果不做任何设置,执行引擎会继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成,下次调用才会使用已编译的版本。另外,方法调用计数器的值也不是一个绝对次数,而是一段时间之内被调用的次数,超过这个时间,次数就减半,这称为计数器热度的衰减。 - -下图表示回边计数器触发即时编译: -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404222324.png) -回边计数器没有计数器热度衰减的过程,因此统计的就是绝对次数,并且当计数器溢出时,它还会把方法计数器的值也调整到溢出状态,这样下次进入该方法的时候就会执行标准编译过程。 - -##### 编译优化技术 - -虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器之中,那么在编译器编译的过程中,到底做了些什么事情呢?下面将介绍几种最有代表性的优化技术: -**公共子表达式消除** -如果一个表达式E已经计算过了,并且先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共表达式,可以直接用之前的结果替换。 -例:int d = (c * b) * 12 + a + (a + b * c) => int d = E * 12 + a + (a + E) - -**数组边界检查消除** -Java语言中访问数组元素都要进行上下界的范围检查,每次读写都有一次条件判定操作,这无疑是一种负担。编译器只要通过数据流分析就可以判定循环变量的取值范围永远在数组长度以内,那么整个循环中就可以把上下界检查消除,这样可以省很多次的条件判断操作。 - -另一种方法叫做隐式异常处理,Java中空指针的判断和算术运算中除数为0的检查都采用了这个思路: - -``` -if(foo != null){ - return foo.value; -}else{ - throw new NullPointException(); -} - -使用隐式异常优化以后: -try{ - return foo.value; -}catch(segment_fault){ - uncommon_trap(); -} -当foo极少为空时,隐式异常优化是值得的,但是foo经常为空,这样的优化反而会让程序变慢,而HotSpot虚拟机会根据运行期收集到的Profile信息自动选择最优方案。 -``` - -**方法内联** -方法内联能去除方法调用的成本,同时也为其他优化建立了良好的基础,因此各种编译器一般会把内联优化放在优化序列的最靠前位置,然而由于Java对象的方法默认都是虚方法,因此方法调用都需要在运行时进行多态选择,为了解决虚方法的内联问题,首先引入了“类型继承关系分析(CHA)”的技术。 - -``` -1.在内联时,若是非虚方法,则可以直接内联 -2.遇到虚方法,首先根据CHA判断此方法是否有多个目标版本,若只有一个,可以直接内联,但是需要预留一个“逃生门”,称为守护内联,若在程序的后续执行过程中,加载了导致继承关系发生变化的新类,就需要抛弃已经编译的代码,退回到解释状态执行,或者重新编译。 -3.若CHA判断此方法有多个目标版本,则编译器会使用“内联缓存”,第一次调用缓存记录下方法接收者的版本信息,并且每次调用都比较版本,若一致则可以一直使用,若不一致则取消内联,查找虚方法表进行方法分派。 -``` - -**逃逸分析** -逃逸分析的基本行为就是分析对象动态作用域,当一个对象被外部方法所引用,称为方法逃逸;当被外部线程访问,称为线程逃逸。若能证明一个对象不会被外部方法或进程引用,则可以为这个变量进行一些优化: - -``` -1.栈上分配:如果确定一个对象不会逃逸,则可以让它分配在栈上,对象所占用的内存空间就可以随栈帧出栈而销毁。这样可以减小垃圾收集系统的压力。 -2.同步消除:线程同步相对耗时,如果确定一个变量不会逃逸出线程,那这个变量的读写不会有竞争,则对这个变量实施的同步措施也就可以消除掉。 -3.标量替换:如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那么程序真正执行的时候可以不创建这个对象,改为直接创建它的成员变量,这样就可以在栈上分配。 -``` - -**可是目前还不能保证逃逸分析的性能收益必定高于它的消耗,所以这项技术还不是很成熟。** - -### java与C/C++编译器对比 - -``` -Java虚拟机的即时编译器与C/C++的静态编译器相比,可能会由于下面的原因导致输出的本地代码有一些劣势: -1.即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,因此不敢随便引入大规模的优化技术; -2.Java语言是动态的类型安全语言,虚拟器需要频繁的进行动态检查,如空指针,上下界范围,继承关系等; -3.Java中使用虚方法频率远高于C++,则需要进行多态选择的频率远高于C++; -4.Java是可以动态扩展的语言,运行时加载新的类可能改变原有的继承关系,许多全局的优化措施只能以激进优化的方式来完成; -5.Java语言的对象内存都在堆上分配,垃圾回收的压力比C++大 - -然而,Java语言这些性能上的劣势换取了开发效率上的优势,并且由于C++编译器所有优化都是在编译期完成的,以运行期性能监控为基础的优化措施都无法进行,这也是Java编译器独有的优势。 -``` - - - - - - - - - - - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\345\206\215\350\260\210\345\233\233\347\247\215\345\274\225\347\224\250\345\217\212GC\345\256\236\350\267\265.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\345\206\215\350\260\210\345\233\233\347\247\215\345\274\225\347\224\250\345\217\212GC\345\256\236\350\267\265.md" deleted file mode 100644 index cca62d4..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\345\206\215\350\260\210\345\233\233\347\247\215\345\274\225\347\224\250\345\217\212GC\345\256\236\350\267\265.md" +++ /dev/null @@ -1,225 +0,0 @@ -# 目录 - * [一、背景](#一、背景) - * [二、简介](#二、简介) - * [1.强引用 StrongReference](#1强引用-strongreference) - * [2.弱引用 WeakReference](#2弱引用-weakreference) - * [3.软引用 SoftReference](#3软引用-softreference) - * [4.虚引用 PhantomReference](#4虚引用-phantomreference) - * [三、小结](#三、小结) - * [参考文章](#参考文章) - - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 一、背景 - -Java的内存回收不需要程序员负责,JVM会在必要时启动Java GC完成垃圾回收。Java以便我们控制对象的生存周期,提供给了我们四种引用方式,引用强度从强到弱分别为:强引用、软引用、弱引用、虚引用。 - -## 二、简介 - -### 1.强引用 StrongReference - -StrongReference是Java的默认引用形式,使用时不需要显示定义。任何通过强引用所使用的对象不管系统资源有多紧张,Java GC都不会主动回收具有强引用的对象。 -```` -public class StrongReferenceTest { - - public static int M = 1024*1024; - - public static void printlnMemory(String tag){ - Runtime runtime = Runtime.getRuntime(); - int M = StrongReferenceTest.M; - System.out.println("\n"+tag+":"); - System.out.println(runtime.freeMemory()/M+"M(free)/" + runtime.totalMemory()/M+"M(total)"); - } - - public static void main(String[] args){ - StrongReferenceTest.printlnMemory("1.原可用内存和总内存"); - - //实例化10M的数组并与strongReference建立强引用 - byte[] strongReference = new byte[10*StrongReferenceTest.M]; - StrongReferenceTest.printlnMemory("2.实例化10M的数组,并建立强引用"); - System.out.println("strongReference : "+strongReference); - - System.gc(); - StrongReferenceTest.printlnMemory("3.GC后"); - System.out.println("strongReference : "+strongReference); - - //strongReference = null;后,强引用断开了 - strongReference = null; - StrongReferenceTest.printlnMemory("4.强引用断开后"); - System.out.println("strongReference : "+strongReference); - - System.gc(); - StrongReferenceTest.printlnMemory("5.GC后"); - System.out.println("strongReference : "+strongReference); - } -} -```` - -运行结果: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223852.png) -### 2.弱引用 WeakReference - -如果一个对象只具有弱引用,无论内存充足与否,Java GC后对象如果只有弱引用将会被自动回收。 -```` -public class WeakReferenceTest { - - public static int M = 1024*1024; - - public static void printlnMemory(String tag){ - Runtime runtime = Runtime.getRuntime(); - int M = WeakReferenceTest.M; - System.out.println("\n"+tag+":"); - System.out.println(runtime.freeMemory()/M+"M(free)/" + runtime.totalMemory()/M+"M(total)"); - } - - public static void main(String[] args){ - WeakReferenceTest.printlnMemory("1.原可用内存和总内存"); - - //创建弱引用 - WeakReference weakRerference = new WeakReference(new byte[10*WeakReferenceTest.M]); - WeakReferenceTest.printlnMemory("2.实例化10M的数组,并建立弱引用"); - System.out.println("weakRerference.get() : "+weakRerference.get()); - - System.gc(); - StrongReferenceTest.printlnMemory("3.GC后"); - System.out.println("weakRerference.get() : "+weakRerference.get()); - } -} -```` - -运行结果: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223905.png) -### 3.软引用 SoftReference - -软引用和弱引用的特性基本一致, 主要的区别在于软引用在内存不足时才会被回收。如果一个对象只具有软引用,Java GC在内存充足的时候不会回收它,内存不足时才会被回收。 -```` -public class SoftReferenceTest { - - public static int M = 1024*1024; - - public static void printlnMemory(String tag){ - Runtime runtime = Runtime.getRuntime(); - int M = StrongReferenceTest.M; - System.out.println("\n"+tag+":"); - System.out.println(runtime.freeMemory()/M+"M(free)/" + runtime.totalMemory()/M+"M(total)"); - } - - public static void main(String[] args){ - SoftReferenceTest.printlnMemory("1.原可用内存和总内存"); - - //建立软引用 - SoftReference softRerference = new SoftReference(new byte[10*SoftReferenceTest.M]); - SoftReferenceTest.printlnMemory("2.实例化10M的数组,并建立软引用"); - System.out.println("softRerference.get() : "+softRerference.get()); - - System.gc(); - SoftReferenceTest.printlnMemory("3.内存可用容量充足,GC后"); - System.out.println("softRerference.get() : "+softRerference.get()); - - //实例化一个4M的数组,使内存不够用,并建立软引用 - //free=10M=4M+10M-4M,证明内存可用量不足时,GC后byte[10*m]被回收 - SoftReference softRerference2 = new SoftReference(new byte[4*SoftReferenceTest.M]); - SoftReferenceTest.printlnMemory("4.实例化一个4M的数组后"); - System.out.println("softRerference.get() : "+softRerference.get()); - System.out.println("softRerference2.get() : "+softRerference2.get()); - } -} - -```` -运行结果: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223922.png) -### 4.虚引用 PhantomReference - -从PhantomReference类的源代码可以知道,它的get()方法无论何时返回的都只会是null。所以单独使用虚引用时,没有什么意义,需要和引用队列ReferenceQueue类联合使用。当执行Java GC时如果一个对象只有虚引用,就会把这个对象加入到与之关联的ReferenceQueue中。 -```` -public class PhantomReferenceTest { - - public static int M = 1024*1024; - - public static void printlnMemory(String tag){ - Runtime runtime = Runtime.getRuntime(); - int M = PhantomReferenceTest.M; - System.out.println("\n"+tag+":"); - System.out.println(runtime.freeMemory()/M+"M(free)/" + runtime.totalMemory()/M+"M(total)"); - } - - public static void main(String[] args) throws InterruptedException { - - PhantomReferenceTest.printlnMemory("1.原可用内存和总内存"); - byte[] object = new byte[10*PhantomReferenceTest.M]; - PhantomReferenceTest.printlnMemory("2.实例化10M的数组后"); - - //建立虚引用 - ReferenceQueue referenceQueue = new ReferenceQueue(); - PhantomReference phantomReference = new PhantomReference(object,referenceQueue); - - PhantomReferenceTest.printlnMemory("3.建立虚引用后"); - System.out.println("phantomReference : "+phantomReference); - System.out.println("phantomReference.get() : "+phantomReference.get()); - System.out.println("referenceQueue.poll() : "+referenceQueue.poll()); - - //断开byte[10*PhantomReferenceTest.M]的强引用 - object = null; - PhantomReferenceTest.printlnMemory("4.执行object = null;强引用断开后"); - - System.gc(); - PhantomReferenceTest.printlnMemory("5.GC后"); - System.out.println("phantomReference : "+phantomReference); - System.out.println("phantomReference.get() : "+phantomReference.get()); - System.out.println("referenceQueue.poll() : "+referenceQueue.poll()); - - //断开虚引用 - phantomReference = null; - System.gc(); - PhantomReferenceTest.printlnMemory("6.断开虚引用后GC"); - System.out.println("phantomReference : "+phantomReference); - System.out.println("referenceQueue.poll() : "+referenceQueue.poll()); - } -} - -```` -运行结果: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404223938.png) -## 三、小结 - -强引用是 Java 的默认引用形式,使用时不需要显示定义,是我们平时最常使用到的引用方式。不管系统资源有多紧张,Java GC都不会主动回收具有强引用的对象。 弱引用和软引用一般在引用对象为非必需对象的时候使用。它们的区别是被弱引用关联的对象在垃圾回收时总是会被回收,被软引用关联的对象只有在内存不足时才会被回收。 虚引用的get()方法获取的永远是null,无法获取对象实例。Java GC会把虚引用的对象放到引用队列里面。可用来在对象被回收时做额外的一些资源清理或事物回滚等处理。 由于无法从虚引获取到引用对象的实例。它的使用情况比较特别,所以这里不把虚引用放入表格进行对比。这里对强引用、弱引用、软引用进行对比: - - - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250\350\257\246\350\247\243.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250\350\257\246\350\247\243.md" deleted file mode 100644 index 7e89241..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250\350\257\246\350\247\243.md" +++ /dev/null @@ -1,264 +0,0 @@ -# 目录 - * [1 概述](#1-概述) - * [2 对象已经死亡?](#2-对象已经死亡?) - * [2.1引用计数法](#21引用计数法) - * [2.2可达性分析算法](#22可达性分析算法) - * [2.3 再谈引用](#23-再谈引用) - * [2.4 生存还是死亡](#24-生存还是死亡) - * [2.5 回收方法区](#25-回收方法区) - * [3 垃圾收集算法](#3-垃圾收集算法) - * [3.1 标记-清除算法](#31-标记-清除算法) - * [3.2 复制算法](#32-复制算法) - * [3.3 标记-整理算法](#33-标记-整理算法) - * [3.4分代收集算法](#34分代收集算法) - * [4 垃圾收集器](#4-垃圾收集器) - * [4.1 Serial收集器](#41-serial收集器) - * [4.2 ParNew收集器](#42-parnew收集器) - * [4.3 Parallel Scavenge收集器](#43-parallel-scavenge收集器) - * [4.4.Serial Old收集器](#44serial-old收集器) - * [4.5 Parallel Old收集器](#45-parallel-old收集器) - * [4.6 CMS收集器](#46-cms收集器) - * [4.7 G1收集器](#47-g1收集器) - * [5 内存分配与回收策略](#5-内存分配与回收策略) - * [5.1对象优先在Eden区分配](#51对象优先在eden区分配) - * [5.2 大对象直接进入老年代](#52-大对象直接进入老年代) - * [5.3长期存活的对象将进入老年代](#53长期存活的对象将进入老年代) - * [5.4 动态对象年龄判定](#54-动态对象年龄判定) - * [总结:](#总结:) - - -本文转自:https://www.cnblogs.com/snailclimb/p/9086341.html - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -**本节常见面试题(推荐带着问题阅读,问题答案在文中都有提到):** - -如何判断对象是否死亡(两种方法)。 - -简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。 - -垃圾收集有哪些算法,各自的特点? - -HotSpot为什么要分为新生代和老年代? - -常见的垃圾回收器有那些? - -介绍一下CMS,G1收集器。 - -Minor Gc和Full GC 有什么不同呢? - -## 1 概述 - -首先所需要考虑: -- 那些垃圾需要回收? -- 什么时候回收? -- 如何回收? - -当需要排查各种 内存溢出问题、当垃圾收集称为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。 - -## 2 对象已经死亡? - -堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象) - -### 2.1引用计数法 - -给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。 - -这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 - -### 2.2可达性分析算法 - -这个算法的基本思想就是通过一系列的称为**“GC Roots”**的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。 - -### 2.3 再谈引用 - -JDK1.2以后,Java对引用的感念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱) - -**1.强引用** - -以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于**必不可少的生活用品**,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 - -**2.软引用(SoftReference)** - -如果一个对象只具有软引用,那就类似于**可有可物的生活用品**。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 - -软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。 - -**3.弱引用(WeakReference)** - -如果一个对象只具有弱引用,那就类似于**可有可物的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 - -弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - -**4.虚引用(PhantomReference)** - -“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 - -**虚引用主要用来跟踪对象被垃圾回收的活动**。 - -**虚引用与软引用和弱引用的一个区别在于:**虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 - -特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为**软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生**。 - -### 2.4 生存还是死亡 - -即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。 - -### 2.5 回收方法区 - -方法区(或Hotspot虚拟中的永久代)的垃圾收集主要回收两部分内容:**废弃常量和无用的类。** - -判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是**“无用的类”**: -- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。 -- 加载该类的ClassLoader已经被回收。 -- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 - -## 3 垃圾收集算法 - -### 3.1 标记-清除算法 - -算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,会带来两个明显的问题;1:效率问题和2:空间问题(标记清除后会产生大量不连续的碎片) - -### 3.2 复制算法 - -为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 - - -### 3.3 标记-整理算法 - -根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。 - - -### 3.4分代收集算法 - -当前虚拟机的垃圾手机都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 - -比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的所以我们可以选择“标记-清理”或“标记-整理”算法进行垃圾收集。 - -**延伸面试问题:**HotSpot为什么要分为新生代和老年代? - -根据上面的对分代收集算法的介绍回答。 - -## 4 垃圾收集器 - -**如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。** -虽然我们对各个收集器进行比较,但并非了挑选出一个最好的收集器。因为知道现在位置还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,**我们能做的就是根据具体应用场景选择适合自己的垃圾收集器**。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。 - -### 4.1 Serial收集器 - -Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的**“单线程”**的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(**“Stop The World”**了解一下),直到它收集结束。 - -虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 - -但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它**简单而高效(与其他收集器的单线程相比)**。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。 - -### 4.2 ParNew收集器 - -**ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。** - -它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。 - -**并行和并发概念补充:** - -* **并行(Parallel)**:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 - -* **并发(Concurrent)**:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。 - -### 4.3 Parallel Scavenge收集器 - -Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的的多线程收集器。。。那么它有什么特别之处呢? - -**Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。**Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。 - -### 4.4.Serial Old收集器 - -**Serial收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。 - -### 4.5 Parallel Old收集器 - -**Parallel Scavenge收集器的老年代版本**。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。 - -### 4.6 CMS收集器 - -**CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。** - -从名字中的**Mark Sweep**这两个词可以看出,CMS收集器是一种**“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: - -* **初始标记:**暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ; -* **并发标记:**同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 -* **重新标记:**重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 -* **并发清除:**开启用户线程,同时GC线程开始对为标记的区域做清扫。 - -从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点: --**对CPU资源敏感;** --**无法处理浮动垃圾;** --**它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。** - -### 4.7 G1收集器 - -上一代的垃圾收集器(串行serial, 并行parallel, 以及CMS)都把堆内存划分为固定大小的三个部分: 年轻代(young generation), 年老代(old generation), 以及持久代(permanent generation). - - -**G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.** - -被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点: --**并行与并发**:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。 --**分代收集**:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。 --**空间整合**:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 --**可预测的停顿**:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。 - -**G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)**。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 - -G1收集器的运作大致分为以下几个步骤: --**初始标记** --**并发标记** --**最终标记** --**筛选回收** - -上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。 - - -## 5 内存分配与回收策略 - -### 5.1对象优先在Eden区分配 - -大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC. - -**Minor Gc和Full GC 有什么不同呢?** - -**新生代GC(Minor GC)**:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。 - -**老年代GC(Major GC/Full GC)**:指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。 - -### 5.2 大对象直接进入老年代 - -大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 - -### 5.3长期存活的对象将进入老年代 - -既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 - -### 5.4 动态对象年龄判定 - -为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果Survivor 空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。 - -## 总结: - -本节介绍了垃圾收集算法,几款JDK1.7中提供的垃圾收集器特点以及运作原理。 -内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及大量调节参数,是因为只有根据实际应用的需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合、也没有最优的调优方法,那么必须了解每一个具体收集器的行为、优势和劣势、调节参数。 diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\346\267\261\345\205\245\347\220\206\350\247\243JVM\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\346\267\261\345\205\245\347\220\206\350\247\243JVM\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" deleted file mode 100644 index 198ce72..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\346\267\261\345\205\245\347\220\206\350\247\243JVM\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" +++ /dev/null @@ -1,154 +0,0 @@ -# 目录 - * [一.目标:](#一目标:) - * [二.原理 (类的加载过程及其最终产品):](#二原理-(类的加载过程及其最终产品)) - * [三.过程(类的生命周期):](#三过程(类的生命周期):) - * [加载:](#加载:) - * [校验:](#校验:) - * [准备:](#准备:) - * [解析:](#解析:) - * [初始化:](#初始化:) - * [四.类加载器:](#四类加载器:) - * [五.双亲委派机制:](#五双亲委派机制:) - * [参考文章](#参考文章) - - -本文转自互联网,侵删 - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 一.目标: - -1.什么是类的加载? - -2.类的生命周期? - -3.类加载器是什么? - -4.双亲委派机制是什么? - -## 二.原理 (类的加载过程及其最终产品): - -JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。 - -## 三.过程(类的生命周期): - -JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。 - - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404221551.png) - -### 加载: - -加载过程主要完成三件事情: - -1. 通过类的全限定名来获取定义此类的二进制字节流 -2. 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构 -3. 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。 - -这个过程主要就是类加载器完成。 - -### 校验: - -此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。 - -1. 文件格式验证:基于字节流验证。 -2. 元数据验证:基于**_方法区_**的存储结构验证。 -3. 字节码验证:基于方法区的存储结构验证。 -4. 符号引用验证:基于方法区的存储结构验证。 - -### 准备: - -为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如: - -``` -public static int value = 123; - -``` - -此时在准备阶段过后的初始值为0而不是123;将value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法之中.特例: - -``` -public static final int value = 123; - -``` - -此时value的值在准备阶段过后就是123。 - -### 解析: - -把类型中的符号引用转换为直接引用。 - -* 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。 -* 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在 - -主要有以下四种: - -1. 类或接口的解析 -2. 字段解析 -3. 类方法解析 -4. 接口方法解析 - -### 初始化: - -初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。 - -java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始): - -1. 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。 -2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。 -3. 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。 -4. 虚拟机启动时,用户会先初始化要执行的主类(含有main) -5. jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。 - -## 四.类加载器: - -把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。系统自带的类加载器分为三种: - -1. 启动类加载器。 -2. 扩展类加载器。 -3. 应用程序类加载器。 - -## 五.双亲委派机制: - - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404221618.png) - - -双亲委派机制工作过程: - -如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类.而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。 - -双亲委派模型的优点:java类随着它的加载器一起具备了一种带有优先级的层次关系. - -例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱. - - -## 参考文章 - - - - - - - - - -https://blog.csdn.net/android_hl/article/details/53228348 - - diff --git "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\350\231\232\346\213\237\346\234\272\345\255\227\350\212\202\347\240\201\346\211\247\350\241\214\345\274\225\346\223\216.md" "b/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\350\231\232\346\213\237\346\234\272\345\255\227\350\212\202\347\240\201\346\211\247\350\241\214\345\274\225\346\223\216.md" deleted file mode 100644 index aabb34c..0000000 --- "a/docs/Java/JVM/\346\267\261\345\205\245\347\220\206\350\247\243JVM\350\231\232\346\213\237\346\234\272\357\274\232\350\231\232\346\213\237\346\234\272\345\255\227\350\212\202\347\240\201\346\211\247\350\241\214\345\274\225\346\223\216.md" +++ /dev/null @@ -1,304 +0,0 @@ -# 目录 - * [1 概述](#1-概述) - * [2 运行时栈帧结构](#2-运行时栈帧结构) - * [2.1 局部变量表](#21-局部变量表) - * [2.2 操作数栈](#22-操作数栈) - * [2.3 动态连接](#23-动态连接) - * [2.4 方法返回地址](#24-方法返回地址) - * [2.5 附加信息](#25-附加信息) - * [3 方法调用](#3-方法调用) - * [3.1 解析](#31-解析) - * [3.2 分派](#32-分派) - * [3.3 动态类型语言的支持](#33-动态类型语言的支持) - * [4 基于栈的字节码解释执行引擎](#4-基于栈的字节码解释执行引擎) - * [4.1 解释执行](#41-解释执行) - * [4.2 基于栈的指令集和基于寄存器的指令集](#42-基于栈的指令集和基于寄存器的指令集) - * [总结](#总结) - - -本文转自:https://www.cnblogs.com/snailclimb/p/9086337.html - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章将同步到我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《深入理解JVM虚拟机》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 - -该系列博文会告诉你如何从入门到进阶,一步步地学习JVM基础知识,并上手进行JVM调优实战,JVM是每一个Java工程师必须要学习和理解的知识点,你必须要掌握其实现原理,才能更完整地了解整个Java技术体系,形成自己的知识框架。 - -为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 1 概述 - -执行引擎是java虚拟机最核心的组成部件之一。虚拟机的执行引擎由自己实现,所以可以自行定制指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。 - -所有的Java虚拟机的执行引擎都是一致的:**输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果**。本节将主要从概念模型的角度来讲解**虚拟机的方法调用和字节码执行**。 - -## 2 运行时栈帧结构 - -**栈帧(Stack Frame)**是用于支持虚拟机方法调用和方法执行的数据结构,它是虚拟机运行时数据区中**虚拟机栈(Virtual Machine Stack)的栈元素**。 - -栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 - -**栈帧概念结构如下图所示:** - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404221305.png) -### 2.1 局部变量表 - -**局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。 -局部变量表的容量以变量槽(Variable Slot)为最小单位。** 一个Slot可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用,returnAddress已经很少见了,可以忽略。 - -**对于64位的数据类型(Java语言中明确的64位数据类型只有long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。** - -**虚拟机通过索引定位的方式使用局部变量表**,索引值的范围从0开始至局部变量表最大的Slot数量。访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型,就代表会同时使用n和n+1这两个Slot。 - -**为了节省栈帧空间,局部变量Slot可以重用**,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值超出了某个变量的作用域,那么这个变量的Slot就可以交给其他变量使用。这样的设计会带来一些额外的副作用,比如:在某些情况下,Slot的复用会直接影响到系统的收集行为。 - -### 2.2 操作数栈 - -**操作数栈(Operand Stack)**也常称为操作栈,它是一个**后入先出栈**。当一个方法执行开始时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是**出栈/入栈**操作。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230404221316.png) -在概念模型中,一个活动线程中两个栈帧是相互独立的。但大多数虚拟机实现都会做一些优化处理:让下一个栈帧的部分操作数栈与上一个栈帧的部分局部变量表重叠在一起,这样的好处是方法调用时可以共享一部分数据,而无须进行额外的参数复制传递。 - -### 2.3 动态连接 - -每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的**动态连接**; - -字节码中方法调用指令是以常量池中的指向方法的符号引用为参数的,有一部分符号引用会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为**静态解析**,另外一部分在每次的运行期间转化为直接引用,这部分称为**动态连接**。 - -### 2.4 方法返回地址 - -当一个方法被执行后,有两种方式退出这个方法: - -* 第一种是执行引擎遇到任意一个方法返回的字节码指令,这种退出方法的方式称为**正常完成出口(Normal Method Invocation Completion)**。 - -* 另外一种是在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理(即本方法异常处理表中没有匹配的异常处理器),就会导致方法退出,这种退出方式称为**异常完成出口(Abrupt Method Invocation Completion)**。 - 注意:这种退出方式不会给上层调用者产生任何返回值。 - -**无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行**,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。 - -方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。 - -### 2.5 附加信息 - -虚拟机规范允许虚拟机实现向栈帧中添加一些自定义的附加信息,例如与调试相关的信息等。 - -## 3 方法调用 - -方法调用阶段的目的:**确定被调用方法的版本(哪一个方法),不涉及方法内部的具体运行过程**,在程序运行时,进行方法调用是最普遍、最频繁的操作。 - -**一切方法调用在Class文件里存储的都只是符号引用,这是需要在类加载期间或者是运行期间,才能确定为方法在实际 运行时内存布局中的入口地址(相当于之前说的直接引用)**。 - -### 3.1 解析 - -“编译期可知,运行期不可变”的方法(静态方法和私有方法),在类加载的解析阶段,会将其符号引用转化为直接引用(入口地址)。这类方法的调用称为“**解析(Resolution)**”。 - -在Java虚拟机中提供了5条方法调用字节码指令: --**invokestatic**: 调用静态方法 --**invokespecial**:调用实例构造器方法、私有方法、父类方法 --**invokevirtual**:调用所有的虚方法 --**invokeinterface**:调用接口方法,会在运行时在确定一个实现此接口的对象 --**invokedynamic**:先在运行时动态解析出点限定符所引用的方法,然后再执行该方法,在此之前的4条调用命令的分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 - -### 3.2 分派 - -**分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在Java虚拟中是如何实现的。** - -**1 静态分派** - -所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派发生在编译阶段。 - -静态分派最典型的应用就是方法重载。 - -``` -package jvm8_3_2; - -public class StaticDispatch { - static abstract class Human { - - } - - static class Man extends Human { - - } - - static class Woman extends Human { - - } - - public void sayhello(Human guy) { - System.out.println("Human guy"); - - } - - public void sayhello(Man guy) { - System.out.println("Man guy"); - - } - - public void sayhello(Woman guy) { - System.out.println("Woman guy"); - } - - public static void main(String[] args) { - Human man = new Man(); - Human woman = new Woman(); - StaticDispatch staticDispatch = new StaticDispatch(); - staticDispatch.sayhello(man);// Human guy - staticDispatch.sayhello(woman);// Human guy - } - -} -``` - -运行结果: - -Human guy - -Human guy - -**为什么会出现这样的结果呢?** - -Human man = new Man();其中的Human称为变量的**静态类型(Static Type)**,Man称为变量的**实际类型(Actual Type)**。 -**两者的区别是**:静态类型在编译器可知,而实际类型到运行期才确定下来。 -在重载时通过参数的静态类型而不是实际类型作为判定依据,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。所以选择了sayhello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。 - -**2 动态分派** - -在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的应用就是方法重写。 - -``` -package jvm8_3_2; - -public class DynamicDisptch { - - static abstract class Human { - abstract void sayhello(); - } - - static class Man extends Human { - - @Override - void sayhello() { - System.out.println("man"); - } - - } - - static class Woman extends Human { - - @Override - void sayhello() { - System.out.println("woman"); - } - - } - - public static void main(String[] args) { - Human man = new Man(); - Human woman = new Woman(); - man.sayhello(); - woman.sayhello(); - man = new Woman(); - man.sayhello(); - } - -} - -``` - -运行结果: - -man - -woman - -woman - -**3 单分派和多分派** - -方法的接收者、方法的参数都可以称为方法的宗量。根据分批基于多少种宗量,可以将分派划分为单分派和多分派。**单分派是根据一个宗量对目标方法进行选择的,多分派是根据多于一个的宗量对目标方法进行选择的。** - -Java在进行静态分派时,选择目标方法要依据两点:一是变量的静态类型是哪个类型,二是方法参数是什么类型。因为要根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。 - -运行时阶段的动态分派过程,由于编译器已经确定了目标方法的签名(包括方法参数),运行时虚拟机只需要确定方法的接收者的实际类型,就可以分派。因为是根据一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。 - -注:到JDK1.7时,Java语言还是静态多分派、动态单分派的语言,未来有可能支持动态多分派。 - -**4 虚拟机动态分派的实现** - -由于动态分派是非常频繁的动作,而动态分派在方法版本选择过程中又需要在方法元数据中搜索合适的目标方法,虚拟机实现出于性能的考虑,通常不直接进行如此频繁的搜索,而是采用优化方法。 - -其中一种“稳定优化”手段是:在类的方法区中建立一个**虚方法表**(Virtual Method Table, 也称vtable, 与此对应,也存在接口方法表——Interface Method Table,也称itable)。**使用虚方法表索引来代替元数据查找以提高性能。其原理与C++的虚函数表类似。** - -虚方法表中存放的是各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类中该方法相同,都指向父类的实现入口。虚方法表一般在类加载的连接阶段进行初始化。 - -### 3.3 动态类型语言的支持 - -JDK新增加了invokedynamic指令来是实现“动态类型语言”。 - -**静态语言和动态语言的区别:** - -* **静态语言(强类型语言)**: - 静态语言是在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型。 - 例如:C++、Java、Delphi、C#等。 -* **动态语言(弱类型语言)**: - 动态语言是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。 - 例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等。 -* **强类型定义语言**: - 强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量a,那么程序根本不可能将a当作字符串类型处理。强类型定义语言是类型安全的语言。 -* **弱类型定义语言**: - 数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。 - -## 4 基于栈的字节码解释执行引擎 - -虚拟机如何调用方法的内容已经讲解完毕,现在我们来探讨虚拟机是如何执行方法中的字节码指令。 - -### 4.1 解释执行 - -Java语言经常被人们定位为**“解释执行”语言**,在Java初生的JDK1.0时代,这种定义还比较准确的,但当主流的虚拟机中都包含了即时编译后,Class文件中的代码到底会被解释执行还是编译执行,就成了只有虚拟机自己才能准确判断的事情。再后来,Java也发展出来了直接生成本地代码的编译器[如何GCJ(GNU Compiler for the Java)],而C/C++也出现了通过解释器执行的版本(如CINT),这时候再笼统的说“解释执行”,对于整个Java语言来说就成了几乎没有任何意义的概念,**只有确定了谈论对象是某种具体的Java实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切**。 - -Java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程,因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机内部,所以Java程序的编译就是半独立实现的, - -### 4.2 基于栈的指令集和基于寄存器的指令集 - -Java编译器输出的指令流,基本上是一种**基于栈的指令集架构(Instruction Set Architecture,ISA)**,**依赖操作数栈进行工作**。与之相对应的另一套常用的指令集架构是**基于寄存器的指令集**,**依赖寄存器进行工作**。 - -那么,**基于栈的指令集和基于寄存器的指令集这两者有什么不同呢?** - -举个简单例子,分别使用这两种指令计算1+1的结果,**基于栈的指令集会是这个样子:** -iconst_1 - -iconst_1 - -iadd - -istore_0 - -两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈、相加,然后将结果放回栈顶,最后istore_0把栈顶的值放到局部变量表中的第0个Slot中。 - -**如果基于寄存器的指令集,那程序可能会是这个样子:** - -mov eax, 1 - -add eax, 1 - -mov指令把EAX寄存器的值设置为1,然后add指令再把这个值加1,将结果就保存在EAX寄存器里面。 - -**基于栈的指令集主要的优点就是可移植,寄存器是由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。** - -**栈架构的指令集还有一些其他的优点,如代码相对更加紧凑,编译器实现更加简单等。 -栈架构指令集的主要缺点是执行速度相对来说会稍微慢一些。** - -## 总结 - -本节中,我们分析了虚拟机在执行代码时,如何找到正确的方法、如何执行方法内的字节码,以及执行代码时涉及的内存结构。 diff --git "a/docs/Java/basic/Java8\346\226\260\347\211\271\346\200\247\347\273\210\346\236\201\346\214\207\345\215\227.md" "b/docs/Java/basic/Java8\346\226\260\347\211\271\346\200\247\347\273\210\346\236\201\346\214\207\345\215\227.md" deleted file mode 100644 index fa70648..0000000 --- "a/docs/Java/basic/Java8\346\226\260\347\211\271\346\200\247\347\273\210\346\236\201\346\214\207\345\215\227.md" +++ /dev/null @@ -1,834 +0,0 @@ -# 目录 - * [Java语言新特性](#java语言新特性) - * [Lambda表达式](#lambda表达式) - * [函数式接口](#函数式接口) - * [方法引用](#方法引用) - * [接口的默认方法](#接口的默认方法) - * [重复注解](#重复注解) - * [Java编译器的新特性](#java编译器的新特性) - * [方法参数名字可以反射获取](#方法参数名字可以反射获取) - * [Java 类库的新特性](#java-类库的新特性) - * [Optional](#optional) - * [Stream](#stream) - * [Date/Time API (JSR 310)](#datetime-api-jsr-310) - * [并行(parallel)数组](#并行(parallel)数组) - * [CompletableFuture](#completablefuture) - * [Java虚拟机(JVM)的新特性](#java虚拟机(jvm)的新特性) - * [总结](#总结) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - - -这是一个Java8新增特性的总结图。接下来让我们一次实践一下这些新特性吧 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230403215737.png) -## Java语言新特性 - -### Lambda表达式 - - -Lambda表达式(也称为闭包)是整个Java 8发行版中最受期待的在Java语言层面上的改变,Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据:函数式程序员对这一概念非常熟悉。在JVM平台上的很多语言(Groovy,Scala,……)从一开始就有Lambda,但是Java程序员不得不使用毫无新意的匿名类来代替lambda。 - -关于Lambda设计的讨论占用了大量的时间与社区的努力。可喜的是,最终找到了一个平衡点,使得可以使用一种即简洁又紧凑的新方式来构造Lambdas。在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。例如: - - Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) ); - -请注意参数e的类型是由编译器推测出来的。同时,你也可以通过把参数类型与参数包括在括号中的形式直接给出参数的类型: - - Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) ); - -在某些情况下lambda的函数体会更加复杂,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。例如: - - Arrays.asList( "a", "b", "d" ).forEach( e -> { - System.out.print( e ); - System.out.print( e ); - } ); -Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。例如,下面两个代码片段是等价的: - - String separator = ","; - Arrays.asList( "a", "b", "d" ).forEach( - ( String e ) -> System.out.print( e + separator ) ); -和: - - final String separator = ","; - Arrays.asList( "a", "b", "d" ).forEach( - ( String e ) -> System.out.print( e + separator ) ); -Lambda可能会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。下面两个代码片段是等价的: - - Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) ); -和: - - Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> { - int result = e1.compareTo( e2 ); - return result; - } ); - -语言设计者投入了大量精力来思考如何使现有的函数友好地支持lambda。 - -最终采取的方法是:增加函数式接口的概念。函数式接口就是一个具有一个方法的普通接口。像这样的接口,可以被隐式转换为lambda表达式。 - -java.lang.Runnable与java.util.concurrent.Callable是函数式接口最典型的两个例子。 - -在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。 - -为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java8增加了一种特殊的注解@FunctionalInterface(Java8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义: -```` -@FunctionalInterface -public interface Functional { - void method(); -} -```` -需要记住的一件事是:默认方法与静态方法并不影响函数式接口的契约,可以任意使用: -```` -@FunctionalInterface -public interface FunctionalDefaultMethods { - void method(); - - default void defaultMethod() { - } -} -```` -Lambda是Java 8最大的卖点。它具有吸引越来越多程序员到Java平台上的潜力,并且能够在纯Java语言环境中提供一种优雅的方式来支持函数式编程。更多详情可以参考官方文档。 - -下面看一个例子: -```` -public class lambda和函数式编程 { - @Test - public void test1() { - List names = Arrays.asList("peter", "anna", "mike", "xenia"); - - Collections.sort(names, new Comparator() { - @Override - public int compare(String a, String b) { - return b.compareTo(a); - } - }); - System.out.println(Arrays.toString(names.toArray())); - } - - @Test - public void test2() { - List names = Arrays.asList("peter", "anna", "mike", "xenia"); - - Collections.sort(names, (String a, String b) -> { - return b.compareTo(a); - }); - - Collections.sort(names, (String a, String b) -> b.compareTo(a)); - - Collections.sort(names, (a, b) -> b.compareTo(a)); - System.out.println(Arrays.toString(names.toArray())); - } - -} - - static void add(double a,String b) { - System.out.println(a + b); - } - @Test - public void test5() { - D d = (a,b) -> add(a,b); -// interface D { -// void get(int i,String j); -// } - //这里要求,add的两个参数和get的两个参数吻合并且返回类型也要相等,否则报错 -// static void add(double a,String b) { -// System.out.println(a + b); -// } - } - - @FunctionalInterface - interface D { - void get(int i,String j); - } -```` -接下来看看Lambda和匿名内部类的区别 - -匿名内部类仍然是一个类,只是不需要我们显式指定类名,编译器会自动为该类取名。比如有如下形式的代码: -```` -public class LambdaTest { - public static void main(String[] args) { - new Thread(new Runnable() { - @Override - public void run() { - System.out.println("Hello World"); - } - }).start(); - } -} -```` -编译之后将会产生两个 class 文件: - - LambdaTest.class - LambdaTest$1.class - -使用 javap -c LambdaTest.class 进一步分析 LambdaTest.class 的字节码,部分结果如下: -```` - public static void main(java.lang.String[]); - Code: - 0: new #2 // class java/lang/Thread - 3: dup - 4: new #3 // class com/example/myapplication/lambda/LambdaTest$1 - 7: dup - 8: invokespecial #4 // Method com/example/myapplication/lambda/LambdaTest$1."":()V - 11: invokespecial #5 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V - 14: invokevirtual #6 // Method java/lang/Thread.start:()V - 17: return -```` -可以发现在 4: new #3 这一行创建了匿名内部类的对象。 - -而对于 Lambda表达式的实现, 接下来我们将上面的示例代码使用 Lambda 表达式实现,代码如下: -```` -public class LambdaTest { - public static void main(String[] args) { - new Thread(() -> System.out.println("Hello World")).start(); - } -} -```` -此时编译后只会产生一个文件 LambdaTest.class,再来看看通过 javap 对该文件反编译后的结果: -```` -public static void main(java.lang.String[]); -Code: - 0: new #2 // class java/lang/Thread - 3: dup - 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; - 9: invokespecial #4 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V - 12: invokevirtual #5 // Method java/lang/Thread.start:()V - 15: return -```` -从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic 指令进行调用。 - -因此,我们可以得出结论:Lambda 表达式是通过 invokedynamic 指令实现的,并且书写 Lambda 表达式不会产生新的类。 - -既然 Lambda 表达式不会创建匿名内部类,那么在 Lambda 表达式中使用 this 关键字时,其指向的是外部类的引用。 - -### 函数式接口 - -所谓的函数式接口就是只有一个抽象方法的接口,注意这里说的是抽象方法,因为Java8中加入了默认方法的特性,但是函数式接口是不关心接口中有没有默认方法的。 一般函数式接口可以使用@FunctionalInterface注解的形式来标注表示这是一个函数式接口,该注解标注与否对函数式接口没有实际的影响, 不过一般还是推荐使用该注解,就像使用@Override注解一样。 - -lambda表达式是如何符合 Java 类型系统的?每个lambda对应于一个给定的类型,用一个接口来说明。而这个被称为函数式接口(functional interface)的接口必须仅仅包含一个抽象方法声明。每个那个类型的lambda表达式都将会被匹配到这个抽象方法上。因此默认的方法并不是抽象的,你可以给你的函数式接口自由地增加默认的方法。 - - -我们可以使用任意的接口作为lambda表达式,只要这个接口只包含一个抽象方法。为了保证你的接口满足需求,你需要增加@FunctionalInterface注解。编译器知道这个注解,一旦你试图给这个接口增加第二个抽象方法声明时,它将抛出一个编译器错误。 - -下面举几个例子 -```` -public class 函数式接口使用 { - @FunctionalInterface - interface A { - void say(); - default void talk() { - - } - } - @Test - public void test1() { - A a = () -> System.out.println("hello"); - a.say(); - } - - @FunctionalInterface - interface B { - void say(String i); - } - public void test2() { - //下面两个是等价的,都是通过B接口来引用一个方法,而方法可以直接使用::来作为方法引用 - B b = System.out::println; - B b1 = a -> Integer.parseInt("s");//这里的a其实换成别的也行,只是将方法传给接口作为其方法实现 - B b2 = Integer::valueOf;//i与方法传入参数的变量类型一直时,可以直接替换 - B b3 = String::valueOf; - //B b4 = Integer::parseInt;类型不符,无法使用 - - } - @FunctionalInterface - interface C { - int say(String i); - } - public void test3() { - C c = Integer::parseInt;//方法参数和接口方法的参数一样,可以替换。 - int i = c.say("1"); - //当我把C接口的int替换为void时就会报错,因为返回类型不一致。 - System.out.println(i); - //综上所述,lambda表达式提供了一种简便的表达方式,可以将一个方法传到接口中。 - //函数式接口是只提供一个抽象方法的接口,其方法由lambda表达式注入,不需要写实现类, - //也不需要写匿名内部类,可以省去很多代码,比如实现runnable接口。 - //函数式编程就是指把方法当做一个参数或引用来进行操作。除了普通方法以外,静态方法,构造方法也是可以这样操作的。 - } -} -```` -请记住如果@FunctionalInterface 这个注解被遗漏,此代码依然有效。 - -### 方法引用 - -Lambda表达式和方法引用 - -有了函数式接口之后,就可以使用Lambda表达式和方法引用了。其实函数式接口的表中的函数描述符就是Lambda表达式,在函数式接口中Lambda表达式相当于匿名内部类的效果。 举个简单的例子: - -```` -public class TestLambda { - - public static void execute(Runnable runnable) { - runnable.run(); - } - - public static void main(String[] args) { - //Java8之前 - execute(new Runnable() { - @Override - public void run() { - System.out.println("run"); - } - }); - - //使用Lambda表达式 - execute(() -> System.out.println("run")); - } -} -```` -可以看到,相比于使用匿名内部类的方式,Lambda表达式可以使用更少的代码但是有更清晰的表述。注意,Lambda表达式也不是完全等价于匿名内部类的, 两者的不同点在于this的指向和本地变量的屏蔽上。 - -方法引用可以看作Lambda表达式的更简洁的一种表达形式,使用::操作符,方法引用主要有三类: - - 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt); - - 指向任意类型实例方法的方法引用(例如String的length方法,写作String::length); - - 指向现有对象的实例方法的方法引用(例如假设你有一个本地变量localVariable用于存放Variable类型的对象,它支持实例方法getValue,那么可以写成localVariable::getValue)。 - -举个方法引用的简单的例子: - - Function stringToInteger = (String s) -> Integer.parseInt(s); - -//使用方法引用 - - Function stringToInteger = Integer::parseInt; - -方法引用中还有一种特殊的形式,构造函数引用,假设一个类有一个默认的构造函数,那么使用方法引用的形式为: - - Supplier c1 = SomeClass::new; - SomeClass s1 = c1.get(); - -//等价于 - - Supplier c1 = () -> new SomeClass(); - SomeClass s1 = c1.get(); -如果是构造函数有一个参数的情况: - - Function c1 = SomeClass::new; - SomeClass s1 = c1.apply(100); - -//等价于 - - Function c1 = i -> new SomeClass(i); - SomeClass s1 = c1.apply(100); - -### 接口的默认方法 - -Java 8 使我们能够使用default 关键字给接口增加非抽象的方法实现。这个特性也被叫做 扩展方法(Extension Methods)。如下例所示: -```` -public class 接口的默认方法 { - class B implements A { -// void a(){}实现类方法不能重名 - } - interface A { - //可以有多个默认方法 - public default void a(){ - System.out.println("a"); - } - public default void b(){ - System.out.println("b"); - } - //报错static和default不能同时使用 -// public static default void c(){ -// System.out.println("c"); -// } - } - public void test() { - B b = new B(); - b.a(); - - } -} -```` -默认方法出现的原因是为了对原有接口的扩展,有了默认方法之后就不怕因改动原有的接口而对已经使用这些接口的程序造成的代码不兼容的影响。 在Java8中也对一些接口增加了一些默认方法,比如Map接口等等。一般来说,使用默认方法的场景有两个:可选方法和行为的多继承。 - -默认方法的使用相对来说比较简单,唯一要注意的点是如何处理默认方法的冲突。关于如何处理默认方法的冲突可以参考以下三条规则: - -类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。 - -如果无法依据第一条规则进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口。即如果B继承了A,那么B就比A更具体。 - -最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。那么如何显式地指定呢: -```` - public class C implements B, A { - - public void hello() { - B.super().hello(); - } - - } -```` -使用X.super.m(..)显式地调用希望调用的方法。 - -Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。默认方法使接口有点像Traits(Scala中特征(trait)类似于Java中的Interface,但它可以包含实现代码,也就是目前Java8新增的功能),但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。 - -默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。让我们看看下面的例子: -```` -private interface Defaulable { - // Interfaces now allow default methods, the implementer may or - // may not implement (override) them. - default String notRequired() { - return "Default implementation"; - } -} - -private static class DefaultableImpl implements Defaulable { -} - -private static class OverridableImpl implements Defaulable { - @Override - public String notRequired() { - return "Overridden implementation"; - } -} -```` -Defaulable接口用关键字default声明了一个默认方法notRequired(),Defaulable接口的实现者之一DefaultableImpl实现了这个接口,并且让默认方法保持原样。Defaulable接口的另一个实现者OverridableImpl用自己的方法覆盖了默认方法。 - -Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法。例如: - -```` - private interface DefaulableFactory { - // Interfaces now allow static methods - static Defaulable create( Supplier< Defaulable > supplier ) { - return supplier.get(); - } - } -```` -下面的一小段代码片段把上面的默认方法与静态方法黏合到一起。 - -```` - public static void main( String[] args ) { - Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new ); - System.out.println( defaulable.notRequired() ); - - defaulable = DefaulableFactory.create( OverridableImpl::new ); - System.out.println( defaulable.notRequired() ); - } -```` -这个程序的控制台输出如下: - -Default implementation -Overridden implementation -在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。默认方法允许继续使用现有的Java接口,而同时能够保障正常的编译过程。这方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),…… - -尽管默认方法非常强大,但是在使用默认方法时我们需要小心注意一个地方:在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法,因为默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。更多详情请参考官方文档 - - -### 重复注解 -自从Java 5引入了注解机制,这一特性就变得非常流行并且广为使用。然而,使用注解的一个限制是相同的注解在同一位置只能声明一次,不能声明多次。Java 8打破了这条规则,引入了重复注解机制,这样相同的注解可以在同一地方声明多次。 - -重复注解机制本身必须用@Repeatable注解。事实上,这并不是语言层面上的改变,更多的是编译器的技巧,底层的原理保持不变。让我们看一个快速入门的例子: -```` -package com.javacodegeeks.java8.repeatable.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -public class RepeatingAnnotations { - @Target( ElementType.TYPE ) - @Retention( RetentionPolicy.RUNTIME ) - public @interface Filters { - Filter[] value(); - } - - @Target( ElementType.TYPE ) - @Retention( RetentionPolicy.RUNTIME ) - @Repeatable( Filters.class ) - public @interface Filter { - String value(); - }; - - @Filter( "filter1" ) - @Filter( "filter2" ) - public interface Filterable { - } - - public static void main(String[] args) { - for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) { - System.out.println( filter.value() ); - } - } -} -```` -正如我们看到的,这里有个使用@Repeatable( Filters.class )注解的注解类Filter,Filters仅仅是Filter注解的数组,但Java编译器并不想让程序员意识到Filters的存在。这样,接口Filterable就拥有了两次Filter(并没有提到Filter)注解。 - -同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型(请注意Filterable.class.getAnnotation( Filters.class )经编译器处理后将会返回Filters的实例)。 - -程序输出结果如下: - - filter1 - filter2 -更多详情请参考官方文档 - -## Java编译器的新特性 - -### 方法参数名字可以反射获取 - -很长一段时间里,Java程序员一直在发明不同的方式使得方法参数的名字能保留在Java字节码中,并且能够在运行时获取它们(比如,Paranamer类库)。最终,在Java 8中把这个强烈要求的功能添加到语言层面(通过反射API与Parameter.getName()方法)与字节码文件(通过新版的javac的–parameters选项)中。 -```` -package com.javacodegeeks.java8.parameter.names; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; - -public class ParameterNames { - public static void main(String[] args) throws Exception { - Method method = ParameterNames.class.getMethod( "main", String[].class ); - for( final Parameter parameter: method.getParameters() ) { - System.out.println( "Parameter: " + parameter.getName() ); - } - } -} -```` -如果不使用–parameters参数来编译这个类,然后运行这个类,会得到下面的输出: - -Parameter: arg0 -如果使用–parameters参数来编译这个类,程序的结构会有所不同(参数的真实名字将会显示出来): - -Parameter: args - -## Java 类库的新特性 -Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。 - -### Optional -到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。 - -Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。更多详情请参考官方文档。 - -我们下面用两个小例子来演示如何使用Optional类:一个允许为空值,一个不允许为空值。 -```` - public class 空指针Optional { - public static void main(String[] args) { - - //使用of方法,仍然会报空指针异常 - // Optional optional = Optional.of(null); - // System.out.println(optional.get()); - - //抛出没有该元素的异常 - //Exception in thread "main" java.util.NoSuchElementException: No value present - // at java.util.Optional.get(Optional.java:135) - // at com.javase.Java8.空指针Optional.main(空指针Optional.java:14) - // Optional optional1 = Optional.ofNullable(null); - // System.out.println(optional1.get()); - Optional optional = Optional.ofNullable(null); - System.out.println(optional.isPresent()); - System.out.println(optional.orElse(0));//当值为空时给与初始值 - System.out.println(optional.orElseGet(() -> new String[]{"a"}));//使用回调函数设置默认值 - //即使传入Optional容器的元素为空,使用optional.isPresent()方法也不会报空指针异常 - //所以通过optional.orElse这种方式就可以写出避免空指针异常的代码了 - //输出Optional.empty。 - } - } -```` -如果Optional类的实例为非空值的话,isPresent()返回true,否从返回false。为了防止Optional为空值,orElseGet()方法通过回调函数来产生一个默认值。map()函数对当前Optional的值进行转化,然后返回一个新的Optional实例。orElse()方法和orElseGet()方法类似,但是orElse接受一个默认值而不是一个回调函数。下面是这个程序的输出: - -Full Name is set? false -Full Name: [none] -Hey Stranger! -让我们来看看另一个例子: - - - Optional< String > firstName = Optional.of( "Tom" ); - System.out.println( "First Name is set? " + firstName.isPresent() ); - System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); - System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) ); - System.out.println(); - -下面是程序的输出: - -First Name is set? true -First Name: Tom -Hey Tom! - -### Stream -最新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。 - -Stream API极大简化了集合框架的处理(但它的处理的范围不仅仅限于集合框架的处理,这点后面我们会看到)。让我们以一个简单的Task类为例进行介绍: - -Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.让我们引入一个Task的小集合作为演示例子: -```` - final Collection< Task > tasks = Arrays.asList( - new Task( Status.OPEN, 5 ), - new Task( Status.OPEN, 13 ), - new Task( Status.CLOSED, 8 ) - ); -```` -我们下面要讨论的第一个问题是所有状态为OPEN的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用stream:一串支持连续、并行聚集操作的元素。 -```` - // Calculate total points of all active tasks using sum() - final long totalPointsOfOpenTasks = tasks - .stream() - .filter( task -> task.getStatus() == Status.OPEN ) - .mapToInt( Task::getPoints ) - .sum(); - - System.out.println( "Total points: " + totalPointsOfOpenTasks ); -```` -程序在控制台上的输出如下: - - Total points: 18 - -这里有几个注意事项。 - -第一,task集合被转换化为其相应的stream表示。然后,filter操作过滤掉状态为CLOSED的task。 - -下一步,mapToInt操作通过Task::getPoints这种方式调用每个task实例的getPoints方法把Task的stream转化为Integer的stream。最后,用sum函数把所有的分数加起来,得到最终的结果。 - -在继续讲解下面的例子之前,关于stream有一些需要注意的地方(详情在这里).stream操作被分成了中间操作与最终操作这两种。 - -中间操作返回一个新的stream对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始stream -中符合给定谓词的所有元素。 - -像forEach、sum这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。 - -stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。 - -stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。 -```` - // Calculate total points of all tasks - final double totalPoints = tasks - .stream() - .parallel() - .map( task -> task.getPoints() ) // or map( Task::getPoints ) - .reduce( 0, Integer::sum ); - - System.out.println( "Total points (all tasks): " + totalPoints ); -```` -这个例子和第一个例子很相似,但这个例子的不同之处在于这个程序是并行运行的,其次使用reduce方法来算最终的结果。 -下面是这个例子在控制台的输出: - -Total points (all tasks): 26.0 -经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。Stream也可以处理这样的需求,下面是一个例子: - -```` - // Group tasks by their status - final Map< Status, List< Task > > map = tasks - .stream() - .collect( Collectors.groupingBy( Task::getStatus ) ); - System.out.println( map ); -```` - -这个例子的控制台输出如下: - - {CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]} - -让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。 - -```` - // Calculate the weight of each tasks (as percent of total points) - final Collection< String > result = tasks - .stream() // Stream< String > - .mapToInt( Task::getPoints ) // IntStream - .asLongStream() // LongStream - .mapToDouble( points -> points / totalPoints ) // DoubleStream - .boxed() // Stream< Double > - .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream - .mapToObj( percentage -> percentage + "%" ) // Stream< String> - .collect( Collectors.toList() ); // List< String > - - System.out.println( result ); -```` - -下面是这个例子的控制台输出: - -[19%, 50%, 30%] -最后,就像前面提到的,Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。 -```` - final Path path = new File( filename ).toPath(); - try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) { - lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println ); - } -```` - -对一个stream对象调用onClose方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用close()方法时,与关闭相关的处理器就会执行。 - -Stream API、Lambda表达式与方法引用在接口默认方法与静态方法的配合下是Java 8对现代软件开发范式的回应。更多详情请参考官方文档。 - -### Date/Time API (JSR 310) -Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。对日期与时间的操作一直是Java程序员最痛苦的地方之一。标准的 java.util.Date以及后来的java.util.Calendar一点没有改善这种情况(可以这么说,它们一定程度上更加复杂)。 - -这种情况直接导致了Joda-Time——一个可替换标准日期/时间处理且功能非常强大的Java API的诞生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。在设计新版API时,十分注重与旧版API的兼容性:不允许有任何的改变(从java.util.Calendar中得到的深刻教训)。如果需要修改,会返回这个类的一个新实例。 - -让我们用例子来看一下新版API主要类的使用方法。第一个是Clock类,它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()。 -```` - // Get the system clock as UTC offset - final Clock clock = Clock.systemUTC(); - System.out.println( clock.instant() ); - System.out.println( clock.millis() ); -```` -下面是程序在控制台上的输出: - - 2014-04-12T15:19:29.282Z - 1397315969360 - -我们需要关注的其他类是LocaleDate与LocalTime。LocaleDate只持有ISO-8601格式且无时区信息的日期部分。相应的,LocaleTime只持有ISO-8601格式且无时区信息的时间部分。LocaleDate与LocalTime都可以从Clock中得到。 -```` - // Get the local date and local time - final LocalDate date = LocalDate.now(); - final LocalDate dateFromClock = LocalDate.now( clock ); - - System.out.println( date ); - System.out.println( dateFromClock ); - - // Get the local date and local time - final LocalTime time = LocalTime.now(); - final LocalTime timeFromClock = LocalTime.now( clock ); - - System.out.println( time ); - System.out.println( timeFromClock ); -```` -下面是程序在控制台上的输出: - - 2014-04-12 - 2014-04-12 - 11:25:54.568 - 15:25:54.568 - -下面是程序在控制台上的输出: - - 2014-04-12T11:47:01.017-04:00[America/New_York] - 2014-04-12T15:47:01.017Z - 2014-04-12T08:47:01.017-07:00[America/Los_Angeles] - -最后,让我们看一下Duration类:在秒与纳秒级别上的一段时间。Duration使计算两个日期间的不同变的十分简单。下面让我们看一个这方面的例子。 -```` - // Get duration between two dates - final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 ); - final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 ); - - final Duration duration = Duration.between( from, to ); - System.out.println( "Duration in days: " + duration.toDays() ); - System.out.println( "Duration in hours: " + duration.toHours() ); -```` -上面的例子计算了两个日期2014年4月16号与2014年4月16号之间的过程。下面是程序在控制台上的输出: - -Duration in days: 365 -Duration in hours: 8783 -对Java 8在日期/时间API的改进整体印象是非常非常好的。一部分原因是因为它建立在“久战杀场”的Joda-Time基础上,另一方面是因为用来大量的时间来设计它,并且这次程序员的声音得到了认可。更多详情请参考官方文档。 - - -### 并行(parallel)数组 -Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。 -```` - package com.javacodegeeks.java8.parallel.arrays; - - import java.util.Arrays; - import java.util.concurrent.ThreadLocalRandom; - - public class ParallelArrays { - public static void main( String[] args ) { - long[] arrayOfLong = new long [ 20000 ]; - - Arrays.parallelSetAll( arrayOfLong, - index -> ThreadLocalRandom.current().nextInt( 1000000 ) ); - Arrays.stream( arrayOfLong ).limit( 10 ).forEach( - i -> System.out.print( i + " " ) ); - System.out.println(); - - Arrays.parallelSort( arrayOfLong ); - Arrays.stream( arrayOfLong ).limit( 10 ).forEach( - i -> System.out.print( i + " " ) ); - System.out.println(); - } - } -```` -上面的代码片段使用了parallelSetAll()方法来对一个有20000个元素的数组进行随机赋值。然后,调用parallelSort方法。这个程序首先打印出前10个元素的值,之后对整个数组排序。这个程序在控制台上的输出如下(请注意数组元素是随机生产的): - -Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 -Sorted: 39 220 263 268 325 607 655 678 723 793 - -### CompletableFuture - -在Java8之前,我们会使用JDK提供的Future接口来进行一些异步的操作,其实CompletableFuture也是实现了Future接口, 并且基于ForkJoinPool来执行任务,因此本质上来讲,CompletableFuture只是对原有API的封装, 而使用CompletableFuture与原来的Future的不同之处在于可以将两个Future组合起来,或者如果两个Future是有依赖关系的,可以等第一个执行完毕后再实行第二个等特性。 - -**先来看看基本的使用方式:** -```` - public Future getPriceAsync(final String product) { - final CompletableFuture futurePrice = new CompletableFuture<>(); - new Thread(() -> { - double price = calculatePrice(product); - futurePrice.complete(price); //完成后使用complete方法,设置future的返回值 - }).start(); - return futurePrice; - } -```` -得到Future之后就可以使用get方法来获取结果,CompletableFuture提供了一些工厂方法来简化这些API,并且使用函数式编程的方式来使用这些API,例如: - -Fufure price = CompletableFuture.supplyAsync(() -> calculatePrice(product)); -代码是不是一下子简洁了许多呢。之前说了,CompletableFuture可以组合多个Future,不管是Future之间有依赖的,还是没有依赖的。 - -**如果第二个请求依赖于第一个请求的结果,那么可以使用thenCompose方法来组合两个Future** -```` - public List findPriceAsync(String product) { - List> priceFutures = tasks.stream() - .map(task -> CompletableFuture.supplyAsync(() -> task.getPrice(product),executor)) - .map(future -> future.thenApply(Work::parse)) - .map(future -> future.thenCompose(work -> CompletableFuture.supplyAsync(() -> Count.applyCount(work), executor))) - .collect(Collectors.toList()); - - return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList()); - } -```` -上面这段代码使用了thenCompose来组合两个CompletableFuture。supplyAsync方法第二个参数接受一个自定义的Executor。 首先使用CompletableFuture执行一个任务,调用getPrice方法,得到一个Future,之后使用thenApply方法,将Future的结果应用parse方法, 之后再使用执行完parse之后的结果作为参数再执行一个applyCount方法,然后收集成一个CompletableFuture的List, 最后再使用一个流,调用CompletableFuture的join方法,这是为了等待所有的异步任务执行完毕,获得最后的结果。 - -注意,这里必须使用两个流,如果在一个流里调用join方法,那么由于Stream的延迟特性,所有的操作还是会串行的执行,并不是异步的。 - -**再来看一个两个Future之间没有依赖关系的例子:** -```` - Future futurePriceInUsd = CompletableFuture.supplyAsync(() -> shop.getPrice(“price1”)) - .thenCombine(CompletableFuture.supplyAsync(() -> shop.getPrice(“price2”)), (s1, s2) -> s1 + s2); -```` -这里有两个异步的任务,使用thenCombine方法来组合两个Future,thenCombine方法的第二个参数就是用来合并两个Future方法返回值的操作函数。 - -有时候,我们并不需要等待所有的异步任务结束,只需要其中的一个完成就可以了,CompletableFuture也提供了这样的方法: -```` - //假设getStream方法返回一个Stream> - CompletableFuture[] futures = getStream(“listen”).map(f -> f.thenAccept(System.out::println)).toArray(CompletableFuture[]::new); - //等待其中的一个执行完毕 - CompletableFuture.anyOf(futures).join(); - 使用anyOf方法来响应CompletableFuture的completion事件。 -```` -## Java虚拟机(JVM)的新特性 -PermGen空间被移除了,取而代之的是Metaspace(JEP 122)。JVM选项-XX:PermSize与-XX:MaxPermSize分别被-XX:MetaSpaceSize与-XX:MaxMetaspaceSize所代替。 - - -## 总结 -更多展望:Java 8通过发布一些可以增加程序员生产力的特性来推进这个伟大的平台的进步。现在把生产环境迁移到Java 8还为时尚早,但是在接下来的几个月里,它会被大众慢慢的接受。毫无疑问,现在是时候让你的代码与Java 8兼容,并且在Java 8足够安全稳定的时候迁移到Java 8。 - -## 参考文章 - -https://blog.csdn.net/shuaicihai/article/details/72615495 -https://blog.csdn.net/qq_34908167/article/details/79286697 -https://www.jianshu.com/p/4df02599aeb2 -https://www.cnblogs.com/yangzhilong/p/10973006.html -https://www.cnblogs.com/JackpotHan/p/9701147.html - diff --git "a/docs/Java/basic/JavaIO\346\265\201.md" "b/docs/Java/basic/JavaIO\346\265\201.md" deleted file mode 100644 index 9d1ecd1..0000000 --- "a/docs/Java/basic/JavaIO\346\265\201.md" +++ /dev/null @@ -1,450 +0,0 @@ -# 目录 - * [IO概述](#io概述) - * [什么是Java IO流](#什么是java-io流) - * [IO文件](#io文件) - * [字符流和字节流](#字符流和字节流) - * [IO管道](#io管道) - * [Java IO:网络](#java-io:网络) - * [字节和字符数组](#字节和字符数组) - * [System.in, System.out, System.err](#systemin-systemout-systemerr) - * [字符流的Buffered和Filter](#字符流的buffered和filter) - * [JavaIO流面试题](#javaio流面试题) - * [什么是IO流?](#什么是io流?) - * [字节流和字符流的区别。](#字节流和字符流的区别。) - * [Java中流类的超类主要由那些?](#java中流类的超类主要由那些?) - * [FileInputStream和FileOutputStream是什么?](#fileinputstream和fileoutputstream是什么?) - * [System.out.println()是什么?](#systemoutprintln是什么?) - * [什么是Filter流?](#什么是filter流?) - * [有哪些可用的Filter流?](#有哪些可用的filter流?) - * [在文件拷贝的时候,那一种流可用提升更多的性能?](#在文件拷贝的时候,那一种流可用提升更多的性能?) - * [说说管道流(Piped Stream)](#说说管道流piped-stream) - * [说说File类](#说说file类) - * [说说RandomAccessFile?](#说说randomaccessfile) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - - -本文参考 - -并发编程网 – ifeve.com - -## IO概述 - -> 在这一小节,我会试着给出Java IO(java.io)包下所有类的概述。更具体地说,我会根据类的用途对类进行分组。这个分组将会使你在未来的工作中,进行类的用途判定时,或者是为某个特定用途选择类时变得更加容易。 - - -**输入和输出** - - 术语“输入”和“输出”有时候会有一点让人疑惑。一个应用程序的输入往往是另外一个应用程序的输出 - - 那么OutputStream流到底是一个输出到目的地的流呢,还是一个产生输出的流?InputStream流到底会不会输出它的数据给读取数据的程序呢?就我个人而言,在第一天学习Java IO的时候我就感觉到了一丝疑惑。 - - 为了消除这个疑惑,我试着给输入和输出起一些不一样的别名,让它们从概念上与数据的来源和数据的流向相联系。 - -Java的IO包主要关注的是从原始数据源的读取以及输出原始数据到目标媒介。以下是最典型的数据源和目标媒介: - - 文件 - 管道 - 网络连接 - 内存缓存 - System.in, System.out, System.error(注:Java标准输入、输出、错误输出) -下面这张图描绘了一个程序从数据源读取数据,然后将数据输出到其他媒介的原理: - -![](http://ifeve.com/wp-content/uploads/2014/10/%E6%97%A0%E6%A0%87%E9%A2%981.png) - -**流** - - 在Java IO中,流是一个核心的概念。流从概念上来说是一个连续的数据流。你既可以从流中读取数据,也可以往流中写数据。流与数据源或者数据流向的媒介相关联。在Java IO中流既可以是字节流(以字节为单位进行读写),也可以是字符流(以字符为单位进行读写)。 - -类InputStream, OutputStream, Reader 和Writer -一个程序需要InputStream或者Reader从数据源读取数据,需要OutputStream或者Writer将数据写入到目标媒介中。以下的图说明了这一点: - -![](http://ifeve.com/wp-content/uploads/2014/10/%E6%97%A0%E6%A0%87%E9%A2%982.png) - -InputStream和Reader与数据源相关联,OutputStream和writer与目标媒介相关联。 - -**Java IO的用途和特征** - -Java IO中包含了许多InputStream、OutputStream、Reader、Writer的子类。这样设计的原因是让每一个类都负责不同的功能。这也就是为什么IO包中有这么多不同的类的缘故。各类用途汇总如下: - - 文件访问 - 网络访问 - 内存缓存访问 - 线程内部通信(管道) - 缓冲 - 过滤 - 解析 - 读写文本 (Readers / Writers) - 读写基本类型数据 (long, int etc.) - 读写对象 - -当通读过Java IO类的源代码之后,我们很容易就能了解这些用途。这些用途或多或少让我们更加容易地理解,不同的类用于针对不同业务场景。 - -Java IO类概述表 -已经讨论了数据源、目标媒介、输入、输出和各类不同用途的Java IO类,接下来是一张通过输入、输出、基于字节或者字符、以及其他比如缓冲、解析之类的特定用途划分的大部分Java IO类的表格。 - -![](http://ifeve.com/wp-content/uploads/2014/10/QQ%E6%88%AA%E5%9B%BE20141020174145.png) - -Java IO类图 - -![](https://images.cnblogs.com/cnblogs_com/davidgu/java_io_hierarchy.jpg) - -### 什么是Java IO流 - -Java IO流是既可以从中读取,也可以写入到其中的数据流。正如这个系列教程之前提到过的,流通常会与数据源、数据流向目的地相关联,比如文件、网络等等。 - -流和数组不一样,不能通过索引读写数据。在流中,你也不能像数组那样前后移动读取数据,除非使用RandomAccessFile 处理文件。流仅仅只是一个连续的数据流。 - -某些类似PushbackInputStream 流的实现允许你将数据重新推回到流中,以便重新读取。然而你只能把有限的数据推回流中,并且你不能像操作数组那样随意读取数据。流中的数据只能够顺序访问。 -> -> Java IO流通常是基于字节或者基于字符的。字节流通常以“stream”命名,比如InputStream和OutputStream。除了DataInputStream 和DataOutputStream 还能够读写int, long, float和double类型的值以外,其他流在一个操作时间内只能读取或者写入一个原始字节。 -> -> 字符流通常以“Reader”或者“Writer”命名。字符流能够读写字符(比如Latin1或者Unicode字符)。可以浏览Java Readers and Writers获取更多关于字符流输入输出的信息。 - -**InputStream** - -java.io.InputStream类是所有Java IO输入流的基类。如果你正在开发一个从流中读取数据的组件,请尝试用InputStream替代任何它的子类(比如FileInputStream)进行开发。这么做能够让你的代码兼容任何类型而非某种确定类型的输入流。 - -**组合流** - -你可以将流整合起来以便实现更高级的输入和输出操作。比如,一次读取一个字节是很慢的,所以可以从磁盘中一次读取一大块数据,然后从读到的数据块中获取字节。为了实现缓冲,可以把InputStream包装到BufferedInputStream中。 - -代码示例 - InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt")); - -> 缓冲同样可以应用到OutputStream中。你可以实现将大块数据批量地写入到磁盘(或者相应的流)中,这个功能由BufferedOutputStream实现。 -> -> 缓冲只是通过流整合实现的其中一个效果。你可以把InputStream包装到PushbackInputStream中,之后可以将读取过的数据推回到流中重新读取,在解析过程中有时候这样做很方便。或者,你可以将两个InputStream整合成一个SequenceInputStream。 -> -> 将不同的流整合到一个链中,可以实现更多种高级操作。通过编写包装了标准流的类,可以实现你想要的效果和过滤器。 - -### IO文件 - -在Java应用程序中,文件是一种常用的数据源或者存储数据的媒介。所以这一小节将会对Java中文件的使用做一个简短的概述。这篇文章不会对每一个技术细节都做出解释,而是会针对文件存取的方法提供给你一些必要的知识点。在之后的文章中,将会更加详细地描述这些方法或者类,包括方法示例等等。 - - -**通过Java IO读文件** - - 如果你需要在不同端之间读取文件,你可以根据该文件是二进制文件还是文本文件来选择使用FileInputStream或者FileReader。 - - 这两个类允许你从文件开始到文件末尾一次读取一个字节或者字符,或者将读取到的字节写入到字节数组或者字符数组。你不必一次性读取整个文件,相反你可以按顺序地读取文件中的字节和字符。 - -如果你需要跳跃式地读取文件其中的某些部分,可以使用RandomAccessFile。 - -**通过Java IO写文件** - - 如果你需要在不同端之间进行文件的写入,你可以根据你要写入的数据是二进制型数据还是字符型数据选用FileOutputStream或者FileWriter。 - - 你可以一次写入一个字节或者字符到文件中,也可以直接写入一个字节数组或者字符数据。数据按照写入的顺序存储在文件当中。 - -**通过Java IO随机存取文件** - -正如我所提到的,你可以通过RandomAccessFile对文件进行随机存取。 - - 随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。 - - 这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。 - -下面是具体例子: -```` - @Test - //文件流范例,打开一个文件的输入流,读取到字节数组,再写入另一个文件的输出流 - public void test1() { - try { - FileInputStream fileInputStream = new FileInputStream(new File("a.txt")); - FileOutputStream fileOutputStream = new FileOutputStream(new File("b.txt")); - byte []buffer = new byte[128]; - while (fileInputStream.read(buffer) != -1) { - fileOutputStream.write(buffer); - } - //随机读写,通过mode参数来决定读或者写 - RandomAccessFile randomAccessFile = new RandomAccessFile(new File("c.txt"), "rw"); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } -```` -### 字符流和字节流 - -Java IO的Reader和Writer除了基于字符之外,其他方面都与InputStream和OutputStream非常类似。他们被用于读写文本。InputStream和OutputStream是基于字节的,还记得吗? - -Reader -Reader类是Java IO中所有Reader的基类。子类包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader。 - -Writer -Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。 - -这是一个简单的Java IO Reader的例子: -```` -Reader reader = new FileReader("c:\\data\\myfile.txt"); - -int data = reader.read(); - -while(data != -1){ - - char dataChar = (char) data; - - data = reader.read(); - -} -```` -你通常会使用Reader的子类,而不会直接使用Reader。Reader的子类包括InputStreamReader,CharArrayReader,FileReader等等。可以查看Java IO概述浏览完整的Reader表格。 - -**整合Reader与InputStream** - -一个Reader可以和一个InputStream相结合。如果你有一个InputStream输入流,并且想从其中读取字符,可以把这个InputStream包装到InputStreamReader中。把InputStream传递到InputStreamReader的构造函数中: -```` -Reader reader = new InputStreamReader(inputStream); -```` -在构造函数中可以指定解码方式。 - -**Writer** - -Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。这是一个Java IO Writer的例子: -```` -Writer writer = new FileWriter("c:\\data\\file-output.txt"); - -writer.write("Hello World Writer"); - -writer.close(); -```` -同样,你最好使用Writer的子类,不需要直接使用Writer,因为子类的实现更加明确,更能表现你的意图。常用子类包括OutputStreamWriter,CharArrayWriter,FileWriter等。Writer的write(int c)方法,会将传入参数的低16位写入到Writer中,忽略高16位的数据。 - -**整合Writer和OutputStream** - -与Reader和InputStream类似,一个Writer可以和一个OutputStream相结合。把OutputStream包装到OutputStreamWriter中,所有写入到OutputStreamWriter的字符都将会传递给OutputStream。这是一个OutputStreamWriter的例子: -```` -Writer writer = new OutputStreamWriter(outputStream); -```` -### IO管道 - -Java IO中的管道为运行在同一个JVM中的两个线程提供了通信的能力。所以管道也可以作为数据源以及目标媒介。 - -你不能利用管道与不同的JVM中的线程通信(不同的进程)。在概念上,Java的管道不同于Unix/Linux系统中的管道。在Unix/Linux中,运行在不同地址空间的两个进程可以通过管道通信。在Java中,通信的双方应该是运行在同一进程中的不同线程。 - -通过Java IO创建管道 - - 可以通过Java IO中的PipedOutputStream和PipedInputStream创建管道。一个PipedInputStream流应该和一个PipedOutputStream流相关联。 - - 一个线程通过PipedOutputStream写入的数据可以被另一个线程通过相关联的PipedInputStream读取出来。 - -Java IO管道示例 -这是一个如何将PipedInputStream和PipedOutputStream关联起来的简单例子: -```` -//使用管道来完成两个线程间的数据点对点传递 - @Test - public void test2() throws IOException { - PipedInputStream pipedInputStream = new PipedInputStream(); - PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream); - new Thread(new Runnable() { - @Override - public void run() { - try { - pipedOutputStream.write("hello input".getBytes()); - pipedOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }).start(); - new Thread(new Runnable() { - @Override - public void run() { - try { - byte []arr = new byte[128]; - while (pipedInputStream.read(arr) != -1) { - System.out.println(Arrays.toString(arr)); - } - pipedInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }).start(); -```` - -管道和线程 -请记得,当使用两个相关联的管道流时,务必将它们分配给不同的线程。read()方法和write()方法调用时会导致流阻塞,这意味着如果你尝试在一个线程中同时进行读和写,可能会导致线程死锁。 - -管道的替代 -除了管道之外,一个JVM中不同线程之间还有许多通信的方式。实际上,线程在大多数情况下会传递完整的对象信息而非原始的字节数据。但是,如果你需要在线程之间传递字节数据,Java IO的管道是一个不错的选择。 - -### Java IO:网络 - -Java中网络的内容或多或少的超出了Java IO的范畴。关于Java网络更多的是在我的Java网络教程中探讨。但是既然网络是一个常见的数据来源以及数据流目的地,并且因为你使用Java IO的API通过网络连接进行通信,所以本文将简要的涉及网络应用。 - - -当两个进程之间建立了网络连接之后,他们通信的方式如同操作文件一样:利用InputStream读取数据,利用OutputStream写入数据。换句话来说,Java网络API用来在不同进程之间建立网络连接,而Java IO则用来在建立了连接之后的进程之间交换数据。 - -基本上意味着如果你有一份能够对文件进行写入某些数据的代码,那么这些数据也可以很容易地写入到网络连接中去。你所需要做的仅仅只是在代码中利用OutputStream替代FileOutputStream进行数据的写入。因为FileOutputStream是OuputStream的子类,所以这么做并没有什么问题。 -```` - //从网络中读取字节流也可以直接使用OutputStream - public void test3() { - //读取网络进程的输出流 - OutputStream outputStream = new OutputStream() { - @Override - public void write(int b) throws IOException { - } - }; - } - public void process(OutputStream ouput) throws IOException { - //处理网络信息 - //do something with the OutputStream - } -```` -### 字节和字符数组 - - -从InputStream或者Reader中读入数组 - -从OutputStream或者Writer中写数组 - -在java中常用字节和字符数组在应用中临时存储数据。而这些数组又是通常的数据读取来源或者写入目的地。如果你需要在程序运行时需要大量读取文件里的内容,那么你也可以把一个文件加载到数组中。 - -前面的例子中,字符数组或字节数组是用来缓存数据的临时存储空间,不过它们同时也可以作为数据来源或者写入目的地。 -举个例子: -```` - //字符数组和字节数组在io过程中的作用 - public void test4() { - //arr和brr分别作为数据源 - char []arr = {'a','c','d'}; - CharArrayReader charArrayReader = new CharArrayReader(arr); - byte []brr = {1,2,3,4,5}; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(brr); - } -```` -### System.in, System.out, System.err - -System.in, System.out, System.err这3个流同样是常见的数据来源和数据流目的地。使用最多的可能是在控制台程序里利用System.out将输出打印到控制台上。 - -JVM启动的时候通过Java运行时初始化这3个流,所以你不需要初始化它们(尽管你可以在运行时替换掉它们)。 - - - System.in - System.in是一个典型的连接控制台程序和键盘输入的InputStream流。通常当数据通过命令行参数或者配置文件传递给命令行Java程序的时候,System.in并不是很常用。图形界面程序通过界面传递参数给程序,这是一块单独的Java IO输入机制。 - - System.out - System.out是一个PrintStream流。System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。System.out也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。 - - System.err - System.err是一个PrintStream流。System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。一些类似Eclipse的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过System.err输出到控制台上。 - -System.out和System.err的简单例子: -这是一个System.out和System.err结合使用的简单示例: -```` - //测试System.in, System.out, System.err - public static void main(String[] args) { - int in = new Scanner(System.in).nextInt(); - System.out.println(in); - System.out.println("out"); - System.err.println("err"); - //输入10,结果是 - // err(红色) - // 10 - // out - } -```` - -### 字符流的Buffered和Filter - -BufferedReader能为字符输入流提供缓冲区,可以提高许多IO处理的速度。你可以一次读取一大块的数据,而不需要每次从网络或者磁盘中一次读取一个字节。特别是在访问大量磁盘数据时,缓冲通常会让IO快上许多。 - -BufferedReader和BufferedInputStream的主要区别在于,BufferedReader操作字符,而BufferedInputStream操作原始字节。只需要把Reader包装到BufferedReader中,就可以为Reader添加缓冲区(译者注:默认缓冲区大小为8192字节,即8KB)。代码如下: - - Reader input = new BufferedReader(new FileReader("c:\\data\\input-file.txt")); - -你也可以通过传递构造函数的第二个参数,指定缓冲区大小,代码如下: - - Reader input = new BufferedReader(new FileReader("c:\\data\\input-file.txt"), 8 * 1024); - -这个例子设置了8KB的缓冲区。最好把缓冲区大小设置成1024字节的整数倍,这样能更高效地利用内置缓冲区的磁盘。 - -除了能够为输入流提供缓冲区以外,其余方面BufferedReader基本与Reader类似。BufferedReader还有一个额外readLine()方法,可以方便地一次性读取一整行字符。 - -**BufferedWriter** - -与BufferedReader类似,BufferedWriter可以为输出流提供缓冲区。可以构造一个使用默认大小缓冲区的BufferedWriter(译者注:默认缓冲区大小8 * 1024B),代码如下: - - Writer writer = new BufferedWriter(new FileWriter("c:\\data\\output-file.txt")); - -也可以手动设置缓冲区大小,代码如下: - - Writer writer = new BufferedWriter(new FileWriter("c:\\data\\output-file.txt"), 8 * 1024); - -为了更好地使用内置缓冲区的磁盘,同样建议把缓冲区大小设置成1024的整数倍。除了能够为输出流提供缓冲区以外,其余方面BufferedWriter基本与Writer类似。类似地,BufferedWriter也提供了writeLine()方法,能够把一行字符写入到底层的字符输出流中。 - - -**值得注意是,你需要手动flush()方法确保写入到此输出流的数据真正写入到磁盘或者网络中。** - -**FilterReader** - -与FilterInputStream类似,FilterReader是实现自定义过滤输入字符流的基类,基本上它仅仅只是简单覆盖了Reader中的所有方法。 - -就我自己而言,我没发现这个类明显的用途。除了构造函数取一个Reader变量作为参数之外,我没看到FilterReader任何对Reader新增或者修改的地方。如果你选择继承FilterReader实现自定义的类,同样也可以直接继承自Reader从而避免额外的类层级结构。 - -## JavaIO流面试题 - -### 什么是IO流? -它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。 - -### 字节流和字符流的区别。 -字节流在JDK1.0中就被引进了,用于操作包含ASCII字符的文件。JAVA也支持其他的字符如Unicode,为了读取包含Unicode字符的文件,JAVA语言设计者在JDK1.1中引入了字符流。ASCII作为Unicode的子集,对于英语字符的文件,可以可以使用字节流也可以使用字符流。 - -### Java中流类的超类主要由那些? - -java.io.InputStream -java.io.OutputStream -java.io.Reader -java.io.Writer - -### FileInputStream和FileOutputStream是什么? -这是在拷贝文件操作的时候,经常用到的两个类。在处理小文件的时候,它们性能表现还不错,在大文件的时候,最好使用BufferedInputStream (或 BufferedReader) 和 BufferedOutputStream (或 BufferedWriter) - -### System.out.println()是什么? -println是PrintStream的一个方法。out是一个静态PrintStream类型的成员变量,System是一个java.lang包中的类,用于和底层的操作系统进行交互。 - -### 什么是Filter流? -Filter Stream是一种IO流主要作用是用来对存在的流增加一些额外的功能,像给目标文件增加源文件中不存在的行数,或者增加拷贝的性能。 - -### 有哪些可用的Filter流? - -在java.io包中主要由4个可用的filter Stream。两个字节filter stream,两个字符filter stream. 分别是FilterInputStream, FilterOutputStream, FilterReader and FilterWriter.这些类是抽象类,不能被实例化的。 - -### 在文件拷贝的时候,那一种流可用提升更多的性能? -在字节流的时候,使用BufferedInputStream和BufferedOutputStream。 -在字符流的时候,使用BufferedReader 和 BufferedWriter - -### 说说管道流(Piped Stream) -有四种管道流, PipedInputStream, PipedOutputStream, PipedReader 和 PipedWriter.在多个线程或进程中传递数据的时候管道流非常有用。 - -### 说说File类 -它不属于 IO流,也不是用于文件操作的,它主要用于知道一个文件的属性,读写权限,大小等信息。 - -### 说说RandomAccessFile? -它在java.io包中是一个特殊的类,既不是输入流也不是输出流,它两者都可以做到。他是Object的直接子类。通常来说,一个流只有一个功能,要么读,要么写。但是RandomAccessFile既可以读文件,也可以写文件。 DataInputStream 和 DataOutStream有的方法,在RandomAccessFile中都存在。 - -## 参考文章 - -https://www.imooc.com/article/24305 -https://www.cnblogs.com/UncleWang001/articles/10454685.html -https://www.cnblogs.com/Jixiangwei/p/Java.html -https://blog.csdn.net/baidu_37107022/article/details/76890019 - diff --git "a/docs/Java/basic/Java\344\270\255\347\232\204Class\347\261\273\345\222\214Object\347\261\273.md" "b/docs/Java/basic/Java\344\270\255\347\232\204Class\347\261\273\345\222\214Object\347\261\273.md" deleted file mode 100644 index 7ff3baa..0000000 --- "a/docs/Java/basic/Java\344\270\255\347\232\204Class\347\261\273\345\222\214Object\347\261\273.md" +++ /dev/null @@ -1,721 +0,0 @@ -# 目录 - - * [Java中Class类及用法](#java中class类及用法) - * [Class类原理](#class类原理) - * [如何获得一个Class类对象](#如何获得一个class类对象) - * [使用Class类的对象来生成目标类的实例](#使用class类的对象来生成目标类的实例) - * [Object类](#object类) - * [类构造器public Object();](#类构造器public-object) - * [registerNatives()方法;](#registernatives方法) - * [Clone()方法实现浅拷贝](#clone方法实现浅拷贝) - * [getClass()方法](#getclass方法) - * [equals()方法](#equals方法) - * [hashCode()方法;](#hashcode方法) - * [toString()方法](#tostring方法) - * [wait() notify() notifAll()](#wait-notify-notifall) - * [finalize()方法](#finalize方法) - * [CLass类和Object类的关系](#class类和object类的关系) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - -## Java中Class类及用法 -Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。 - -> 这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。 - -说白了就是: - -> Class类也是类的一种,只是名字和class关键字高度相似。Java是大小写敏感的语言。 - -> Class类的对象内容是你创建的类的类型信息,比如你创建一个shapes类,那么,Java会生成一个内容是shapes的Class类的对象 - -> Class类的对象不能像普通类一样,以 new shapes() 的方式创建,它的对象只能由JVM创建,因为这个类没有public构造函数 -```` -/* - * Private constructor. Only the Java Virtual Machine creates Class objects. - * This constructor is not used and prevents the default constructor being - * generated. - */ - //私有构造方法,只能由jvm进行实例化 -private Class(ClassLoader loader) { - // Initialize final field for classLoader. The initialization value of non-null - // prevents future JIT optimizations from assuming this final field is null. - classLoader = loader; -} -```` -> Class类的作用是运行时提供或获得某个对象的类型信息,和C++中的typeid()函数类似。这些信息也可用于反射。 - -### Class类原理 - -看一下Class类的部分源码 -```` - //Class类中封装了类型的各种信息。在jvm中就是通过Class类的实例来获取每个Java类的所有信息的。 - - public class Class类 { - Class aClass = null; - - // private EnclosingMethodInfo getEnclosingMethodInfo() { - // Object[] enclosingInfo = getEnclosingMethod0(); - // if (enclosingInfo == null) - // return null; - // else { - // return new EnclosingMethodInfo(enclosingInfo); - // } - // } - - /**提供原子类操作 - * Atomic operations support. - */ - // private static class Atomic { - // // initialize Unsafe machinery here, since we need to call Class.class instance method - // // and have to avoid calling it in the static initializer of the Class class... - // private static final Unsafe unsafe = Unsafe.getUnsafe(); - // // offset of Class.reflectionData instance field - // private static final long reflectionDataOffset; - // // offset of Class.annotationType instance field - // private static final long annotationTypeOffset; - // // offset of Class.annotationData instance field - // private static final long annotationDataOffset; - // - // static { - // Field[] fields = Class.class.getDeclaredFields0(false); // bypass caches - // reflectionDataOffset = objectFieldOffset(fields, "reflectionData"); - // annotationTypeOffset = objectFieldOffset(fields, "annotationType"); - // annotationDataOffset = objectFieldOffset(fields, "annotationData"); - // } - - //提供反射信息 - // reflection data that might get invalidated when JVM TI RedefineClasses() is called - // private static class ReflectionData { - // volatile Field[] declaredFields; - // volatile Field[] publicFields; - // volatile Method[] declaredMethods; - // volatile Method[] publicMethods; - // volatile Constructor[] declaredConstructors; - // volatile Constructor[] publicConstructors; - // // Intermediate results for getFields and getMethods - // volatile Field[] declaredPublicFields; - // volatile Method[] declaredPublicMethods; - // volatile Class[] interfaces; - // - // // Value of classRedefinedCount when we created this ReflectionData instance - // final int redefinedCount; - // - // ReflectionData(int redefinedCount) { - // this.redefinedCount = redefinedCount; - // } - // } - //方法数组 - // static class MethodArray { - // // Don't add or remove methods except by add() or remove() calls. - // private Method[] methods; - // private int length; - // private int defaults; - // - // MethodArray() { - // this(20); - // } - // - // MethodArray(int initialSize) { - // if (initialSize < 2) - // throw new IllegalArgumentException("Size should be 2 or more"); - // - // methods = new Method[initialSize]; - // length = 0; - // defaults = 0; - // } - - //注解信息 - // annotation data that might get invalidated when JVM TI RedefineClasses() is called - // private static class AnnotationData { - // final Map, Annotation> annotations; - // final Map, Annotation> declaredAnnotations; - // - // // Value of classRedefinedCount when we created this AnnotationData instance - // final int redefinedCount; - // - // AnnotationData(Map, Annotation> annotations, - // Map, Annotation> declaredAnnotations, - // int redefinedCount) { - // this.annotations = annotations; - // this.declaredAnnotations = declaredAnnotations; - // this.redefinedCount = redefinedCount; - // } - // } - } -```` - -> 我们都知道所有的java类都是继承了object这个类,在object这个类中有一个方法:getclass().这个方法是用来取得该类已经被实例化了的对象的该类的引用,这个引用指向的是Class类的对象。 -> -> 我们自己无法生成一个Class对象(构造函数为private),而 这个Class类的对象是在当各类被调入时,由 Java 虚拟机自动创建 Class 对象,或通过类装载器中的 defineClass 方法生成。 - - //通过该方法可以动态地将字节码转为一个Class类对象 - protected final Class defineClass(String name, byte[] b, int off, int len) - throws ClassFormatError - { - return defineClass(name, b, off, len, null); - } -> - -### 如何获得一个Class类对象 - -请注意,以下这些方法都是值、指某个类对应的Class对象已经在堆中生成以后,我们通过不同方式获取对这个Class对象的引用。而上面说的DefineClass才是真正将字节码加载到虚拟机的方法,会在堆中生成新的一个Class对象。 - -第一种办法,Class类的forName函数 -> -> public class shapes{} -> Class obj= Class.forName("shapes"); -> 第二种办法,使用对象的getClass()函数 - -> public class shapes{} -> shapes s1=new shapes(); -> Class obj=s1.getClass(); -> Class obj1=s1.getSuperclass();//这个函数作用是获取shapes类的父类的类型 - -第三种办法,使用类字面常量 - -> Class obj=String.class; -> Class obj1=int.class; -> 注意,使用这种办法生成Class类对象时,不会使JVM自动加载该类(如String类)。==而其他办法会使得JVM初始化该类。== - -### 使用Class类的对象来生成目标类的实例 -> -> 生成不精确的object实例 -> - -获取一个Class类的对象后,可以用 newInstance() 函数来生成目标类的一个实例。然而,该函数并不能直接生成目标类的实例,只能生成object类的实例 - -> Class obj=Class.forName("shapes"); -> Object ShapesInstance=obj.newInstance(); -> 使用泛化Class引用生成带类型的目标实例 - -> -> Class obj=shapes.class; -> shapes newShape=obj.newInstance(); -> 因为有了类型限制,所以使用泛化Class语法的对象引用不能指向别的类。 - - Class obj1=int.class; - Class obj2=int.class; - obj1=double.class; - //obj2=double.class; 这一行代码是非法的,obj2不能改指向别的类 - - 然而,有个灵活的用法,使得你可以用Class的对象指向基类的任何子类。 - Class obj=int.class; - obj=Number.class; - obj=double.class; - - 因此,以下语法生成的Class对象可以指向任何类。 - Class obj=int.class; - obj=double.class; - obj=shapes.class; - 最后一个奇怪的用法是,当你使用这种泛型语法来构建你手头有的一个Class类的对象的基类对象时,必须采用以下的特殊语法 - - public class shapes{} - class round extends shapes{} - Class rclass=round.class; - Class sclass= rclass.getSuperClass(); - //Class sclass=rclass.getSuperClass(); - - 我们明知道,round的基类就是shapes,但是却不能直接声明 Class < shapes >,必须使用特殊语法 - - Class < ? super round > - - 这个记住就可以啦。 - -## Object类 - -这部分主要参考http://ihenu.iteye.com/blog/2233249 - -Object类是Java中其他所有类的祖先,没有Object类Java面向对象无从谈起。作为其他所有类的基类,Object具有哪些属性和行为,是Java语言设计背后的思维体现。 - -Object类位于java.lang包中,java.lang包包含着Java最基础和核心的类,在编译时会自动导入。Object类没有定义属性,一共有13个方法,13个方法之中并不是所有方法都是子类可访问的,一共有9个方法是所有子类都继承了的。 - -先大概介绍一下这些方法 - - 1.clone方法 - 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。 - 2.getClass方法 - final方法,获得运行时类型。 - 3.toString方法 - 该方法用得比较多,一般子类都有覆盖。 - 4.finalize方法 - 该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。 - 5.equals方法 - 该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。 - 6.hashCode方法 - 该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。 - 一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。 - 7.wait方法 - wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。 - 调用该方法后当前线程进入睡眠状态,直到以下事件发生。 - (1)其他线程调用了该对象的notify方法。 - (2)其他线程调用了该对象的notifyAll方法。 - (3)其他线程调用了interrupt中断该线程。 - (4)时间间隔到了。 - 此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。 - 8.notify方法 - 该方法唤醒在该对象上等待的某个线程。 - 9.notifyAll方法 - 该方法唤醒在该对象上等待的所有线程。 - -### 类构造器public Object(); - -> 大部分情况下,Java中通过形如 new A(args..)形式创建一个属于该类型的对象。其中A即是类名,A(args..)即此类定义中相对应的构造函数。通过此种形式创建的对象都是通过类中的构造函数完成。 - -> 为体现此特性,Java中规定:在类定义过程中,对于未定义构造函数的类,默认会有一个无参数的构造函数,作为所有类的基类,Object类自然要反映出此特性,在源码中,未给出Object类构造函数定义,但实际上,此构造函数是存在的。 -> -> 当然,并不是所有的类都是通过此种方式去构建,也自然的,并不是所有的类构造函数都是public。 - - -### registerNatives()方法; -```` -private static native void registerNatives(); -```` - -> registerNatives函数前面有native关键字修饰,Java中,用native关键字修饰的函数表明该方法的实现并不是在Java中去完成,而是由C/C++去完成,并被编译成了.dll,由Java去调用。 -> -> 方法的具体实现体在dll文件中,对于不同平台,其具体实现应该有所不同。用native修饰,即表示操作系统,需要提供此方法,Java本身需要使用。 -> -> 具体到registerNatives()方法本身,其主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦。 -> -> 既然如此,可能有人会问,registerNatives()修饰符为private,且并没有执行,作用何以达到?其实,在Java源码中,此方法的声明后有紧接着一段静态代码块: - -```` -private static native void registerNatives(); -static { - registerNatives(); -} -```` -### Clone()方法实现浅拷贝 -```` -protected native Object clone() throwsCloneNotSupportedException; -```` -> 看,clode()方法又是一个被声明为native的方法,因此,我们知道了clone()方法并不是Java的原生方法,具体的实现是有C/C++完成的。clone英文翻译为"克隆",其目的是创建并返回此对象的一个副本。 - -> 形象点理解,这有一辆科鲁兹,你看着不错,想要个一模一样的。你调用此方法即可像变魔术一样变出一辆一模一样的科鲁兹出来。配置一样,长相一样。但从此刻起,原来的那辆科鲁兹如果进行了新的装饰,与你克隆出来的这辆科鲁兹没有任何关系了。 -> -> 你克隆出来的对象变不变完全在于你对克隆出来的科鲁兹有没有进行过什么操作了。Java术语表述为:clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。 - -明白了clone的含义后,接下来看看如果调用clone()函数对象进行此克隆操作。 - -首先看一下下面的这个例子: - -```` - package com.corn.objectsummary; - - import com.corn.Person; - - public class ObjectTest { - - public static void main(String[] args) { - - Object o1 = new Object(); - // The method clone() from the type Object is not visible - Object clone = o1.clone(); - } - - } -```` - -> 例子很简单,在main()方法中,new一个Oject对象后,想直接调用此对象的clone方法克隆一个对象,但是出现错误提示:"The method clone() from the type Object is not visible" -> -> why? 根据提示,第一反应是ObjectTest类中定义的Oject对象无法访问其clone()方法。回到Object类中clone()方法的定义,可以看到其被声明为protected,估计问题就在这上面了,protected修饰的属性或方法表示:在同一个包内或者不同包的子类可以访问。 -> -> 显然,Object类与ObjectTest类在不同的包中,但是ObjectTest继承自Object,是Object类的子类,于是,现在却出现子类中通过Object引用不能访问protected方法,原因在于对"不同包中的子类可以访问"没有正确理解。 -> -> "不同包中的子类可以访问",是指当两个类不在同一个包中的时候,继承自父类的子类内部且主调(调用者)为子类的引用时才能访问父类用protected修饰的成员(属性/方法)。 在子类内部,主调为父类的引用时并不能访问此protected修饰的成员。!(super关键字除外) - -于是,上例改成如下形式,我们发现,可以正常编译: - -```` -public class clone方法 { - public static void main(String[] args) { - - } - public void test1() { - - User user = new User(); - //User copy = user.clone(); - } - public void test2() { - User user = new User(); - //User copy = (User)user.clone(); - } -} - -```` -是的,因为此时的主调已经是子类的引用了。 - -> 上述代码在运行过程中会抛出"java.lang.CloneNotSupportedException",表明clone()方法并未正确执行完毕,问题的原因在与Java中的语法规定: -> -> clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,并且子类直接调用Object类的clone()方法,则会抛出CloneNotSupportedException异常。 -> -> Cloneable接口仅是一个表示接口,接口本身不包含任何方法,用来指示Object.clone()可以合法的被子类引用所调用。 -> -> 于是,上述代码改成如下形式,即可正确指定clone()方法以实现克隆。 -```` -public class User implements Cloneable{ - public int id; - public String name; - public UserInfo userInfo; - - public static void main(String[] args) { - User user = new User(); - UserInfo userInfo = new UserInfo(); - user.userInfo = userInfo; - System.out.println(user); - System.out.println(user.userInfo); - try { - User copy = (User) user.clone(); - System.out.println(copy); - System.out.println(copy.userInfo); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - } - } - //拷贝的User实例与原来不一样,是两个对象。 - // com.javase.Class和Object.Object方法.用到的类.User@4dc63996 - // com.javase.Class和Object.Object方法.用到的类.UserInfo@d716361 - //而拷贝后对象的userinfo引用对象是同一个。 - //所以这是浅拷贝 - // com.javase.Class和Object.Object方法.用到的类.User@6ff3c5b5 - // com.javase.Class和Object.Object方法.用到的类.UserInfo@d716361 - } -```` - -总结: -clone方法实现的是浅拷贝,只拷贝当前对象,并且在堆中分配新的空间,放这个复制的对象。但是对象如果里面有其他类的子对象,那么就不会拷贝到新的对象中。 - -深拷贝和浅拷贝的区别 - -> 浅拷贝 -> 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。 -> -> 深拷贝 -> 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。 -> 现在为了要在clone对象时进行深拷贝, 那么就要Clonable接口,覆盖并实现clone方法,除了调用父类中的clone方法得到新的对象, 还要将该类中的引用变量也clone出来。如果只是用Object中默认的clone方法,是浅拷贝的。 - -那么这两种方式有什么相同和不同呢? - -> new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。 -> -> 分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。 -> -> 而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, -> -> 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。 - -也就是说,一个对象在浅拷贝以后,只是把对象复制了一份放在堆空间的另一个地方,但是成员变量如果有引用指向其他对象,这个引用指向的对象和被拷贝的对象中引用指向的对象是一样的。当然,基本数据类型还是会重新拷贝一份的。 - -### getClass()方法 -```` -public final native Class getClass(); - -```` - -> getClass()也是一个native方法,返回的是此Object对象的类对象/运行时类对象Class。效果与Object.class相同。 -> -> 首先解释下"类对象"的概念:在Java中,类是是对具有一组相同特征或行为的实例的抽象并进行描述,对象则是此类所描述的特征或行为的具体实例。 -> -> 作为概念层次的类,其本身也具有某些共同的特性,如都具有类名称、由类加载器去加载,都具有包,具有父类,属性和方法等。 -> -> 于是,Java中定义了一个类,Class,去描述其他类所具有的这些特性,因此,从此角度去看,类本身也都是属于Class类的对象。为与经常意义上的对象相区分,在此称之为"类对象"。 -```` - public class getClass方法 { - public static void main(String[] args) { - User user = new User(); - //getclass方法是native方法,可以取到堆区唯一的Class对象 - Class aClass = user.getClass(); - Class bClass = User.class; - try { - Class cClass = Class.forName("com.javase.Class和Object.Object方法.用到的类.User"); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - System.out.println(aClass); - System.out.println(bClass); - // class com.javase.Class和Object.Object方法.用到的类.User - // class com.javase.Class和Object.Object方法.用到的类.User - try { - User a = (User) aClass.newInstance(); - - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - } -```` - -此处主要大量涉及到Java中的反射知识 - -### equals()方法 - -5.public boolean equals(Object obj); -> -> 与equals在Java中经常被使用,大家也都知道与equals的区别: -> -> ==表示的是变量值完成相同(对于基础类型,地址中存储的是值,引用类型则存储指向实际对象的地址); -> -> equals表示的是对象的内容完全相同,此处的内容多指对象的特征/属性。 - -实际上,上面说法是不严谨的,更多的只是常见于String类中。首先看一下Object类中关于equals()方法的定义: - - - public boolean equals(Object obj) { - return (this == obj); - } - -> 由此可见,Object原生的equals()方法内部调用的正是==,与==具有相同的含义。既然如此,为什么还要定义此equals()方法? -> -> equals()方法的正确理解应该是:判断两个对象是否相等。那么判断对象相等的标尺又是什么? -> -> 如上,在object类中,此标尺即为==。当然,这个标尺不是固定的,其他类中可以按照实际的需要对此标尺含义进行重定义。如String类中则是依据字符串内容是否相等来重定义了此标尺含义。如此可以增加类的功能型和实际编码的灵活性。当然了,如果自定义的类没有重写equals()方法来重新定义此标尺,那么默认的将是其父类的equals(),直到object基类。 -> -> 如下场景的实际业务需求,对于User bean,由实际的业务需求可知当属性uid相同时,表示的是同一个User,即两个User对象相等。则可以重写equals以重定义User对象相等的标尺。 - - -ObjectTest中打印出true,因为User类定义中重写了equals()方法,这很好理解,很可能张三是一个人小名,张三丰才是其大名,判断这两个人是不是同一个人,这时只用判断uid是否相同即可。 - -> 如上重写equals方法表面上看上去是可以了,实则不然。因为它破坏了Java中的约定:重写equals()方法必须重写hasCode()方法。 - -### hashCode()方法; -```` -public native int hashCode() -```` - -hashCode()方法返回一个整形数值,表示该对象的哈希码值。 - -hashCode()具有如下约定: - -> 1).在Java应用程序程序执行期间,对于同一对象多次调用hashCode()方法时,其返回的哈希码是相同的,前提是将对象进行equals比较时所用的标尺信息未做修改。在Java应用程序的一次执行到另外一次执行,同一对象的hashCode()返回的哈希码无须保持一致; -> -> 2).如果两个对象相等(依据:调用equals()方法),那么这两个对象调用hashCode()返回的哈希码也必须相等; -> -> 3).反之,两个对象调用hasCode()返回的哈希码相等,这两个对象不一定相等。 - - 即严格的数学逻辑表示为: 两个对象相等 <=> equals()相等 => hashCode()相等。因此,重写equlas()方法必须重写hashCode()方法,以保证此逻辑严格成立,同时可以推理出:hasCode()不相等 => equals()不相等 <=> 两个对象不相等。 - - 可能有人在此产生疑问:既然比较两个对象是否相等的唯一条件(也是冲要条件)是equals,那么为什么还要弄出一个hashCode(),并且进行如此约定,弄得这么麻烦? - - 其实,这主要体现在hashCode()方法的作用上,其主要用于增强哈希表的性能。 - - 以集合类中,以Set为例,当新加一个对象时,需要判断现有集合中是否已经存在与此对象相等的对象,如果没有hashCode()方法,需要将Set进行一次遍历,并逐一用equals()方法判断两个对象是否相等,此种算法时间复杂度为o(n)。通过借助于hasCode方法,先计算出即将新加入对象的哈希码,然后根据哈希算法计算出此对象的位置,直接判断此位置上是否已有对象即可。(注:Set的底层用的是Map的原理实现) - -> 在此需要纠正一个理解上的误区:对象的hashCode()返回的不是对象所在的物理内存地址。甚至也不一定是对象的逻辑地址,hashCode()相同的两个对象,不一定相等,换言之,不相等的两个对象,hashCode()返回的哈希码可能相同。 -> -> 因此,在上述代码中,重写了equals()方法后,需要重写hashCode()方法。 - - public class equals和hashcode方法 { - @Override - //修改equals时必须同时修改hashcode方法,否则在作为key时会出问题 - public boolean equals(Object obj) { - return (this == obj); - } - - @Override - //相同的对象必须有相同hashcode,不同对象可能有相同hashcode - public int hashCode() { - return hashCode() >> 2; - } - } - - -### toString()方法 -```` -public String toString(); - - toString()方法返回该对象的字符串表示。先看一下Object中的具体方法体: - - public String toString() { - return getClass().getName() + "@" + Integer.toHexString(hashCode()); - } - -```` - -> toString()方法相信大家都经常用到,即使没有显式调用,但当我们使用System.out.println(obj)时,其内部也是通过toString()来实现的。 -> -> getClass()返回对象的类对象,getClassName()以String形式返回类对象的名称(含包名)。Integer.toHexString(hashCode())则是以对象的哈希码为实参,以16进制无符号整数形式返回此哈希码的字符串表示形式。 -> -> 如上例中的u1的哈希码是638,则对应的16进制为27e,调用toString()方法返回的结果为:com.corn.objectsummary.User@27e。 -> -> 因此:toString()是由对象的类型和其哈希码唯一确定,同一类型但不相等的两个对象分别调用toString()方法返回的结果可能相同。 - - -### wait() notify() notifAll() - -> -> 一说到wait(...) / notify() | notifyAll()几个方法,首先想到的是线程。确实,这几个方法主要用于java多线程之间的协作。先具体看下这几个方法的主要含义: -> -> wait():调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notify()/notifyAll()方法。 -> -> wait(long timeout)/wait(long timeout, int nanos):调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notisfy()/notisfyAll()方法,或超过指定的超时时间量。 -> -> notify()/notifyAll():唤醒在此对象监视器上等待的单个线程/所有线程。 -> -> wait(...) / notify() | notifyAll()一般情况下都是配套使用。下面来看一个简单的例子: - -这是一个生产者消费者的模型,只不过这里只用flag来标识哪个线程需要工作 -```` - public class wait和notify { - //volatile保证线程可见性 - volatile static int flag = 1; - //object作为锁对象,用于线程使用wait和notify方法 - volatile static Object o = new Object(); - public static void main(String[] args) { - new Thread(new Runnable() { - @Override - public void run() { - //wait和notify只能在同步代码块内使用 - synchronized (o) { - while (true) { - if (flag == 0) { - try { - Thread.sleep(2000); - System.out.println("thread1 wait"); - //释放锁,线程挂起进入object的等待队列,后续代码运行 - o.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - System.out.println("thread1 run"); - System.out.println("notify t2"); - flag = 0; - //通知等待队列的一个线程获取锁 - o.notify(); - } - } - } - }).start(); - //解释同上 - new Thread(new Runnable() { - @Override - public void run() { - while (true) { - synchronized (o) { - if (flag == 1) { - try { - Thread.sleep(2000); - System.out.println("thread2 wait"); - o.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - System.out.println("thread2 run"); - System.out.println("notify t1"); - flag = 1; - o.notify(); - } - } - } - }).start(); - } - - //输出结果是 - // thread1 run - // notify t2 - // thread1 wait - // thread2 run - // notify t1 - // thread2 wait - // thread1 run - // notify t2 - //不断循环 - } -```` - -> 从上述例子的输出结果中可以得出如下结论: -> -> 1、wait(...)方法调用后当前线程将立即阻塞,且适当其所持有的同步代码块中的锁,直到被唤醒或超时或打断后且重新获取到锁后才能继续执行; -> -> 2、notify()/notifyAll()方法调用后,其所在线程不会立即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,此时释放锁,因此,如果其同步代码块后还有代码,其执行则依赖于JVM的线程调度。 - -在Java源码中,可以看到wait()具体定义如下: - -```` -public final void wait() throws InterruptedException { - wait(0); -} -```` - -> 且wait(long timeout, int nanos)方法定义内部实质上也是通过调用wait(long timeout)完成。而wait(long timeout)是一个native方法。因此,wait(...)方法本质上都是native方式实现。 - -notify()/notifyAll()方法也都是native方法。 - -Java中线程具有较多的知识点,是一块比较大且重要的知识点。后期会有博文专门针对Java多线程作出详细总结。此处不再细述。 - -### finalize()方法 -finalize方法主要与Java垃圾回收机制有关。首先我们看一下finalized方法在Object中的具体定义: - -```` -protected void finalize() throws Throwable { } -```` - -> 我们发现Object类中finalize方法被定义成一个空方法,为什么要如此定义呢?finalize方法的调用时机是怎么样的呢? -> -> 首先,Object中定义finalize方法表明Java中每一个对象都将具有finalize这种行为,其具体调用时机在:JVM准备对此对形象所占用的内存空间进行垃圾回收前,将被调用。由此可以看出,此方法并不是由我们主动去调用的(虽然可以主动去调用,此时与其他自定义方法无异)。 - -## CLass类和Object类的关系 - -> Object类和Class类没有直接的关系。 -> -> Object类是一切java类的父类,对于普通的java类,即便不声明,也是默认继承了Object类。典型的,可以使用Object类中的toString()方法。 -> -> Class类是用于java反射机制的,一切java类,都有一个对应的Class对象,他是一个final类。Class 类的实例表示,正在运行的 Java 应用程序中的类和接口。 - -转一个知乎很有趣的问题 -https://www.zhihu.com/question/30301819 - - Java的对象模型中: - 1 所有的类都是Class类的实例,Object是类,那么Object也是Class类的一个实例。 - - 2 所有的类都最终继承自Object类,Class是类,那么Class也继承自Object。 - - 3 这就像是先有鸡还是先有蛋的问题,请问实际中JVM是怎么处理的? - - -> 这个问题中,第1个假设是错的:java.lang.Object是一个Java类,但并不是java.lang.Class的一个实例。后者只是一个用于描述Java类与接口的、用于支持反射操作的类型。这点上Java跟其它一些更纯粹的面向对象语言(例如Python和Ruby)不同。 -> -> 而第2个假设是对的:java.lang.Class是java.lang.Object的派生类,前者继承自后者。虽然第1个假设不对,但“鸡蛋问题”仍然存在:在一个已经启动完毕、可以使用的Java对象系统里,必须要有一个java.lang.Class实例对应java.lang.Object这个类;而java.lang.Class是java.lang.Object的派生类,按“一般思维”前者应该要在后者完成初始化之后才可以初始化… -> -> 事实是:这些相互依赖的核心类型完全可以在“混沌”中一口气都初始化好,然后对象系统的状态才叫做完成了“bootstrap”,后面就可以按照Java对象系统的一般规则去运行。JVM、JavaScript、Python、Ruby等的运行时都有这样的bootstrap过程。 -> -> 在“混沌”(boostrap过程)里,JVM可以为对象系统中最重要的一些核心类型先分配好内存空间,让它们进入[已分配空间]但[尚未完全初始化]状态。此时这些对象虽然已经分配了空间,但因为状态还不完整所以尚不可使用。 -> -> 然后,通过这些分配好的空间把这些核心类型之间的引用关系串好。到此为止所有动作都由JVM完成,尚未执行任何Java字节码。然后这些核心类型就进入了[完全初始化]状态,对象系统就可以开始自我运行下去,也就是可以开始执行Java字节码来进一步完成Java系统的初始化了。 - -## 参考文章 - -https://www.cnblogs.com/congsg2016/p/5317362.html -https://www.jb51.net/article/125936.htm -https://blog.csdn.net/dufufd/article/details/80537638 -https://blog.csdn.net/farsight1/article/details/80664104 -https://blog.csdn.net/xiaomingdetianxia/article/details/77429180 - -### Java技术江湖 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源,关注公众号后,后台回复关键字 **“Java”** 即可免费无套路获取。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/1.png) - diff --git "a/docs/Java/basic/Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/Java/basic/Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" deleted file mode 100644 index b871b14..0000000 --- "a/docs/Java/basic/Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" +++ /dev/null @@ -1,546 +0,0 @@ - -# 目录 - -* [Java 基本数据类型](#java-基本数据类型) - * [Java 的两大数据类型:](#java-的两大数据类型) - * [内置数据类型](#内置数据类型) - * [引用类型](#引用类型) - * [Java 常量](#java-常量) - * [自动拆箱和装箱(详解)](#自动拆箱和装箱(详解)) - * [实现](#实现) - * [自动装箱与拆箱中的“坑”](#自动装箱与拆箱中的坑) - * [了解基本类型缓存(常量池)的最佳实践](#了解基本类型缓存(常量池)的最佳实践) - * [总结:](#总结:) - * [基本数据类型的存储方式](#基本数据类型的存储方式) - * [存在栈中](#存在栈中) - * [存在堆里](#存在堆里) - * [参考文章](#参考文章) - - - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -# Java 基本数据类型 - -变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间。 - -内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据。 - -![](https://www.runoob.com/wp-content/uploads/2013/12/memorypic1.jpg) - -因此,通过定义不同类型的变量,可以在内存中储存整数、小数或者字符。 - -## Java 的两大数据类型: - -- 内置数据类型 -- 引用数据类型 - -### 内置数据类型 - -Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 - -**byte:** - -- byte 数据类型是8位、有符号的,以二进制补码表示的整数; -- 最小值是 -128(-2^7); -- 最大值是 127(2^7-1); -- 默认值是 0; -- byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一; -- 例子:byte a = 100,byte b = -50。 - -**short:** - -- short 数据类型是 16 位、有符号的以二进制补码表示的整数 -- 最小值是 -32768(-2^15); -- 最大值是 32767(2^15 - 1); -- Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一; -- 默认值是 0; -- 例子:short s = 1000,short r = -20000。 - -**int:** - -- int 数据类型是32位、有符号的以二进制补码表示的整数; -- 最小值是 -2,147,483,648(-2^31); -- 最大值是 2,147,483,647(2^31 - 1); -- 一般地整型变量默认为 int 类型; -- 默认值是 0 ; -- 例子:int a = 100000, int b = -200000。 - -**long:** - -- long 数据类型是 64 位、有符号的以二进制补码表示的整数; -- 最小值是 -9,223,372,036,854,775,808(-2^63); -- 最大值是 9,223,372,036,854,775,807(2^63 -1); -- 这种类型主要使用在需要比较大整数的系统上; -- 默认值是 0L; -- 例子: long a = 100000L,Long b = -200000L。 - "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。 - -**float:** - -- float 数据类型是单精度、32位、符合IEEE 754标准的浮点数; -- float 在储存大型浮点数组的时候可节省内存空间; -- 默认值是 0.0f; -- 浮点数不能用来表示精确的值,如货币; -- 例子:float f1 = 234.5f。 - -**double:** - -- double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数; -- 浮点数的默认类型为double类型; -- double类型同样不能表示精确的值,如货币; -- 默认值是 0.0d; -- 例子:double d1 = 123.4。 - -**boolean:** - -- boolean数据类型表示一位的信息; -- 只有两个取值:true 和 false; -- 这种类型只作为一种标志来记录 true/false 情况; -- 默认值是 false; -- 例子:boolean one = true。 - -**char:** - -- char类型是一个单一的 16 位 Unicode 字符; -- 最小值是 \u0000(即为0); -- 最大值是 \uffff(即为65,535); -- char 数据类型可以储存任何字符; -- 例子:char letter = 'A';。 - -### 引用类型 - -- 在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不能被改变了。 -- 对象、数组都是引用数据类型。 -- 所有引用类型的默认值都是null。 -- 一个引用变量可以用来引用任何与之兼容的类型。 -- 例子:Site site = new Site("Runoob")。 - -### Java 常量 - -常量在程序运行时是不能被修改的。 - -在 Java 中使用 final 关键字来修饰常量,声明方式和变量类似: - -``` -final double PI = 3.1415927; -``` - -虽然常量名也可以用小写,但为了便于识别,通常使用大写字母表示常量。 - -字面量可以赋给任何内置类型的变量。例如: - -``` -byte a = 68; -char a = 'A' -``` - -## 自动拆箱和装箱(详解) - -Java 5增加了自动装箱与自动拆箱机制,方便基本类型与包装类型的相互转换操作。在Java 5之前,如果要将一个int型的值转换成对应的包装器类型Integer,必须显式的使用new创建一个新的Integer对象,或者调用静态方法Integer.valueOf()。 -```` -//在Java 5之前,只能这样做 -Integer value = new Integer(10); -//或者这样做 -Integer value = Integer.valueOf(10); -//直接赋值是错误的 -//Integer value = 10; -```` -在Java 5中,可以直接将整型赋给Integer对象,由编译器来完成从int型到Integer类型的转换,这就叫自动装箱。 - -```` -//在Java 5中,直接赋值是合法的,由编译器来完成转换 -Integer value = 10; -与此对应的,自动拆箱就是可以将包装类型转换为基本类型,具体的转换工作由编译器来完成。 -//在Java 5 中可以直接这么做 -Integer value = new Integer(10); -int i = value; -```` - -自动装箱与自动拆箱为程序员提供了很大的方便,而在实际的应用中,自动装箱与拆箱也是使用最广泛的特性之一。自动装箱和自动拆箱其实是Java编译器提供的一颗语法糖(语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通过可提高开发效率,增加代码可读性,增加代码的安全性)。 - -### 实现 - -在八种包装类型中,每一种包装类型都提供了两个方法: - -静态方法valueOf(基本类型):将给定的基本类型转换成对应的包装类型; - -实例方法xxxValue():将具体的包装类型对象转换成基本类型; -下面我们以int和Integer为例,说明Java中自动装箱与自动拆箱的实现机制。看如下代码: -```` - class Auto //code1 - { - public static void main(String[] args) - { - //自动装箱 - Integer inte = 10; - //自动拆箱 - int i = inte; - - //再double和Double来验证一下 - Double doub = 12.40; - double d = doub; - - } - - } -```` -上面的代码先将int型转为Integer对象,再讲Integer对象转换为int型,毫无疑问,这是可以正确运行的。可是,这种转换是怎么进行的呢?使用反编译工具,将生成的Class文件在反编译为Java文件,让我们看看发生了什么: - -```` - class Auto//code2 - { - public static void main(String[] paramArrayOfString) - { - Integer localInteger = Integer.valueOf(10); - - int i = localInteger.intValue(); - - Double localDouble = Double.valueOf(12.4D); - double d = localDouble.doubleValue(); - - } - } -```` - -我们可以看到经过javac编译之后,code1的代码被转换成了code2,实际运行时,虚拟机运行的就是code2的代码。也就是说,虚拟机根本不知道有自动拆箱和自动装箱这回事;在将Java源文件编译为class文件的过程中,javac编译器在自动装箱的时候,调用了Integer.valueOf()方法,在自动拆箱时,又调用了intValue()方法。我们可以看到,double和Double也是如此。 -实现总结:其实自动装箱和自动封箱是编译器为我们提供的一颗语法糖。在自动装箱时,编译器调用包装类型的valueOf()方法;在自动拆箱时,编译器调用了相应的xxxValue()方法。 - -### 自动装箱与拆箱中的“坑” - -在使用自动装箱与自动拆箱时,要注意一些陷阱,为了避免这些陷阱,我们有必要去看一下各种包装类型的源码。 - -Integer源码 - -```` -public final class Integer extends Number implements Comparable { - private final int value; - - -/*Integer的构造方法,接受一个整型参数,Integer对象表示的int值,保存在value中*/ - public Integer(int value) { - this.value = value; - } - -/*equals()方法判断的是:所代表的int型的值是否相等*/ - public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); - } - return false; -} - -/*返回这个Integer对象代表的int值,也就是保存在value中的值*/ - public int intValue() { - return value; - } - - /** - * 首先会判断i是否在[IntegerCache.low,Integer.high]之间 - * 如果是,直接返回Integer.cache中相应的元素 - * 否则,调用构造方法,创建一个新的Integer对象 - */ - public static Integer valueOf(int i) { - assert IntegerCache.high >= 127; - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); - } - -/** - * 静态内部类,缓存了从[low,high]对应的Integer对象 - * low -128这个值不会被改变 - * high 默认是127,可以改变,最大不超过:Integer.MAX_VALUE - (-low) -1 - * cache 保存从[low,high]对象的Integer对象 - */ - private static class IntegerCache { - static final int low = -128; - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - } - - private IntegerCache() {} -} -```` - - -以上是Oracle(Sun)公司JDK 1.7中Integer源码的一部分,通过分析上面的代码,得到: - -1)Integer有一个实例域value,它保存了这个Integer所代表的int型的值,且它是final的,也就是说这个Integer对象一经构造完成,它所代表的值就不能再被改变。 - -2)Integer重写了equals()方法,它通过比较两个Integer对象的value,来判断是否相等。 - -3)重点是静态内部类IntegerCache,通过类名就可以发现:它是用来缓存数据的。它有一个数组,里面保存的是连续的Integer对象。 - (a) low:代表缓存数据中最小的值,固定是-128。 - - (b) high:代表缓存数据中最大的值,它可以被该改变,默认是127。high最小是127,最大是Integer.MAX_VALUE-(-low)-1,如果high超过了这个值,那么cache[ ]的长度就超过Integer.MAX_VALUE了,也就溢出了。 - - (c) cache[]:里面保存着从[low,high]所对应的Integer对象,长度是high-low+1(因为有元素0,所以要加1)。 - -4)调用valueOf(inti)方法时,首先判断i是否在[low,high]之间,如果是,则复用Integer.cache[i-low]。比如,如果Integer.valueOf(3),直接返回Integer.cache[131];如果i不在这个范围,则调用构造方法,构造出一个新的Integer对象。 - -5)调用intValue(),直接返回value的值。 -通过3)和4)可以发现,默认情况下,在使用自动装箱时,VM会复用[-128,127]之间的Integer对象。 -```` -Integer a1 = 1; -Integer a2 = 1; -Integer a3 = new Integer(1); -//会打印true,因为a1和a2是同一个对象,都是Integer.cache[129] -System.out.println(a1 == a2); -//false,a3构造了一个新的对象,不同于a1,a2 -System.out.println(a1 == a3); -```` -### 了解基本类型缓存(常量池)的最佳实践 - -``` -//基本数据类型的常量池是-128到127之间。 -// 在这个范围中的基本数据类的包装类可以自动拆箱,比较时直接比较数值大小。 -public static void main(String[] args) { - - //int的自动拆箱和装箱只在-128到127范围中进行,超过该范围的两个integer的 == 判断是会返回false的。 - Integer a1 = 128; - Integer a2 = -128; - Integer a3 = -128; - Integer a4 = 128; - System.out.println(a1 == a4); - System.out.println(a2 == a3); - - Byte b1 = 127; - Byte b2 = 127; - Byte b3 = -128; - Byte b4 = -128; - //byte都是相等的,因为范围就在-128到127之间 - System.out.println(b1 == b2); - System.out.println(b3 == b4); - - Long c1 = 128L; - Long c2 = 128L; - Long c3 = -128L; - Long c4 = -128L; - System.out.println(c1 == c2); - System.out.println(c3 == c4); - - //char没有负值 - //发现char也是在0到127之间自动拆箱 - Character d1 = 128; - Character d2 = 128; - Character d3 = 127; - Character d4 = 127; - System.out.println(d1 == d2); - System.out.println(d3 == d4); - - `结果` - - `false` - `true` - `true` - `true` - `false` - `true` - `false` - `true` - - Integer i = 10; - Byte b = 10; - //比较Byte和Integer.两个对象无法直接比较,报错 - //System.out.println(i == b); - System.out.println("i == b " + i.equals(b)); - //答案是false,因为包装类的比较时先比较是否是同一个类,不是的话直接返回false. - int ii = 128; - short ss = 128; - long ll = 128; - char cc = 128; - System.out.println("ii == bb " + (ii == ss)); - System.out.println("ii == ll " + (ii == ll)); - System.out.println("ii == cc " + (ii == cc)); - - 结果 - i == b false - ii == bb true - ii == ll true - ii == cc true - - //这时候都是true,因为基本数据类型直接比较值,值一样就可以。 -``` - -### 总结: - -通过上面的代码,我们分析一下自动装箱与拆箱发生的时机: - -(1)当需要一个对象的时候会自动装箱,比如Integer a = 10;equals(Object o)方法的参数是Object对象,所以需要装箱。 - -(2)当需要一个基本类型时会自动拆箱,比如int a = new Integer(10);算术运算是在基本类型间进行的,所以当遇到算术运算时会自动拆箱,比如代码中的 c == (a + b); - -(3) 包装类型 == 基本类型时,包装类型自动拆箱; - -需要注意的是:“==”在没遇到算术运算时,不会自动拆箱;基本类型只会自动装箱为对应的包装类型,代码中最后一条说明的内容。 - -在JDK 1.5中提供了自动装箱与自动拆箱,这其实是Java 编译器的语法糖,编译器通过调用包装类型的valueOf()方法实现自动装箱,调用xxxValue()方法自动拆箱。自动装箱和拆箱会有一些陷阱,那就是包装类型复用了某些对象。 - -(1)Integer默认复用了[-128,127]这些对象,其中高位置可以修改; - -(2)Byte复用了全部256个对象[-128,127]; - -(3)Short复用了[-128,127]这些对象; - -(4)Long复用了[-128,127]; - -(5)Character复用了[0,127],Charater不能表示负数; - -Double和Float是连续不可数的,所以没法复用对象,也就不存在自动装箱复用陷阱。 - -Boolean没有自动装箱与拆箱,它也复用了Boolean.TRUE和Boolean.FALSE,通过Boolean.valueOf(boolean b)返回的Blooean对象要么是TRUE,要么是FALSE,这点也要注意。 - -本文介绍了“真实的”自动装箱与拆箱,为了避免写出错误的代码,又从包装类型的源码入手,指出了各种包装类型在自动装箱和拆箱时存在的陷阱,同时指出了自动装箱与拆箱发生的时机。 - -## 基本数据类型的存储方式 - -上面自动拆箱和装箱的原理其实与常量池有关。 - -### 存在栈中 - -public void(int a) -{ -int i = 1; -int j = 1; -} -方法中的i 存在虚拟机栈的局部变量表里,i是一个引用,j也是一个引用,它们都指向局部变量表里的整型值 1. -int a是传值引用,所以a也会存在局部变量表。 - -### 存在堆里 - -class A{ -int i = 1; -A a = new A(); -} -i是类的成员变量。类实例化的对象存在堆中,所以成员变量也存在堆中,引用a存的是对象的地址,引用i存的是值,这个值1也会存在堆中。可以理解为引用i指向了这个值1。也可以理解为i就是1. - -3 包装类对象怎么存 -其实我们说的常量池也可以叫对象池。 -比如String a= new String("a").intern()时会先在常量池找是否有“a"对象如果有的话直接返回“a"对象在常量池的地址,即让引用a指向常量”a"对象的内存地址。 -public native String intern(); -Integer也是同理。 - -下图是Integer类型在常量池中查找同值对象的方法。 - -``` -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -private static class IntegerCache { - static final int low = -128; - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - try { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } catch( NumberFormatException nfe) { - // If the property cannot be parsed into an int, ignore it. - } - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - - // range [-128, 127] must be interned (JLS7 5.1.7) - assert IntegerCache.high >= 127; - } - - private IntegerCache() {} -} -``` - -所以基本数据类型的包装类型可以在常量池查找对应值的对象,找不到就会自动在常量池创建该值的对象。 - -而String类型可以通过intern来完成这个操作。 - -JDK1.7后,常量池被放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子: - -``` -[java] view plain copy -String s = new String("1"); -s.intern(); -String s2 = "1"; -System.out.println(s == s2); - -String s3 = new String("1") + new String("1"); -s3.intern(); -String s4 = "11"; -System.out.println(s3 == s4); -输出结果为: - -[java] view plain copy -JDK1.6以及以下:false false -JDK1.7以及以上:false true -``` -JDK1.6查找到常量池存在相同值的对象时会直接返回该对象的地址。 - -JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。 - -那么其他字符串在常量池找值时就会返回另一个堆中对象的地址。 - -下一节详细介绍String以及相关包装类。 - -具体请见:https://blog.csdn.net/a724888/article/details/80042298 - -关于Java面向对象三大特性,请参考: - -https://blog.csdn.net/a724888/article/details/80033043 - -## 参考文章 - - - - - - - - - - diff --git "a/docs/Java/basic/Java\345\274\202\345\270\270.md" "b/docs/Java/basic/Java\345\274\202\345\270\270.md" deleted file mode 100644 index 4f77437..0000000 --- "a/docs/Java/basic/Java\345\274\202\345\270\270.md" +++ /dev/null @@ -1,842 +0,0 @@ -# 目录 - - * [为什么要使用异常](#为什么要使用异常) - * [异常基本定义](#异常基本定义) - * [异常体系](#异常体系) - * [初识异常](#初识异常) - * [异常和错误](#异常和错误) - * [异常的处理方式](#异常的处理方式) - * ["不负责任"的throws](#不负责任的throws) - * [纠结的finally](#纠结的finally) - * [throw : JRE也使用的关键字](#throw--jre也使用的关键字) - * [异常调用链](#异常调用链) - * [自定义异常](#自定义异常) - * [异常的注意事项](#异常的注意事项) - * [当finally遇上return](#当finally遇上return) - * [JAVA异常常见面试题](#java异常常见面试题) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - -## 为什么要使用异常 - -> 首先我们可以明确一点就是异常的处理机制可以确保我们程序的健壮性,提高系统可用率。虽然我们不是特别喜欢看到它,但是我们不能不承认它的地位,作用。 - -在没有异常机制的时候我们是这样处理的:通过函数的返回值来判断是否发生了异常(这个返回值通常是已经约定好了的),调用该函数的程序负责检查并且分析返回值。虽然可以解决异常问题,但是这样做存在几个缺陷: -> -> 1、 容易混淆。如果约定返回值为-11111时表示出现异常,那么当程序最后的计算结果真的为-1111呢? -> -> 2、 代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。 -> -> 3、 由调用函数来分析异常,这要求程序员对库函数有很深的了解。 -> - -在OO中提供的异常处理机制是提供代码健壮的强有力的方式。使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。 - -而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。 - -这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。(摘自《Think in java 》)。 - -该部分内容选自http://www.cnblogs.com/chenssy/p/3438130.html - -## 异常基本定义 - -> 在《Think in java》中是这样定义异常的:异常情形是指阻止当前方法或者作用域继续执行的问题。在这里一定要明确一点:异常代码某种程度的错误,尽管Java有异常处理机制,但是我们不能以“正常”的眼光来看待异常,异常处理机制的原因就是告诉你:这里可能会或者已经产生了错误,您的程序出现了不正常的情况,可能会导致程序失败! - -> 那么什么时候才会出现异常呢?只有在你当前的环境下程序无法正常运行下去,也就是说程序已经无法来正确解决问题了,这时它所就会从当前环境中跳出,并抛出异常。抛出异常后,它首先会做几件事。 - -> 首先,它会使用new创建一个异常对象,然后在产生异常的位置终止程序,并且从当前环境中弹出对异常对象的引用,这时。异常处理机制就会接管程序,并开始寻找一个恰当的地方来继续执行程序,这个恰当的地方就是异常处理程序。 - -> 总的来说异常处理机制就是当程序发生异常时,它强制终止程序运行,记录异常信息并将这些信息反馈给我们,由我们来确定是否处理异常。 - -## 异常体系 - -![img](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/22185952-834d92bc2bfe498f9a33414cc7a2c8a4.png) - -从上面这幅图可以看出,Throwable是java语言中所有错误和异常的超类(万物即可抛)。它有两个子类:Error、Exception。 - -Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。 - -Throwable又派生出Error类和Exception类。 - -错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。 - -异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。 - -总体上我们根据Javac对异常的处理要求,将异常类分为2类。 - -> 非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。 - -> 对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。 - -> 检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。 - -> 这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。 - -需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。 - -这部分内容摘自http://www.importnew.com/26613.html - -## 初识异常 - -异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。 - -异常最先发生的地方,叫做异常抛出点。 -```` - public class 异常 { - public static void main (String [] args ) - { - System . out. println( "----欢迎使用命令行除法计算器----" ) ; - CMDCalculate (); - } - public static void CMDCalculate () - { - Scanner scan = new Scanner ( System. in ); - int num1 = scan .nextInt () ; - int num2 = scan .nextInt () ; - int result = devide (num1 , num2 ) ; - System . out. println( "result:" + result) ; - scan .close () ; - } - public static int devide (int num1, int num2 ){ - return num1 / num2 ; - } - - // ----欢迎使用命令行除法计算器---- - // 1 - // 0 - // Exception in thread "main" java.lang.ArithmeticException: / by zero - // at com.javase.异常.异常.devide(异常.java:24) - // at com.javase.异常.异常.CMDCalculate(异常.java:19) - // at com.javase.异常.异常.main(异常.java:12) - - // ----欢迎使用命令行除法计算器---- - // r - // Exception in thread "main" java.util.InputMismatchException - // at java.util.Scanner.throwFor(Scanner.java:864) - // at java.util.Scanner.next(Scanner.java:1485) - // at java.util.Scanner.nextInt(Scanner.java:2117) - // at java.util.Scanner.nextInt(Scanner.java:2076) - // at com.javase.异常.异常.CMDCalculate(异常.java:17) - // at com.javase.异常.异常.main(异常.java:12) - -```` - -从上面的例子可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。 - -这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。 - -> 上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。 - -代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。但是为什么只throws了IOException呢?因为FileNotFoundException是IOException的子类,在处理范围内。 - -## 异常和错误 - -下面看一个例子 -```` - //错误即error一般指jvm无法处理的错误 - //异常是Java定义的用于简化错误处理流程和定位错误的一种工具。 - public class 错误和错误 { - Error error = new Error(); - - public static void main(String[] args) { - throw new Error(); - } - - //下面这四个异常或者错误有着不同的处理方法 - public void error1 (){ - //编译期要求必须处理,因为这个异常是最顶层异常,包括了检查异常,必须要处理 - try { - throw new Throwable(); - } catch (Throwable throwable) { - throwable.printStackTrace(); - } - } - //Exception也必须处理。否则报错,因为检查异常都继承自exception,所以默认需要捕捉。 - public void error2 (){ - try { - throw new Exception(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - //error可以不处理,编译不报错,原因是虚拟机根本无法处理,所以啥都不用做 - public void error3 (){ - throw new Error(); - } - - //runtimeexception众所周知编译不会报错 - public void error4 (){ - throw new RuntimeException(); - } - // Exception in thread "main" java.lang.Error - // at com.javase.异常.错误.main(错误.java:11) - - } -```` -## 异常的处理方式 - -在编写代码处理异常时,对于检查异常,有2种不同的处理方式: - -> 使用try…catch…finally语句块处理它。 -> - -> 或者,在函数签名中使用throws 声明交给函数调用者caller去解决。 -> - -下面看几个具体的例子,包括error,exception和throwable - -上面的例子是运行时异常,不需要显示捕获。 -下面这个例子是可检查异常需,要显示捕获或者抛出。 -```` - @Test - public void testException() throws IOException - { - //FileInputStream的构造函数会抛出FileNotFoundException - FileInputStream fileIn = new FileInputStream("E:\\a.txt"); - - int word; - //read方法会抛出IOException - while((word = fileIn.read())!=-1) - { - System.out.print((char)word); - } - //close方法会抛出IOException - fileIn.close(); - } - -一般情况下的处理方式 try catch finally - - public class 异常处理方式 { - - @Test - public void main() { - try{ - //try块中放可能发生异常的代码。 - InputStream inputStream = new FileInputStream("a.txt"); - - //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。 - int i = 1/0; - //如果发生异常,则尝试去匹配catch块。 - throw new SQLException(); - //使用1.8jdk同时捕获多个异常,runtimeexception也可以捕获。只是捕获后虚拟机也无法处理,所以不建议捕获。 - }catch(SQLException | IOException | ArrayIndexOutOfBoundsException exception){ - System.out.println(exception.getMessage()); - //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。 - - //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。 - - //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。 - - //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。 - - //如果try中没有发生异常,则所有的catch块将被忽略。 - - }catch(Exception exception){ - System.out.println(exception.getMessage()); - //... - }finally{ - //finally块通常是可选的。 - //无论异常是否发生,异常是否匹配被处理,finally都会执行。 - - //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 - } - -一个try至少要跟一个catch或者finally - - try { - int i = 1; - }finally { - //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。 - } - } - - -异常出现时该方法后面的代码不会运行,即使异常已经被捕获。这里举出一个奇特的例子,在catch里再次使用try catch finally - - @Test - public void test() { - try { - throwE(); - System.out.println("我前面抛出异常了"); - System.out.println("我不会执行了"); - } catch (StringIndexOutOfBoundsException e) { - System.out.println(e.getCause()); - }catch (Exception ex) { - //在catch块中仍然可以使用try catch finally - try { - throw new Exception(); - }catch (Exception ee) { - - }finally { - System.out.println("我所在的catch块没有执行,我也不会执行的"); - } - } - } - //在方法声明中抛出的异常必须由调用方法处理或者继续往上抛, - // 当抛到jre时由于无法处理终止程序 - public void throwE (){ - // Socket socket = new Socket("127.0.0.1", 80); - - //手动抛出异常时,不会报错,但是调用该方法的方法需要处理这个异常,否则会出错。 - // java.lang.StringIndexOutOfBoundsException - // at com.javase.异常.异常处理方式.throwE(异常处理方式.java:75) - // at com.javase.异常.异常处理方式.test(异常处理方式.java:62) - throw new StringIndexOutOfBoundsException(); - } -```` - -其实有的语言在遇到异常后仍然可以继续运行 - -> 有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 ) -> -> 而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式) - -## "不负责任"的throws - -throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。 - -采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。 -```` -public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN -{ - //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。 -} -```` - -## 纠结的finally - -finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。 - -良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。 - -需要注意的地方: - -1、finally块没有处理异常的能力。处理异常的只能是catch块。 - -2、在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。 - -3、在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。 - -```` - public class finally使用 { - public static void main(String[] args) { - try { - throw new IllegalAccessException(); - }catch (IllegalAccessException e) { - // throw new Throwable(); - //此时如果再抛异常,finally无法执行,只能报错。 - //finally无论何时都会执行 - //除非我显示调用。此时finally才不会执行 - System.exit(0); - - }finally { - System.out.println("算你狠"); - } - } - } -```` - -## throw : JRE也使用的关键字 - -throw exceptionObject - -程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。 - -throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,==它和由JRE自动形成的异常抛出点没有任何差别。== -```` -public void save(User user) -{ - if(user == null) - throw new IllegalArgumentException("User对象为空"); - //...... -} -```` -后面开始的大部分内容都摘自http://www.cnblogs.com/lulipro/p/7504267.html - -该文章写的十分细致到位,令人钦佩,是我目前为之看到关于异常最详尽的文章,可以说是站在巨人的肩膀上了。 - -## 异常调用链 - -异常的链化 - -在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常。 - -==但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。== - -> 异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。 - -查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。 -```` -public class Throwable implements Serializable { - private Throwable cause = this; - public Throwable(String message, Throwable cause) { - fillInStackTrace(); - detailMessage = message; - this.cause = cause; - } - public Throwable(Throwable cause) { - fillInStackTrace(); - detailMessage = (cause==null ? null : cause.toString()); - this.cause = cause; - } - //........ -} -```` - -下面看一个比较实在的异常链例子哈 -```` -public class 异常链 { - @Test - public void test() { - C(); - } - public void A () throws Exception { - try { - int i = 1; - i = i / 0; - //当我注释掉这行代码并使用B方法抛出一个error时,运行结果如下 -// 四月 27, 2018 10:12:30 下午 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines -// 信息: Discovered TestEngines with IDs: [junit-jupiter] -// java.lang.Error: B也犯了个错误 -// at com.javase.异常.异常链.B(异常链.java:33) -// at com.javase.异常.异常链.C(异常链.java:38) -// at com.javase.异常.异常链.test(异常链.java:13) -// at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) -// Caused by: java.lang.Error -// at com.javase.异常.异常链.B(异常链.java:29) - - }catch (ArithmeticException e) { - //这里通过throwable类的构造方法将最底层的异常重新包装并抛出,此时注入了A方法的信息。最后打印栈信息时可以看到caused by - A方法的异常。 - //如果直接抛出,栈信息打印结果只能看到上层方法的错误信息,不能看到其实是A发生了错误。 - //所以需要包装并抛出 - throw new Exception("A方法计算错误", e); - } - - } - public void B () throws Exception,Error { - try { - //接收到A的异常, - A(); - throw new Error(); - }catch (Exception e) { - throw e; - }catch (Error error) { - throw new Error("B也犯了个错误", error); - } - } - public void C () { - try { - B(); - }catch (Exception | Error e) { - e.printStackTrace(); - } - - } - - //最后结果 -// java.lang.Exception: A方法计算错误 -// at com.javase.异常.异常链.A(异常链.java:18) -// at com.javase.异常.异常链.B(异常链.java:24) -// at com.javase.异常.异常链.C(异常链.java:31) -// at com.javase.异常.异常链.test(异常链.java:11) -// 省略 -// Caused by: java.lang.ArithmeticException: / by zero -// at com.javase.异常.异常链.A(异常链.java:16) -// ... 31 more -} -```` -## 自定义异常 - -如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。 - -按照国际惯例,自定义的异常应该总是包含如下的构造函数: - -一个无参构造函数 -一个带有String参数的构造函数,并传递给父类的构造函数。 -一个带有String参数和Throwable参数,并都传递给父类构造函数 -一个带有Throwable 参数的构造函数,并传递给父类的构造函数。 -下面是IOException类的完整源代码,可以借鉴。 -```` - public class IOException extends Exception - { - static final long serialVersionUID = 7818375828146090155L; - - public IOException() - { - super(); - } - - public IOException(String message) - { - super(message); - } - - public IOException(String message, Throwable cause) - { - super(message, cause); - } - - public IOException(Throwable cause) - { - super(cause); - } - } -```` -## 异常的注意事项 - -异常的注意事项 - -> 当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。 -> -> 例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。 - -至于为什么?我想,也许下面的例子可以说明。 -```` -class Father -{ - public void start() throws IOException - { - throw new IOException(); - } -} - -class Son extends Father -{ - public void start() throws Exception - { - throw new SQLException(); - } -} -/**********************假设上面的代码是允许的(实质是错误的)***********************/ - -class Test -{ - public static void main(String[] args) - { - Father[] objs = new Father[2]; - objs[0] = new Father(); - objs[1] = new Son(); - - for(Father obj:objs) - { - //因为Son类抛出的实质是SQLException,而IOException无法处理它。 - //那么这里的try。。catch就不能处理Son中的异常。 - //多态就不能实现了。 - try { - obj.start(); - }catch(IOException) - { - //处理IOException - } - } - } -} -```` -==Java的异常执行流程是线程独立的,线程之间没有影响== - -> Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。 -> -> 也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。 - -下面看一个例子 -```` -public class 多线程的异常 { - @Test - public void test() { - go(); - } - public void go () { - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0;i <= 2;i ++) { - int finalI = i; - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - executorService.execute(new Runnable() { - @Override - //每个线程抛出异常时并不会影响其他线程的继续执行 - public void run() { - try { - System.out.println("start thread" + finalI); - throw new Exception(); - }catch (Exception e) { - System.out.println("thread" + finalI + " go wrong"); - } - } - }); - } -// 结果: -// start thread0 -// thread0 go wrong -// start thread1 -// thread1 go wrong -// start thread2 -// thread2 go wrong - } -} -```` - -## 当finally遇上return - - -首先一个不容易理解的事实: - -在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。 -```` -public static void main(String[] args) -{ - int re = bar(); - System.out.println(re); -} -private static int bar() -{ - try{ - return 5; - } finally{ - System.out.println("finally"); - } -} -/*输出: -finally -*/ -```` - -也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。 - -finally中的return 会覆盖 try 或者catch中的返回值。 -```` -public static void main(String[] args) - { - int result; - - result = foo(); - System.out.println(result); /////////2 - - result = bar(); - System.out.println(result); /////////2 - } - - @SuppressWarnings("finally") - public static int foo() - { - trz{ - int a = 5 / 0; - } catch (Exception e){ - return 1; - } finally{ - return 2; - } - - } - - @SuppressWarnings("finally") - public static int bar() - { - try { - return 1; - }finally { - return 2; - } - } -```` -finally中的return会抑制(消灭)前面try或者catch块中的异常 -```` -class TestException -{ - public static void main(String[] args) - { - int result; - try{ - result = foo(); - System.out.println(result); //输出100 - } catch (Exception e){ - System.out.println(e.getMessage()); //没有捕获到异常 - } - - try{ - result = bar(); - System.out.println(result); //输出100 - } catch (Exception e){ - System.out.println(e.getMessage()); //没有捕获到异常 - } - } - - //catch中的异常被抑制 - @SuppressWarnings("finally") - public static int foo() throws Exception - { - try { - int a = 5/0; - return 1; - }catch(ArithmeticException amExp) { - throw new Exception("我将被忽略,因为下面的finally中使用了return"); - }finally { - return 100; - } - } - - //try中的异常被抑制 - @SuppressWarnings("finally") - public static int bar() throws Exception - { - try { - int a = 5/0; - return 1; - }finally { - return 100; - } - } -} -```` -finally中的异常会覆盖(消灭)前面try或者catch中的异常 -```` -class TestException -{ - public static void main(String[] args) - { - int result; - try{ - result = foo(); - } catch (Exception e){ - System.out.println(e.getMessage()); //输出:我是finaly中的Exception - } - - try{ - result = bar(); - } catch (Exception e){ - System.out.println(e.getMessage()); //输出:我是finaly中的Exception - } - } - - //catch中的异常被抑制 - @SuppressWarnings("finally") - public static int foo() throws Exception - { - try { - int a = 5/0; - return 1; - }catch(ArithmeticException amExp) { - throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常"); - }finally { - throw new Exception("我是finaly中的Exception"); - } - } - - //try中的异常被抑制 - @SuppressWarnings("finally") - public static int bar() throws Exception - { - try { - int a = 5/0; - return 1; - }finally { - throw new Exception("我是finaly中的Exception"); - } - - } -} -```` -上面的3个例子都异于常人的编码思维,因此我建议: - -> 不要在fianlly中使用return。 - -> 不要在finally中抛出异常。 - -> 减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。 - -> 将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。 - -## JAVA异常常见面试题 - -  下面是我个人总结的在Java和J2EE开发者在面试中经常被问到的有关Exception和Error的知识。在分享我的回答的时候,我也给这些问题作了快速修订,并且提供源码以便深入理解。我总结了各种难度的问题,适合新手码农和高级Java码农。如果你遇到了我列表中没有的问题,并且这个问题非常好,请在下面评论中分享出来。你也可以在评论中分享你面试时答错的情况。 - -**1) Java中什么是Exception?** -这个问题经常在第一次问有关异常的时候或者是面试菜鸟的时候问。我从来没见过面高级或者资深工程师的时候有人问这玩意,但是对于菜鸟,是很愿意问这个的。简单来说,异常是Java传达给你的系统和程序错误的方式。在java中,异常功能是通过实现比如Throwable,Exception,RuntimeException之类的类,然后还有一些处理异常时候的关键字,比如throw,throws,try,catch,finally之类的。所有的异常都是通过Throwable衍生出来的。Throwable把错误进一步划分为java.lang.Exception -和 java.lang.Error. - -java.lang.Error 用来处理系统错误,例如java.lang.StackOverFlowError 之类的。然后Exception用来处理程序错误,请求的资源不可用等等。 - -**2) Java中的检查型异常和非检查型异常有什么区别?** - -这又是一个非常流行的Java异常面试题,会出现在各种层次的Java面试中。检查型异常和非检查型异常的主要区别在于其处理方式。检查型异常需要使用try, catch和finally关键字在编译期进行处理,否则会出现编译器会报错。对于非检查型异常则不需要这样做。Java中所有继承自java.lang.Exception类的异常都是检查型异常,所有继承自RuntimeException的异常都被称为非检查型异常。 - -**3) Java中的NullPointerException和ArrayIndexOutOfBoundException之间有什么相同之处?** - -在Java异常面试中这并不是一个很流行的问题,但会出现在不同层次的初学者面试中,用来测试应聘者对检查型异常和非检查型异常的概念是否熟悉。顺便说一下,该题的答案是,这两个异常都是非检查型异常,都继承自RuntimeException。该问题可能会引出另一个问题,即Java和C的数组有什么不同之处,因为C里面的数组是没有大小限制的,绝对不会抛出ArrayIndexOutOfBoundException。 - -**4)在Java异常处理的过程中,你遵循的那些最好的实践是什么?** - -这个问题在面试技术经理是非常常见的一个问题。因为异常处理在项目设计中是非常关键的,所以精通异常处理是十分必要的。异常处理有很多最佳实践,下面列举集中,它们提高你代码的健壮性和灵活性: - -  1) 调用方法的时候返回布尔值来代替返回null,这样可以NullPointerException。由于空指针是java异常里最恶心的异常 - -  2) catch块里别不写代码。空catch块是异常处理里的错误事件,因为它只是捕获了异常,却没有任何处理或者提示。通常你起码要打印出异常信息,当然你最好根据需求对异常信息进行处理。 - -  3)能抛受控异常(checked Exception)就尽量不抛受非控异常(checked Exception)。通过去掉重复的异常处理代码,可以提高代码的可读性。 - -  4) 绝对不要让你的数据库相关异常显示到客户端。由于绝大多数数据库和SQLException异常都是受控异常,在Java中,你应该在DAO层把异常信息处理,然后返回处理过的能让用户看懂并根据异常提示信息改正操作的异常信息。 - -  5) 在Java中,一定要在数据库连接,数据库查询,流处理后,在finally块中调用close()方法。 - -**5) 既然我们可以用RuntimeException来处理错误,那么你认为为什么Java中还存在检查型异常?** - -这是一个有争议的问题,在回答该问题时你应当小心。虽然他们肯定愿意听到你的观点,但其实他们最感兴趣的还是有说服力的理由。我认为其中一个理由是,存在检查型异常是一个设计上的决定,受到了诸如C++等比Java更早编程语言设计经验的影响。绝大多数检查型异常位于java.io包内,这是合乎情理的,因为在你请求了不存在的系统资源的时候,一段强壮的程序必须能够优雅的处理这种情况。通过把IOException声明为检查型异常,Java 确保了你能够优雅的对异常进行处理。另一个可能的理由是,可以使用catch或finally来确保数量受限的系统资源(比如文件描述符)在你使用后尽早得到释放。Joshua -Bloch编写的[Effective Java 一书](http://www.amazon.com/dp/0321356683/?tag=javamysqlanta-20)中多处涉及到了该话题,值得一读。 - -**6) throw 和 throws这两个关键字在java中有什么不同?** - -  一个java初学者应该掌握的面试问题。throw 和 throws乍看起来是很相似的尤其是在你还是一个java初学者的时候。尽管他们看起来相似,都是在处理异常时候使用到的。但在代码里的使用方法和用到的地方是不同的。throws总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常, 你也可以申明未检查的异常,但这不是编译器强制的。如果方法抛出了异常那么调用这个方法的时候就需要将这个异常处理。另一个关键字 throw 是用来抛出任意异常的,按照语法你可以抛出任意 Throwable(i.e. Throwable -或任何Throwable的衍生类) , throw可以中断程序运行,因此可以用来代替return . 最常见的例子是用 throw 在一个空方法中需要return的地方抛出 UnSupportedOperationException. -可以看下这篇[文章](http://javarevisited.blogspot.com/2012/02/difference-between-throw-and-throws-in.html)查看这两个关键字在java中更多的差异 。 - -**7) 什么是“异常链”?** - -  “异常链”是Java中非常流行的异常处理概念,是指在进行一个异常处理时抛出了另外一个异常,由此产生了一个异常链条。该技术大多用于将“ 受检查异常” ( checked exception)封装成为“非受检查异常”(unchecked exception)或者RuntimeException。顺便说一下,如果因为因为异常你决定抛出一个新的异常,你一定要包含原有的异常,这样,处理程序才可以通过getCause()和initCause()方法来访问异常最终的根源。 - -**8) 你曾经自定义实现过异常吗?怎么写的?** - -  很显然,我们绝大多数都写过自定义或者业务异常,像AccountNotFoundException。在面试过程中询问这个Java异常问题的主要原因是去发现你如何使用这个特性的。这可以更准确和精致的去处理异常,当然这也跟你选择checked 还是unchecked exception息息相关。通过为每一个特定的情况创建一个特定的异常,你就为调用者更好的处理异常提供了更好的选择。相比通用异常(general exception),我更倾向更为精确的异常。大量的创建自定义异常会增加项目class的个数,因此,在自定义异常和通用异常之间维持一个平衡是成功的关键。 - -**9) JDK7中对异常处理做了什么改变?** - -  这是最近新出的Java异常处理的面试题。JDK7中对错误(Error)和异常(Exception)处理主要新增加了2个特性,一是在一个catch块中可以出来多个异常,就像原来用多个catch块一样。另一个是自动化资源管理(ARM), 也称为try-with-resource块。这2个特性都可以在处理异常时减少代码量,同时提高代码的可读性。对于这些特性了解,不仅帮助开发者写出更好的异常处理的代码,也让你在面试中显的更突出。我推荐大家读一下Java 7攻略,这样可以更深入的了解这2个非常有用的特性。 - -**10) 你遇到过 OutOfMemoryError 错误嘛?你是怎么搞定的?** - -  这个面试题会在面试高级程序员的时候用,面试官想知道你是怎么处理这个危险的OutOfMemoryError错误的。必须承认的是,不管你做什么项目,你都会碰到这个问题。所以你要是说没遇到过,面试官肯定不会买账。要是你对这个问题不熟悉,甚至就是没碰到过,而你又有3、4年的Java经验了,那么准备好处理这个问题吧。在回答这个问题的同时,你也可以借机向面试秀一下你处理内存泄露、调优和调试方面的牛逼技能。我发现掌握这些技术的人都能给面试官留下深刻的印象。 - -**11) 如果执行finally代码块之前方法返回了结果,或者JVM退出了,finally块中的代码还会执行吗?** - -  这个问题也可以换个方式问:“如果在try或者finally的代码块中调用了System.exit(),结果会是怎样”。了解finally块是怎么执行的,即使是try里面已经使用了return返回结果的情况,对了解Java的异常处理都非常有价值。只有在try里面是有System.exit(0)来退出JVM的情况下finally块中的代码才不会执行。 - -**12)Java中final,finalize,finally关键字的区别** - -  这是一个经典的Java面试题了。我的一个朋友为Morgan Stanley招电信方面的核心Java开发人员的时候就问过这个问题。final和finally是Java的关键字,而finalize则是方法。final关键字在创建不可变的类的时候非常有用,只是声明这个类是final的。而finalize()方法则是垃圾回收器在回收一个对象前调用,但也Java规范里面没有保证这个方法一定会被调用。finally关键字是唯一一个和这篇文章讨论到的异常处理相关的关键字。在你的产品代码中,在关闭连接和资源文件的是时候都必须要用到finally块。 - -## 参考文章 - -https://www.xuebuyuan.com/3248044.html -https://www.jianshu.com/p/49d2c3975c56 -http://c.biancheng.net/view/1038.html -https://blog.csdn.net/Lisiluan/article/details/88745820 -https://blog.csdn.net/michaelgo/article/details/82790253 - - diff --git "a/docs/Java/basic/Java\346\263\250\350\247\243\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/Java/basic/Java\346\263\250\350\247\243\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" deleted file mode 100644 index 435e587..0000000 --- "a/docs/Java/basic/Java\346\263\250\350\247\243\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ /dev/null @@ -1,665 +0,0 @@ -# 目录 - * [Java注解简介](#java注解简介) - * [注解如同标签](#注解如同标签) - * [Java 注解概述](#java-注解概述) - * [什么是注解?](#什么是注解?) - * [注解的用处](#注解的用处) - * [注解的原理](#注解的原理) - * [元注解](#元注解) - * [JDK里的注解](#jdk里的注解) - * [注解处理器实战](#注解处理器实战) - * [不同类型的注解](#不同类型的注解) - * [类注解](#类注解) - * [方法注解](#方法注解) - * [参数注解](#参数注解) - * [变量注解](#变量注解) - * [Java注解相关面试题](#java注解相关面试题) - * [什么是注解?他们的典型用例是什么?](#什么是注解?他们的典型用例是什么?) - * [描述标准库中一些有用的注解。](#描述标准库中一些有用的注解。) - * [可以从注解方法声明返回哪些对象类型?](#可以从注解方法声明返回哪些对象类型?) - * [哪些程序元素可以注解?](#哪些程序元素可以注解?) - * [有没有办法限制可以应用注解的元素?](#有没有办法限制可以应用注解的元素?) - * [什么是元注解?](#什么是元注解?) - * [下面的代码会编译吗?](#下面的代码会编译吗?) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## Java注解简介 - -Annotation 中文译过来就是注解、标释的意思,在 Java 中注解是一个很重要的知识点,但经常还是有点让新手不容易理解。 - -**我个人认为,比较糟糕的技术文档主要特征之一就是:用专业名词来介绍专业名词。** -比如: - -> Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。 -> 这是大多数网站上对于 Java 注解,解释确实正确,但是说实在话,我第一次学习的时候,头脑一片空白。这什么跟什么啊?听了像没有听一样。因为概念太过于抽象,所以初学者实在是比较吃力才能够理解,然后随着自己开发过程中不断地强化练习,才会慢慢对它形成正确的认识。 - -我在写这篇文章的时候,我就在思考。如何让自己或者让读者能够比较直观地认识注解这个概念?是要去官方文档上翻译说明吗?我马上否定了这个答案。 - -后来,我想到了一样东西————墨水,墨水可以挥发、可以有不同的颜色,用来解释注解正好。 - -不过,我继续发散思维后,想到了一样东西能够更好地代替墨水,那就是印章。印章可以沾上不同的墨水或者印泥,可以定制印章的文字或者图案,如果愿意它也可以被戳到你任何想戳的物体表面。 - -但是,我再继续发散思维后,又想到一样东西能够更好地代替印章,那就是标签。标签是一张便利纸,标签上的内容可以自由定义。常见的如货架上的商品价格标签、图书馆中的书本编码标签、实验室中化学材料的名称类别标签等等。 - -并且,往抽象地说,标签并不一定是一张纸,它可以是对人和事物的属性评价。也就是说,标签具备对于抽象事物的解释。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230403213103.png) - -所以,基于如此,我完成了自我的知识认知升级,我决定用标签来解释注解。 - -### 注解如同标签 - -之前某新闻客户端的评论有盖楼的习惯,于是 “乔布斯重新定义了手机、罗永浩重新定义了傻X” 就经常极为工整地出现在了评论楼层中,并且广大网友在相当长的一段时间内对于这种行为乐此不疲。这其实就是等同于贴标签的行为。 -在某些网友眼中,罗永浩就成了傻X的代名词。 - -广大网友给罗永浩贴了一个名为“傻x”的标签,他们并不真正了解罗永浩,不知道他当教师、砸冰箱、办博客的壮举,但是因为“傻x”这样的标签存在,这有助于他们直接快速地对罗永浩这个人做出评价,然后基于此,罗永浩就可以成为茶余饭后的谈资,这就是标签的力量。 - -而在网络的另一边,老罗靠他的人格魅力自然收获一大批忠实的拥泵,他们对于老罗贴的又是另一种标签。 - -![](https://img-blog.csdn.net/20170627213530055?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYnJpYmx1ZQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) - - -老罗还是老罗,但是由于人们对于它贴上的标签不同,所以造成对于他的看法大相径庭,不喜欢他的人整天在网络上评论抨击嘲讽,而崇拜欣赏他的人则会愿意挣钱购买锤子手机的发布会门票。 - -我无意于评价这两种行为,我再引个例子。 - -《奇葩说》是近年网络上非常火热的辩论节目,其中辩手陈铭被另外一个辩手马薇薇攻击说是————“站在宇宙中心呼唤爱”,然后贴上了一个大大的标签————“鸡汤男”,自此以后,观众再看到陈铭的时候,首先映入脑海中便是“鸡汤男”三个大字,其实本身而言陈铭非常优秀,为人师表、作风正派、谈吐举止得体,但是在网络中,因为娱乐至上的环境所致,人们更愿意以娱乐的心态来认知一切,于是“鸡汤男”就如陈铭自己所说成了一个撕不了的标签。 - -**我们可以抽象概括一下,标签是对事物行为的某些角度的评价与解释。** - -到这里,终于可以引出本文的主角注解了。 - -**初学者可以这样理解注解:想像代码具有生命,注解就是对于代码中某些鲜活个体的贴上去的一张标签。简化来讲,注解如同一张标签。** - -在未开始学习任何注解具体语法而言,你可以把注解看成一张标签。这有助于你快速地理解它的大致作用。如果初学者在学习过程有大脑放空的时候,请不要慌张,对自己说: - -注解,标签。注解,标签。 - -## Java 注解概述 -### 什么是注解? - - -> 对于很多初次接触的开发者来说应该都有这个疑问?Annontation是Java5开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。 - - -  Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。 - - - -### 注解的用处 - - 1、生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等 - 2、跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2依赖注入,未来java开发,将大量注解配置,具有很大用处; - 3、在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。 - - - -### 注解的原理 -  注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。 - - - -### 元注解 -java.lang.annotation提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解): - @Documented –注解是否将包含在JavaDoc中 - @Retention –什么时候使用该注解 - @Target –注解用于什么地方 - @Inherited – 是否允许子类继承该注解 - - 1.)@Retention– 定义该注解的生命周期 - - ● RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。 - - ● RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式 - - ● RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。 - - 2.)Target – 表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType参数包括 - - ● ElementType.CONSTRUCTOR:用于描述构造器 - ● ElementType.FIELD:成员变量、对象、属性(包括enum实例) - ● ElementType.LOCAL_VARIABLE:用于描述局部变量 - ● ElementType.METHOD:用于描述方法 - ● ElementType.PACKAGE:用于描述包 - ● ElementType.PARAMETER:用于描述参数 - ● ElementType.TYPE:用于描述类、接口(包括注解类型) 或enum声明 - - 3.)@Documented–一个简单的Annotations标记注解,表示是否将注解信息添加在java文档中。 - - 4.)@Inherited – 定义该注释和子类的关系 - @Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。 - - -### JDK里的注解 -JDK 内置注解 -先来看几个 Java 内置的注解,让大家热热身。 - -@Override 演示 -```` - class Parent { - public void run() { - } - } - - class Son extends Parent { - /** - * 这个注解是为了检查此方法是否真的是重写父类的方法 - * 这时候就不用我们用肉眼去观察到底是不是重写了 - */ - @Override - public void run() { - } - } -```` -@Deprecated 演示 -```` -class Parent { - - /** - * 此注解代表过时了,但是如果可以调用到,当然也可以正常使用 - * 但是,此方法有可能在以后的版本升级中会被慢慢的淘汰 - * 可以放在类,变量,方法上面都起作用 - */ - @Deprecated - public void run() { - } - } - - public class JDKAnnotationDemo { - public static void main(String[] args) { - Parent parent = new Parent(); - parent.run(); // 在编译器中此方法会显示过时标志 - } - } -```` -@SuppressWarnings 演示 -```` -class Parent { - - // 因为定义的 name 没有使用,那么编译器就会有警告,这时候使用此注解可以屏蔽掉警告 - // 即任意不想看到的编译时期的警告都可以用此注解屏蔽掉,但是不推荐,有警告的代码最好还是处理一下 - @SuppressWarnings("all") - private String name; - } -```` -@FunctionalInterface 演示 - -```` -/** - * 此注解是 Java8 提出的函数式接口,接口中只允许有一个抽象方法 - * 加上这个注解之后,类中多一个抽象方法或者少一个抽象方法都会报错 - */ -@FunctionalInterface -interface Func { - void run(); -} -```` - -## 注解处理器实战 - -注解处理器 -注解处理器才是使用注解整个流程中最重要的一步了。所有在代码中出现的注解,它到底起了什么作用,都是在注解处理器中定义好的。 -概念:注解本身并不会对程序的编译方式产生影响,而是注解处理器起的作用;注解处理器能够通过在运行时使用反射获取在程序代码中的使用的注解信息,从而实现一些额外功能。前提是我们自定义的注解使用的是 RetentionPolicy.RUNTIME 修饰的。这也是我们在开发中使用频率很高的一种方式。 - -我们先来了解下如何通过在运行时使用反射获取在程序中的使用的注解信息。如下类注解和方法注解。 - -类注解 - -```` -Class aClass = ApiController.class; -Annotation[] annotations = aClass.getAnnotations(); - -for(Annotation annotation : annotations) { - if(annotation instanceof ApiAuthAnnotation) { - ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation; - System.out.println("name: " + apiAuthAnnotation.name()); - System.out.println("age: " + apiAuthAnnotation.age()); - } -} -方法注解 -Method method = ... //通过反射获取方法对象 -Annotation[] annotations = method.getDeclaredAnnotations(); - -for(Annotation annotation : annotations) { - if(annotation instanceof ApiAuthAnnotation) { - ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation; - System.out.println("name: " + apiAuthAnnotation.name()); - System.out.println("age: " + apiAuthAnnotation.age()); - } -} -```` -此部分内容可参考: 通过反射获取注解信息 - -注解处理器实战 -接下来我通过在公司中的一个实战改编来演示一下注解处理器的真实使用场景。 -需求: 网站后台接口只能是年龄大于 18 岁的才能访问,否则不能访问 -前置准备: 定义注解(这里使用上文的完整注解),使用注解(这里使用上文中使用注解的例子) -接下来要做的事情: 写一个切面,拦截浏览器访问带注解的接口,取出注解信息,判断年龄来确定是否可以继续访问。 - -在 dispatcher-servlet.xml 文件中定义 aop 切面 -```` - - - - - - - - -```` -切面类处理逻辑即注解处理器代码如 -```` -@Component("apiAuthAspect") -public class ApiAuthAspect { - - public Object auth(ProceedingJoinPoint pjp) throws Throwable { - Method method = ((MethodSignature) pjp.getSignature()).getMethod(); - ApiAuthAnnotation apiAuthAnnotation = method.getAnnotation(ApiAuthAnnotation.class); - Integer age = apiAuthAnnotation.age(); - if (age > 18) { - return pjp.proceed(); - } else { - throw new RuntimeException("你未满18岁,禁止访问"); - } - } -} -```` - -## 不同类型的注解 - -### 类注解 - -你可以在运行期访问类,方法或者变量的注解信息,下是一个访问类注解的例子: - -``` -Class aClass = TheClass.class; -Annotation[] annotations = aClass.getAnnotations(); - -for(Annotation annotation : annotations){ - if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); - } -} -``` - -你还可以像下面这样指定访问一个类的注解: - -``` -Class aClass = TheClass.class; -Annotation annotation = aClass.getAnnotation(MyAnnotation.class); - -if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); -} -``` - -### 方法注解 - -下面是一个方法注解的例子: - -``` -public class TheClass { - @MyAnnotation(name="someName", value = "Hello World") - public void doSomething(){} -} -``` - -你可以像这样访问方法注解: - -``` -Method method = ... //获取方法对象 -Annotation[] annotations = method.getDeclaredAnnotations(); - -for(Annotation annotation : annotations){ - if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); - } -} -``` - -你可以像这样访问指定的方法注解: - -``` -Method method = ... // 获取方法对象 -Annotation annotation = method.getAnnotation(MyAnnotation.class); - -if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); -} -``` - -### 参数注解 - -方法参数也可以添加注解,就像下面这样: - -``` -public class TheClass { - public static void doSomethingElse( - @MyAnnotation(name="aName", value="aValue") String parameter){ - } -} -``` - -你可以通过 Method对象来访问方法参数注解: - -``` -Method method = ... //获取方法对象 -Annotation[][] parameterAnnotations = method.getParameterAnnotations(); -Class[] parameterTypes = method.getParameterTypes(); - -int i=0; -for(Annotation[] annotations : parameterAnnotations){ - Class parameterType = parameterTypes[i++]; - - for(Annotation annotation : annotations){ - if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("param: " + parameterType.getName()); - System.out.println("name : " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); - } - } -} -``` - -需要注意的是 Method.getParameterAnnotations()方法返回一个注解类型的二维数组,每一个方法的参数包含一个注解数组。 - -### 变量注解 - -下面是一个变量注解的例子: - -``` -public class TheClass { - - @MyAnnotation(name="someName", value = "Hello World") - public String myField = null; -} -``` - -你可以像这样来访问变量的注解: - -``` -Field field = ... //获取方法对象目录 -Annotation[] annotations = field.getDeclaredAnnotations(); - -for(Annotation annotation : annotations){ - if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); - } -} -``` - -你可以像这样访问指定的变量注解: - -``` -Field field = ...//获取方法对象目录 - -Annotation annotation = field.getAnnotation(MyAnnotation.class); - -if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); -} -``` - -## Java注解相关面试题 - -### 什么是注解?他们的典型用例是什么? - -注解是绑定到程序源代码元素的元数据,对运行代码的操作没有影响。 - -他们的典型用例是: - -* 编译器的信息 - 使用注解,编译器可以检测错误或抑制警告 -* 编译时和部署时处理 - 软件工具可以处理注解并生成代码,配置文件等。 -* 运行时处理 - 可以在运行时检查注解以自定义程序的行为 - -### 描述标准库中一些有用的注解。 - -java.lang和java.lang.annotation包中有几个注解,更常见的包括但不限于此: - -* @Override -标记方法是否覆盖超类中声明的元素。如果它无法正确覆盖该方法,编译器将发出错误 -* @Deprecated - 表示该元素已弃用且不应使用。如果程序使用标有此批注的方法,类或字段,编译器将发出警告 -* @SuppressWarnings - 告诉编译器禁止特定警告。在与泛型出现之前编写的遗留代码接口时最常用的 -* @FunctionalInterface - 在Java 8中引入,表明类型声明是一个功能接口,可以使用Lambda Expression提供其实现 - - -### 可以从注解方法声明返回哪些对象类型? - -返回类型必须是基本类型,String,Class,Enum或数组类型之一。否则,编译器将抛出错误。 - -这是一个成功遵循此原则的示例代码: - -``` -enum Complexity { - LOW, HIGH -} - -public @interface ComplexAnnotation { - Class value(); - - int[] types(); - - Complexity complexity(); -} - -``` - -下一个示例将无法编译,因为Object不是有效的返回类型: - -``` -public @interface FailingAnnotation { - Object complexity(); -} - -``` - -### 哪些程序元素可以注解? - -注解可以应用于整个源代码的多个位置。它们可以应用于类,构造函数和字段的声明: - -``` -@SimpleAnnotation -public class Apply { - @SimpleAnnotation - private String aField; - - @SimpleAnnotation - public Apply() { - // ... - } -} - -``` - -方法及其参数: - -``` -@SimpleAnnotation -public void aMethod(@SimpleAnnotation String param) { - // ... -} - -``` - -局部变量,包括循环和资源变量: - -``` -@SimpleAnnotation -int i = 10; - -for (@SimpleAnnotation int j = 0; j < i; j++) { - // ... -} - -try (@SimpleAnnotation FileWriter writer = getWriter()) { - // ... -} catch (Exception ex) { - // ... -} - -``` - -其他注解类型: - -``` -@SimpleAnnotation -public @interface ComplexAnnotation { - // ... -} - -``` - -甚至包,通过package-info.java文件: - -``` -@PackageAnnotation -package com.baeldung.interview.annotations; - -``` - -从Java 8开始,它们也可以应用于类型的使用。为此,注解必须指定值为ElementType.USE的@Target注解: - -``` -@Target(ElementType.TYPE_USE) -public @interface SimpleAnnotation { - // ... -} - -``` - -现在,注解可以应用于类实例创建: - -``` -new @SimpleAnnotation Apply(); - -``` - -类型转换: - -``` -aString = (@SimpleAnnotation String) something; - -``` - -接口中: - -``` -public class SimpleList - implements @SimpleAnnotation List<@SimpleAnnotation T> { - // ... -} - -``` - -抛出异常上: - -``` -void aMethod() throws @SimpleAnnotation Exception { - // ... -} - -``` - -### 有没有办法限制可以应用注解的元素? - -有,@ Target注解可用于此目的。如果我们尝试在不适用的上下文中使用注解,编译器将发出错误。 - -以下是仅将@SimpleAnnotation批注的用法限制为字段声明的示例: - -``` -@Target(ElementType.FIELD) -public @interface SimpleAnnotation { - // ... -} - -``` - -如果我们想让它适用于更多的上下文,我们可以传递多个常量: - -``` -@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE }) - -``` - -我们甚至可以制作一个注解,因此它不能用于注解任何东西。当声明的类型仅用作复杂注解中的成员类型时,这可能会派上用场: - -``` -@Target({}) -public @interface NoTargetAnnotation { - // ... -} - -``` - -### 什么是元注解? - -元注解适用于其他注解的注解。 - -所有未使用@Target标记或使用它标记但包含ANNOTATION_TYPE常量的注解也是元注解: - -``` -@Target(ElementType.ANNOTATION_TYPE) -public @interface SimpleAnnotation { - // ... -} - -``` - - -### 下面的代码会编译吗? - -``` -@Target({ ElementType.FIELD, ElementType.TYPE, ElementType.FIELD }) -public @interface TestAnnotation { - int[] value() default {}; -} - -``` - -不能。如果在@Target注解中多次出现相同的枚举常量,那么这是一个编译时错误。 - -删除重复常量将使代码成功编译: - -``` -@Target({ ElementType.FIELD, ElementType.TYPE}) - -``` - -## 参考文章 - -https://blog.fundodoo.com/2018/04/19/130.html -https://blog.csdn.net/qq_37939251/article/details/83215703 -https://blog.51cto.com/4247649/2109129 -https://www.jianshu.com/p/2f2460e6f8e7 -https://blog.csdn.net/yuzongtao/article/details/83306182 - - - diff --git "a/docs/Java/basic/Java\347\261\273\345\222\214\345\214\205.md" "b/docs/Java/basic/Java\347\261\273\345\222\214\345\214\205.md" deleted file mode 100644 index 43d7244..0000000 --- "a/docs/Java/basic/Java\347\261\273\345\222\214\345\214\205.md" +++ /dev/null @@ -1,411 +0,0 @@ -# 目录 - -* [Java中的包概念](#java中的包概念) - * [包的作用](#包的作用) - * [package 的目录结构](#package-的目录结构) - * [设置 CLASSPATH 系统变量](#设置-classpath-系统变量) -* [常用jar包](#常用jar包) - * [java软件包的类型](#java软件包的类型) - * [dt.jar](#dtjar) - * [rt.jar](#rtjar) -* [*.java文件的奥秘](#java文件的奥秘) - * [*.Java文件简介](#java文件简介) - * [为什么一个java源文件中只能有一个public类?](#为什么一个java源文件中只能有一个public类?) - * [Main方法](#main方法) - * [外部类的访问权限](#外部类的访问权限) - * [Java包的命名规则](#java包的命名规则) -* [参考文章](#参考文章) - - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - -## Java中的包概念 - -Java中的包是封装一组类,子包和接口的机制。软件包用于: - -防止命名冲突。例如,可以有两个名称分别为Employee的类,college.staff.cse.Employee和college.staff.ee.Employee -更轻松地搜索/定位和使用类,接口,枚举和注释 - -提供受控访问:受保护和默认有包级别访问控制。受保护的成员可以通过同一个包及其子类中的类访问。默认成员(没有任何访问说明符)只能由同一个包中的类访问。 - -包可以被视为数据封装(或数据隐藏)。 - -我们所需要做的就是将相关类放入包中。之后,我们可以简单地从现有的软件包中编写一个导入类,并将其用于我们的程序中。一个包是一组相关类的容器,其中一些类可以访问,并且其他类被保存用于内部目的。 -我们可以在程序中尽可能多地重用包中的现有类。 - -为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。 - -### 包的作用 - -* 1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。 - -* 2、如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。 - -* 3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。 - -Java 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类(class)、接口、枚举(enumerations)和注释(annotation)等。 - - 包语句的语法格式为: - - package pkg1[.pkg2[.pkg3…]]; - - 例如,一个Something.java 文件它的内容 - - package net.java.util; public class Something{ ... } - -那么它的路径应该是**net/java/util/Something.java**这样保存的。 package(包) 的作用是把不同的 java 程序分类保存,更方便的被其他 java 程序调用。 - -一个包(package)可以定义为一组相互联系的类型(类、接口、枚举和注释),为这些类型提供访问保护和命名空间管理的功能。 - -以下是一些 Java 中的包: - -* **java.lang**-打包基础的类 -* **java.io**-包含输入输出功能的函数 - -开发者可以自己把一组类和接口等打包,并定义自己的包。而且在实际开发中这样做是值得提倡的,当你自己完成类的实现之后,将相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和注释等是相关的。 - -由于包创建了新的命名空间(namespace),所以不会跟其他包中的任何名字产生命名冲突。使用包这种机制,更容易实现访问控制,并且让定位相关类更加简单。 - -### package 的目录结构 - -类放在包中会有两种主要的结果: - -* 包名成为类名的一部分,正如我们前面讨论的一样。 -* 包名必须与相应的字节码所在的目录结构相吻合。 - -下面是管理你自己 java 中文件的一种简单方式: - -将类、接口等类型的源码放在一个文本中,这个文件的名字就是这个类型的名字,并以.java作为扩展名。例如: - -```` -// 文件名 : Car.java -package vehicle; -public class Car { -// 类实现 -} -```` - -接下来,把源文件放在一个目录中,这个目录要对应类所在包的名字。 - -....\vehicle\Car.java - -现在,正确的类名和路径将会是如下样子: - -* 类名 -> vehicle.Car - -* 路径名 -> vehicle\Car.java (在 windows 系统中) - -通常,一个公司使用它互联网域名的颠倒形式来作为它的包名.例如:互联网域名是 runoob.com,所有的包名都以 com.runoob 开头。包名中的每一个部分对应一个子目录。 - -例如:有一个**com.runoob.test**的包,这个包包含一个叫做 Runoob.java 的源文件,那么相应的,应该有如下面的一连串子目录: - -....\com\runoob\test\Runoob.java - -编译的时候,编译器为包中定义的每个类、接口等类型各创建一个不同的输出文件,输出文件的名字就是这个类型的名字,并加上 .class 作为扩展后缀。 例如: -```` -// 文件名: Runoob.java -package com.runoob.test; -public class Runoob { } -class Google { } -```` - - -现在,我们用-d选项来编译这个文件,如下: -```` - $javac -d . Runoob.java -```` -这样会像下面这样放置编译了的文件: - -```` - .\com\runoob\test\Runoob.class - .\com\runoob\test\Google.class -```` - -你可以像下面这样来导入所有**\com\runoob\test\**中定义的类、接口等: -```` - import com.runoob.test.*; -```` - -编译之后的 .class 文件应该和 .java 源文件一样,它们放置的目录应该跟包的名字对应起来。但是,并不要求 .class 文件的路径跟相应的 .java 的路径一样。你可以分开来安排源码和类的目录。 - -```` - \sources\com\runoob\test\Runoob.java - \classes\com\runoob\test\Google.class -```` - -这样,你可以将你的类目录分享给其他的编程人员,而不用透露自己的源码。用这种方法管理源码和类文件可以让编译器和java 虚拟机(JVM)可以找到你程序中使用的所有类型。 - -类目录的绝对路径叫做**class path**。设置在系统变量**CLASSPATH**中。编译器和 java 虚拟机通过将 package 名字加到 class path 后来构造 .class 文件的路径。 - -```` - \classes 是 class path,package 名字是 com.runoob.test,而编译器和 JVM 会在 \classes\com\runoob\test 中找 .class 文件。 -```` - -一个 class path 可能会包含好几个路径,多路径应该用分隔符分开。默认情况下,编译器和 JVM 查找当前目录。JAR 文件按包含 Java 平台相关的类,所以他们的目录默认放在了 class path 中。 - -### 设置 CLASSPATH 系统变量 - -用下面的命令显示当前的CLASSPATH变量: - -* Windows 平台(DOS 命令行下):C:\> set CLASSPATH -* UNIX 平台(Bourne shell 下):# echo $CLASSPATH - -删除当前CLASSPATH变量内容: - -* Windows 平台(DOS 命令行下):C:\> set CLASSPATH= -* UNIX 平台(Bourne shell 下):# unset CLASSPATH; export CLASSPATH - -设置CLASSPATH变量: - -* Windows 平台(DOS 命令行下): C:\> set CLASSPATH=C:\users\jack\java\classes -* UNIX 平台(Bourne shell 下):# CLASSPATH=/home/jack/java/classes; export CLASSPATH - -Java包(package)详解 -java包的作用是为了区别类名的命名空间   - -1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。、 - -2、如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的, - -当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。 - -3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。 - -创建包 -创建包的时候,你需要为这个包取一个合适的名字。之后,如果其他的一个源文件包含了这个包提供的类、接口、枚举或者注释类型的时候,都必须将这个包的声明放在这个源文件的开头。 - -包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都应用于它。 - -如果一个源文件中没有使用包声明,那么其中的类,函数,枚举,注释等将被放在一个无名的包(unnamed package)中。 - -例子 -让我们来看一个例子,这个例子创建了一个叫做animals的包。通常使用小写的字母来命名避免与类、接口名字的冲突。 - -在 animals 包中加入一个接口(interface): - -```` -package animals; -interface Animal { - public void eat(); - public void travel(); -} -```` - -接下来,在同一个包中加入该接口的实现: - -```` -package animals; -/* 文件名 : MammalInt.java */ -public class MammalInt implements Animal{ - public void eat(){ - System.out.println("Mammal eats"); - } - public void travel(){ - System.out.println("Mammal travels"); - } - public int noOfLegs(){ - return 0; - } - public static void main(String args[]){ - MammalInt m = new MammalInt(); - m.eat(); - m.travel(); - } -} -```` - -import 关键字 -为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 "import" 语句可完成此功能。 - -在 java 源文件中 import 语句应位于 package 语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式为: - -```` - import package1[.package2…].(classname|*); -```` - -如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。 - -通常,一个公司使用它互联网域名的颠倒形式来作为它的包名.例如:互联网域名是 runoob.com,所有的包名都以 com.runoob 开头。包名中的每一个部分对应一个子目录。 - -例如:有一个 com.runoob.test 的包,这个包包含一个叫做 Runoob.java 的源文件,那么相应的,应该有如下面的一连串子目录: -``` - ....\com\runoob\test\Runoob.java -``` -## 常用jar包 - -### java软件包的类型 - -软件包的类型有内置的软件包和用户定义的软件包内置软件包 -这些软件包由大量的类组成,这些类是Java API的一部分。一些常用的内置软件包有: - -1)java.lang:包含语言支持类(例如分类,用于定义基本数据类型,数学运算)。该软件包会自动导入。 - -2) java.io:包含分类以支持输入/输出操作。 - -3) java.util:包含实现像链接列表,字典和支持等数据结构的实用类; 用于日期/时间操作。 - -4) java.applet:包含用于创建Applets的类。 - -5) java.awt:包含用于实现图形用户界面组件的类(如按钮,菜单等)。 - -6) java.net:包含支持网络操作的类。 - -### dt.jar - -> SUN对于dt.jar的定义:Also includesdt.jar, the DesignTime archive of BeanInfo files that tell interactive development environments (IDE's) how to display the Java components and how to let the developer customize them for the application。 - -中文翻译过来就是:dt.jar是BeanInfo文件的DesignTime归档,BeanInfo文件用来告诉集成开发环境(IDE)如何显示Java组件还有如何让开发人员根据应用程序自定义它们。这段文字中提到了几个关键字:DesignTime,BeanInfo,IDE,Java components。其实dt.jar就是DesignTime Archive的缩写。那么何为DesignTime。 - -何为DesignTime?翻译过来就是设计时。其实了解JavaBean的人都知道design time和runtime(运行时)这两个术语的含义。设计时(DesignTIme)是指在开发环境中通过添加控件,设置控件或窗体属性等方法,建立应用程序的时间。 - -与此相对应的运行时(RunTIme)是指可以象用户那样与应用程序交互作用的时间。那么现在再理解一下上面的翻译,其实dt.jar包含了swing控件中的BeanInfo,而IDE的GUI Designer需要这些信息。那让我们看一下dt.jar中到底有什么? - -dt.jar中全部是Swing组件的BeanInfo。那么到底什么是BeanInfo呢? - -何为BeanInfo?JavaBean和BeanInfo有很大的关系。Sun所制定的JavaBean规范,很大程度上是为IDE准备的——它让IDE能够以可视化的方式设置JavaBean的属性。如果在IDE中开发一个可视化应用程序,我们需要通过属性设置的方式对组成应用的各种组件进行定制,IDE通过属性编辑器让开发人员使用可视化的方式设置组件的属性。 - -dt.jar里面主要是swing组件的BeanInfo。IDE根据这些BeanInfo显示这些组件以及开发人员如何定制他们。 - -### rt.jar -rt.jar是runtime的归档。Java基础类库,也就是Java doc里面看到的所有的类的class文件。 - -![image](https://www.pianshen.com/images/75/856cbbdf52da90fa4f9bbb7b0597ce63.png) - -rt.jar 默认就在Root Classloader的加载路径里面的,而在Claspath配置该变量是不需要的;同时jre/lib目录下的其他jar:jce.jar、jsse.jar、charsets.jar、resources.jar都在Root Classloader中。 - -## java文件的奥秘 -### Java文件简介 - -.java文件你可以认为只是一个文本文件, 这个文件即是用java语言写成的程序,或者说任务的代码块。 - -.class文件本质上是一种二进制文件, 它一般是由.java文件通过 javac这个命令(jdk本身提供的工具)生成的一个文件, 而这个文件可以由jvm(java虚拟机)装载(类装载),然后进java解释执行, 这也就是运行你的程序。 - -你也可以这样比较一下: -.java与 .c , .cpp, .asm等等文件,本质 上一样的, 只是用一种 语言来描述你要怎么去完成一件事(一个任务), 而这种语言 计算机本身 是没有办法知道是什么含义的, 它面向的只是程序员本身, 程序员可以通过 语言本身(语法) 来描述或组织这个任务,这也就 是所谓的编程。 - -最后你当然是需要计算机按照你的意图来运行你的程序, 这时候就先得有一个翻译(编译, 汇编, 链接等等复杂的过程)把它变成机器可理解的指令(这就是大家说的机器语言,机器语言本身也是一种编程语言,只是程序很难写,很难读懂,基本上没有办法维护)。 - - -这里的.class文件在计算的体系结构中本质上对应的是一种机器语言(而这里的机器叫作JVM),所以JVM本身是可以直接运行这里的.class文件。所以 你可以进一步地认为,.java与.class与其它的编程语法一样,它们都是程序员用来描述自己的任务的一种语言,只是它们面向的对象不一样,而计算机本身只能识别它自已定义的那些指令什么的(再次强调,这里的计算机本身没有那么严格的定义) - -> In short: -> -> .java是Java的源文件后缀,里面存放程序员编写的功能代码。 -> -> .class文件是字节码文件,由.java源文件通过javac命令编译后生成的文件。是可以运行在任何支持Java虚拟机的硬件平台和操作系统上的二进制文件。 -> -> .class文件并不本地的可执行程序。Java虚拟机就是去运行.class文件从而实现程序的运行。 - - -### 为什么一个java源文件中只能有一个public类? - -在java编程思想(第四版)一书中有这样3段话(6.4 类的访问权限): - ->   1.每个编译单元(文件)都只能有一个public类,这表示,每个编译单元都有单一的公共接口,用public类来表现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出错误信息。 -> ->   2.public类的名称必须完全与含有该编译单元的文件名相同,包含大小写。如果不匹配,同样将得到编译错误。 -> ->   3.虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下,可以随意对文件命名。 - -总结相关的几个问题: - -1、一个”.java”源文件中是否可以包括多个类(不是内部类)?有什么限制? - -> 答:可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。 - -2、为什么一个文件中只能有一个public的类 - -> 答:编译器在编译时,针对一个java源代码文件(也称为“编译单元”)只会接受一个public类。否则报错。 - -3、在java文件中是否可以没有public类 - -> 答:public类不是必须的,java文件中可以没有public类。 - -4、为什么这个public的类的类名必须和文件名相同 - -> 答: 是为了方便虚拟机在相应的路径中找到相应的类所对应的字节码文件。 - -### Main方法 - -主函数:是一个特殊的函数,作为程序的入口,可以被JVM调用 - -主函数的定义: -> public:代表着该函数访问权限是最大的 - -> static:代表主函数随着类的加载就已经存在了 - -> void:主函数没有具体的返回值 - -> main:不是关键字,但是一个特殊的单词,能够被JVM识别 - -> (String[] args):函数的参数,参数类型是一个数组,该数组中的元素师字符串,字符串数组。main(String[] args) 字符串数组的 此时空数组的长度是0,但也可以在 运行的时候向其中传入参数。 - -主函数时固定格式的,JVM识别 - -主函数可以被重载,但是JVM只识别main(String[] args),其他都是作为一般函数。这里面的args知识数组变量可以更改,其他都不能更改。 - -一个java文件中可以包含很多个类,每个类中有且仅有一个主函数,但是每个java文件中可以包含多个主函数,在运行时,需要指定JVM入口是哪个。例如一个类的主函数可以调用另一个类的主函数。不一定会使用public类的主函数。 - -### 外部类的访问权限 - -外部类只能用public和default修饰。 - -为什么要对外部类或类做修饰呢? - -> 1.存在包概念:public 和 default 能区分这个外部类能对不同包作一个划分 (default修饰的类,其他包中引入不了这个类,public修饰的类才能被import) -> -> 2.protected是包内可见并且子类可见,但是当一个外部类想要继承一个protected修饰的非同包类时,压根找不到这个类,更别提几层了 -> -> 3.private修饰的外部类,其他任何外部类都无法导入它。 - -```` -//Java中的文件名要和public修饰的类名相同,否则会报错 -//如果没有public修饰的类,则文件可以随意命名 -public class Java中的类文件 { -} -//非公共开类的访问权限默认是包访问权限,不能用private和protected -//一个外部类的访问权限只有两种,一种是包内可见,一种是包外可见。 -//如果用private修饰,其他类根本无法看到这个类,也就没有意义了。 -//如果用protected,虽然也是包内可见,但是如果有子类想要继承该类但是不同包时, -//压根找不到这个类,也不可能继承它了,所以干脆用default代替。 -class A{ -} -```` - -### Java包的命名规则 - -> 以 java.* 开头的是Java的核心包,所有程序都会使用这些包中的类; - -> 以 javax.* 开头的是扩展包,x 是 extension 的意思,也就是扩展。虽然 javax.* 是对 java.* 的优化和扩展,但是由于 javax.* 使用的越来越多,很多程序都依赖于 javax.*,所以 javax.* 也是核心的一部分了,也随JDK一起发布。 - -> 以 org.* 开头的是各个机构或组织发布的包,因为这些组织很有影响力,它们的代码质量很高,所以也将它们开发的部分常用的类随JDK一起发布。 - -> 在包的命名方面,为了防止重名,有一个惯例:大家都以自己域名的倒写形式作为开头来为自己开发的包命名,例如百度发布的包会以 com.baidu.* 开头,w3c组织发布的包会以 org.w3c.* 开头,微学苑发布的包会以 net.weixueyuan.* 开头…… - -> 组织机构的域名后缀一般为 org,公司的域名后缀一般为 com,可以认为 org.* 开头的包为非盈利组织机构发布的包,它们一般是开源的,可以免费使用在自己的产品中,不用考虑侵权问题,而以 com.* 开头的包往往由盈利性的公司发布,可能会有版权问题,使用时要注意。 - - - -## 参考文章 - -https://www.cnblogs.com/ryanzheng/p/8465701.html -https://blog.csdn.net/fuhanghang/article/details/84102404 -https://www.runoob.com/java/java-package.html -https://www.breakyizhan.com/java/4260.html -https://blog.csdn.net/qq_36626914/article/details/80627454 - diff --git "a/docs/Java/basic/Java\350\207\252\345\212\250\346\213\206\347\256\261\350\243\205\347\256\261\351\207\214\351\232\220\350\227\217\347\232\204\347\247\230\345\257\206.md" "b/docs/Java/basic/Java\350\207\252\345\212\250\346\213\206\347\256\261\350\243\205\347\256\261\351\207\214\351\232\220\350\227\217\347\232\204\347\247\230\345\257\206.md" deleted file mode 100644 index 4d2dfac..0000000 --- "a/docs/Java/basic/Java\350\207\252\345\212\250\346\213\206\347\256\261\350\243\205\347\256\261\351\207\214\351\232\220\350\227\217\347\232\204\347\247\230\345\257\206.md" +++ /dev/null @@ -1,582 +0,0 @@ -# 目录 - -* [Java 基本数据类型](#java-基本数据类型) - * [Java 的两大数据类型:](#java-的两大数据类型) - * [内置数据类型](#内置数据类型) - * [引用类型](#引用类型) - * [Java 常量](#java-常量) - * [自动拆箱和装箱(详解)](#自动拆箱和装箱(详解)) - * [简易实现](#简易实现) - * [自动装箱与拆箱中的“坑”](#自动装箱与拆箱中的坑) - * [了解基本类型缓存(常量池)的最佳实践](#了解基本类型缓存(常量池)的最佳实践) - * [总结:](#总结:) - * [基本数据类型的存储方式](#基本数据类型的存储方式) - * [存在栈中:](#存在栈中:) - * [存在堆里](#存在堆里) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - -# Java 基本数据类型 - -变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间。 - -内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据。 - -![](https://www.runoob.com/wp-content/uploads/2013/12/memorypic1.jpg) - -因此,通过定义不同类型的变量,可以在内存中储存整数、小数或者字符。 - -## Java 的两大数据类型: - -* 内置数据类型 -* 引用数据类型 - -* * * - -### 内置数据类型 - -Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 - -**byte:** - -* byte 数据类型是8位、有符号的,以二进制补码表示的整数; -* 最小值是 -128(-2^7); -* 最大值是 127(2^7-1); -* 默认值是 0; -* byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一; -* 例子:byte a = 100,byte b = -50。 - -**short:** - -* short 数据类型是 16 位、有符号的以二进制补码表示的整数 -* 最小值是 -32768(-2^15); -* 最大值是 32767(2^15 - 1); -* Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一; -* 默认值是 0; -* 例子:short s = 1000,short r = -20000。 - -**int:** - -* int 数据类型是32位、有符号的以二进制补码表示的整数; -* 最小值是 -2,147,483,648(-2^31); -* 最大值是 2,147,483,647(2^31 - 1); -* 一般地整型变量默认为 int 类型; -* 默认值是 0 ; -* 例子:int a = 100000, int b = -200000。 - -**long:** - -* long 数据类型是 64 位、有符号的以二进制补码表示的整数; -* 最小值是 -9,223,372,036,854,775,808(-2^63); -* 最大值是 9,223,372,036,854,775,807(2^63 -1); -* 这种类型主要使用在需要比较大整数的系统上; -* 默认值是 0L; -* 例子: long a = 100000L,Long b = -200000L。 - "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。 - -**float:** - -* float 数据类型是单精度、32位、符合IEEE 754标准的浮点数; -* float 在储存大型浮点数组的时候可节省内存空间; -* 默认值是 0.0f; -* 浮点数不能用来表示精确的值,如货币; -* 例子:float f1 = 234.5f。 - -**double:** - -* double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数; -* 浮点数的默认类型为double类型; -* double类型同样不能表示精确的值,如货币; -* 默认值是 0.0d; -* 例子:double d1 = 123.4。 - -**boolean:** - -* boolean数据类型表示一位的信息; -* 只有两个取值:true 和 false; -* 这种类型只作为一种标志来记录 true/false 情况; -* 默认值是 false; -* 例子:boolean one = true。 - -**char:** - -* char类型是一个单一的 16 位 Unicode 字符; -* 最小值是 \u0000(即为0); -* 最大值是 \uffff(即为65,535); -* char 数据类型可以储存任何字符; -* 例子:char letter = 'A';。 - - -``` -//8位 -byte bx = Byte.MAX_VALUE; -byte bn = Byte.MIN_VALUE; -//16位 -short sx = Short.MAX_VALUE; -short sn = Short.MIN_VALUE; -//32位 -int ix = Integer.MAX_VALUE; -int in = Integer.MIN_VALUE; -//64位 -long lx = Long.MAX_VALUE; -long ln = Long.MIN_VALUE; -//32位 -float fx = Float.MAX_VALUE; -float fn = Float.MIN_VALUE; -//64位 -double dx = Double.MAX_VALUE; -double dn = Double.MIN_VALUE; -//1位 -boolean bt = Boolean.TRUE; -boolean bf = Boolean.FALSE; -``` - -``` -打印它们的结果可以得到 - -`127` -`-128` -`32767` -`-32768` -`2147483647` -`-2147483648` -`9223372036854775807` -`-9223372036854775808` -`3.4028235E38` -`1.4E-45` -`1.7976931348623157E308` -`4.9E-324` -`true` -`false` -``` - -### 引用类型 - -- 在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不能被改变了。 -- 对象、数组都是引用数据类型。 -- 所有引用类型的默认值都是null。 -- 一个引用变量可以用来引用任何与之兼容的类型。 -- 例子:Site site = new Site("Runoob")。 - -### Java 常量 - -常量在程序运行时是不能被修改的。 - -在 Java 中使用 final 关键字来修饰常量,声明方式和变量类似: - -``` -final double PI = 3.1415927; -``` - -虽然常量名也可以用小写,但为了便于识别,通常使用大写字母表示常量。 - -字面量可以赋给任何内置类型的变量。例如: - -``` -byte a = 68; -char a = 'A' -``` - -## 自动拆箱和装箱(详解) - -Java 5增加了自动装箱与自动拆箱机制,方便基本类型与包装类型的相互转换操作。在Java 5之前,如果要将一个int型的值转换成对应的包装器类型Integer,必须显式的使用new创建一个新的Integer对象,或者调用静态方法Integer.valueOf()。 -```` -//在Java 5之前,只能这样做 -Integer value = new Integer(10); -//或者这样做 -Integer value = Integer.valueOf(10); -//直接赋值是错误的 -//Integer value = 10;` -```` -在Java 5中,可以直接将整型赋给Integer对象,由编译器来完成从int型到Integer类型的转换,这就叫自动装箱。 -```` -//在Java 5中,直接赋值是合法的,由编译器来完成转换 -Integer value = 10; -与此对应的,自动拆箱就是可以将包装类型转换为基本类型,具体的转换工作由编译器来完成。 -//在Java 5 中可以直接这么做 -Integer value = new Integer(10); -int i = value; -```` -自动装箱与自动拆箱为程序员提供了很大的方便,而在实际的应用中,自动装箱与拆箱也是使用最广泛的特性之一。自动装箱和自动拆箱其实是Java编译器提供的一颗语法糖(语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通过可提高开发效率,增加代码可读性,增加代码的安全性)。 - -### 简易实现 - -在八种包装类型中,每一种包装类型都提供了两个方法: - -静态方法valueOf(基本类型):将给定的基本类型转换成对应的包装类型; - -实例方法xxxValue():将具体的包装类型对象转换成基本类型; -下面我们以int和Integer为例,说明Java中自动装箱与自动拆箱的实现机制。看如下代码: - -```` -class Auto //code1 -{ - public static void main(String[] args) - { - //自动装箱 - Integer inte = 10; - //自动拆箱 - int i = inte; - - //再double和Double来验证一下 - Double doub = 12.40; - double d = doub; - - } -} -```` - -上面的代码先将int型转为Integer对象,再讲Integer对象转换为int型,毫无疑问,这是可以正确运行的。可是,这种转换是怎么进行的呢?使用反编译工具,将生成的Class文件在反编译为Java文件,让我们看看发生了什么: -```` -class Auto//code2 -{ - public static void main(String[] paramArrayOfString) - { - Integer localInteger = Integer.valueOf(10); - - int i = localInteger.intValue(); - Double localDouble = Double.valueOf(12.4D); - double d = localDouble.doubleValue(); - } -} -```` -我们可以看到经过javac编译之后,code1的代码被转换成了code2,实际运行时,虚拟机运行的就是code2的代码。也就是说,虚拟机根本不知道有自动拆箱和自动装箱这回事;在将Java源文件编译为class文件的过程中,javac编译器在自动装箱的时候,调用了Integer.valueOf()方法,在自动拆箱时,又调用了intValue()方法。我们可以看到,double和Double也是如此。 -实现总结:其实自动装箱和自动封箱是编译器为我们提供的一颗语法糖。在自动装箱时,编译器调用包装类型的valueOf()方法;在自动拆箱时,编译器调用了相应的xxxValue()方法。 - -### 自动装箱与拆箱中的“坑” - -在使用自动装箱与自动拆箱时,要注意一些陷阱,为了避免这些陷阱,我们有必要去看一下各种包装类型的源码。 - -Integer源码 - -```` -public final class Integer extends Number implements Comparable { - private final int value; - - /*Integer的构造方法,接受一个整型参数,Integer对象表示的int值,保存在value中*/ - public Integer(int value) { - this.value = value; - } - - /*equals()方法判断的是:所代表的int型的值是否相等*/ - public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); - } - return false; - } - - /*返回这个Integer对象代表的int值,也就是保存在value中的值*/ - public int intValue() { - return value; - } - - /** - * 首先会判断i是否在[IntegerCache.low,Integer.high]之间 - * 如果是,直接返回Integer.cache中相应的元素 - * 否则,调用构造方法,创建一个新的Integer对象 - */ - public static Integer valueOf(int i) { - assert IntegerCache.high >= 127; - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); - } - - /** - * 静态内部类,缓存了从[low,high]对应的Integer对象 - * low -128这个值不会被改变 - * high 默认是127,可以改变,最大不超过:Integer.MAX_VALUE - (-low) -1 - * cache 保存从[low,high]对象的Integer对象 - */ - private static class IntegerCache { - static final int low = -128; - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - } - - private IntegerCache() {} - } -} -```` - - 以上是Oracle(Sun)公司JDK 1.7中Integer源码的一部分,通过分析上面的代码,得到: - 1)Integer有一个实例域value,它保存了这个Integer所代表的int型的值,且它是final的,也就是说这个Integer对象一经构造完成,它所代表的值就不能再被改变。 - 2)Integer重写了equals()方法,它通过比较两个Integer对象的value,来判断是否相等。 - 3)重点是静态内部类IntegerCache,通过类名就可以发现:它是用来缓存数据的。它有一个数组,里面保存的是连续的Integer对象。 - (a) low:代表缓存数据中最小的值,固定是-128。 - (b) high:代表缓存数据中最大的值,它可以被该改变,默认是127。high最小是127,最大是Integer.MAX_VALUE-(-low)-1,如果high超过了这个值,那么cache[ ]的长度就超过Integer.MAX_VALUE了,也就溢出了。 - (c) cache[]:里面保存着从[low,high]所对应的Integer对象,长度是high-low+1(因为有元素0,所以要加1)。 - 4)调用valueOf(int i)方法时,首先判断i是否在[low,high]之间,如果是,则复用Integer.cache[i-low]。比如,如果Integer.valueOf(3),直接返回Integer.cache[131];如果i不在这个范围,则调用构造方法,构造出一个新的Integer对象。 - 5)调用intValue(),直接返回value的值。 - 通过3)和4)可以发现,默认情况下,在使用自动装箱时,VM会复用[-128,127]之间的Integer对象。 - - Integer a1 = 1; - Integer a2 = 1; - Integer a3 = new Integer(1); - //会打印true,因为a1和a2是同一个对象,都是Integer.cache[129] - System.out.println(a1 == a2); - //false,a3构造了一个新的对象,不同于a1,a2 - System.out.println(a1 == a3); - -### 了解基本类型缓存(常量池)的最佳实践 - -```` -//基本数据类型的常量池是-128到127之间。 -// 在这个范围中的基本数据类的包装类可以自动拆箱,比较时直接比较数值大小。 -public static void main(String[] args) { - -//int的自动拆箱和装箱只在-128到127范围中进行,超过该范围的两个integer的 == 判断是会返回false的。 -Integer a1 = 128; -Integer a2 = -128; -Integer a3 = -128; -Integer a4 = 128; -System.out.println(a1 == a4); -System.out.println(a2 == a3); - -Byte b1 = 127; -Byte b2 = 127; -Byte b3 = -128; -Byte b4 = -128; -//byte都是相等的,因为范围就在-128到127之间 -System.out.println(b1 == b2); -System.out.println(b3 == b4); - -Long c1 = 128L; -Long c2 = 128L; -Long c3 = -128L; -Long c4 = -128L; -System.out.println(c1 == c2); -System.out.println(c3 == c4); - -//char没有负值 -//发现char也是在0到127之间自动拆箱 -Character d1 = 128; -Character d2 = 128; -Character d3 = 127; -Character d4 = 127; -System.out.println(d1 == d2); -System.out.println(d3 == d4); - -`结果` - -`false` -`true` -`true` -`true` -`false` -`true` -`false` -`true` - - -Integer i = 10; -Byte b = 10; -//比较Byte和Integer.两个对象无法直接比较,报错 -//System.out.println(i == b); -System.out.println("i == b " + i.equals(b)); -//答案是false,因为包装类的比较时先比较是否是同一个类,不是的话直接返回false. - -int ii = 128; -short ss = 128; -long ll = 128; -char cc = 128; -System.out.println("ii == bb " + (ii == ss)); -System.out.println("ii == ll " + (ii == ll)); -System.out.println("ii == cc " + (ii == cc)); - -结果 -i == b false -ii == bb true -ii == ll true -ii == cc true - -//这时候都是true,因为基本数据类型直接比较值,值一样就可以。 -```` - -### 总结: - -通过上面的代码,我们分析一下自动装箱与拆箱发生的时机: - -(1)当需要一个对象的时候会自动装箱,比如Integer a = 10;equals(Object o)方法的参数是Object对象,所以需要装箱。 - -(2)当需要一个基本类型时会自动拆箱,比如int a = new Integer(10);算术运算是在基本类型间进行的,所以当遇到算术运算时会自动拆箱,比如代码中的 c == (a + b); - -(3)包装类型 == 基本类型时,包装类型自动拆箱; - -需要注意的是:“==”在没遇到算术运算时,不会自动拆箱;基本类型只会自动装箱为对应的包装类型,代码中最后一条说明的内容。 - -在JDK 1.5中提供了自动装箱与自动拆箱,这其实是Java 编译器的语法糖,编译器通过调用包装类型的valueOf()方法实现自动装箱,调用xxxValue()方法自动拆箱。自动装箱和拆箱会有一些陷阱,那就是包装类型复用了某些对象。 - -(1)Integer默认复用了[-128,127]这些对象,其中高位置可以修改; - -(2)Byte复用了全部256个对象[-128,127]; - -(3)Short服用了[-128,127]这些对象; - -(4)Long服用了[-128,127]; - -(5)Character复用了[0,127],Charater不能表示负数; - -Double和Float是连续不可数的,所以没法复用对象,也就不存在自动装箱复用陷阱。 - -Boolean没有自动装箱与拆箱,它也复用了Boolean.TRUE和Boolean.FALSE,通过Boolean.valueOf(boolean b)返回的Blooean对象要么是TRUE,要么是FALSE,这点也要注意。 - -本文介绍了“真实的”自动装箱与拆箱,为了避免写出错误的代码,又从包装类型的源码入手,指出了各种包装类型在自动装箱和拆箱时存在的陷阱,同时指出了自动装箱与拆箱发生的时机。 - - -## 基本数据类型的存储方式 -上面自动拆箱和装箱的原理其实与常量池有关。 - -### 存在栈中: - - public void(int a) - { - int i = 1; - int j = 1; - } - -方法中的i 存在虚拟机栈的局部变量表里,i是一个引用,j也是一个引用,它们都指向局部变量表里的整型值 1. -int a是传值引用,所以a也会存在局部变量表。 - -### 存在堆里 -class A{ -int i = 1; -A a = new A(); -} -i是类的成员变量。类实例化的对象存在堆中,所以成员变量也存在堆中,引用a存的是对象的地址,引用i存的是值,这个值1也会存在堆中。可以理解为引用i指向了这个值1。也可以理解为i就是1. - -3 包装类对象怎么存 -其实我们说的常量池也可以叫对象池。 - -比如String a= new String("a").intern()时会先在常量池找是否有“a"对象如果有的话直接返回“a"对象在常量池的地址,即让引用a指向常量”a"对象的内存地址。 -Integer也是同理。 - -下图是Integer类型在常量池中查找同值对象的方法。 - -```` -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -private static class IntegerCache { - static final int low = -128; - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - try { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } catch( NumberFormatException nfe) { - // If the property cannot be parsed into an int, ignore it. - } - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - - // range [-128, 127] must be interned (JLS7 5.1.7) - assert IntegerCache.high >= 127; - } - - private IntegerCache() {} -} -```` - -所以基本数据类型的包装类型可以在常量池查找对应值的对象,找不到就会自动在常量池创建该值的对象。 - -而String类型可以通过intern来完成这个操作。 - -JDK1.7后,常量池被放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子: - - -``` -String s = new String("1"); -s.intern(); -String s2 = "1"; -System.out.println(s == s2); - -String s3 = new String("1") + new String("1"); -s3.intern(); -String s4 = "11"; -System.out.println(s3 == s4); -输出结果为: - -[java] view plain copy -JDK1.6以及以下:false false -JDK1.7以及以上:false true -``` - -JDK1.6查找到常量池存在相同值的对象时会直接返回该对象的地址。 - -JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。 - -那么其他字符串在常量池找值时就会返回另一个堆中对象的地址。 - -下一节详细介绍String以及相关包装类。 - -具体请见:https://blog.csdn.net/a724888/article/details/80042298 - -关于Java面向对象三大特性,请参考: - -https://blog.csdn.net/a724888/article/details/80033043 - -## 参考文章 - - - - - - - - - - - - diff --git "a/docs/Java/basic/Java\351\233\206\345\220\210\346\241\206\346\236\266\346\242\263\347\220\206.md" "b/docs/Java/basic/Java\351\233\206\345\220\210\346\241\206\346\236\266\346\242\263\347\220\206.md" deleted file mode 100644 index 16fe721..0000000 --- "a/docs/Java/basic/Java\351\233\206\345\220\210\346\241\206\346\236\266\346\242\263\347\220\206.md" +++ /dev/null @@ -1,712 +0,0 @@ -# 目录 - * [集合类大图](#集合类大图) - * [Collection接口](#collection接口) - * [List接口](#list接口) - * [Set接口](#set接口) - * [Map接口](#map接口) - * [Queue](#queue) - * [关于Java集合的小抄](#关于java集合的小抄) - * [List](#list) - * [ArrayList](#arraylist) - * [LinkedList](#linkedlist) - * [CopyOnWriteArrayList](#copyonwritearraylist) - * [遗憾](#遗憾) - * [Map](#map) - * [HashMap](#hashmap) - * [LinkedHashMap](#linkedhashmap) - * [TreeMap](#treemap) - * [EnumMap](#enummap) - * [ConcurrentHashMap](#concurrenthashmap) - * [ConcurrentSkipListMap](#concurrentskiplistmap) - * [Set](#set) - * [Queue](#queue-1) - * [普通队列](#普通队列) - * [PriorityQueue](#priorityqueue) - * [线程安全的队列](#线程安全的队列) - * [线程安全的阻塞队列](#线程安全的阻塞队列) - * [同步队列](#同步队列) - * [参考文章](#参考文章) - ---- -title: 夯实Java基础系列19:一文搞懂Java集合类框架,以及常见面试题 -date: 2019-9-19 15:56:26 # 文章生成时间,一般不改 -categories: - - Java技术江湖 - - Java基础 -tags: - - Java集合类 ---- - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - -本文参考 https://www.cnblogs.com/chenssy/p/3495238.html - -## 集合类大图 - -在编写java程序中,我们最常用的除了八种基本数据类型,String对象外还有一个集合类,在我们的的程序中到处充斥着集合类的身影! - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230403214433.png) -java中集合大家族的成员实在是太丰富了,有常用的ArrayList、HashMap、HashSet,也有不常用的Stack、Queue,有线程安全的Vector、HashTable,也有线程不安全的LinkedList、TreeMap等等! - -![](https://images0.cnblogs.com/blog/381060/201312/28124707-3a873160808e457686d67c118af6fa70.png) - -上面的图展示了整个集合大家族的成员以及他们之间的关系。下面就上面的各个接口、基类做一些简单的介绍(主要介绍各个集合的特点。区别)。 - -下面几张图更清晰地介绍了结合类接口间的关系: - -> Collections和Collection。 -> Arrays和Collections。 -> -> ![](https://www.programcreek.com/wp-content/uploads/2009/02/CollectionVsCollections.jpeg) - -> Collection的子接口 - -![](https://www.programcreek.com/wp-content/uploads/2009/02/java-collection-hierarchy.jpeg) -> map的实现类 - -![](https://www.programcreek.com/wp-content/uploads/2009/02/MapClassHierarchy-600x354.jpg) - -## Collection接口 - - Collection接口是最基本的集合接口,它不提供直接的实现,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。Collection所代表的是一种规则,它所包含的元素都必须遵循一条或者多条规则。如有些允许重复而有些则不能重复、有些必须要按照顺序插入而有些则是散列,有些支持排序但是有些则不支持。 - - 在Java中所有实现了Collection接口的类都必须提供两套标准的构造函数,一个是无参,用于创建一个空的Collection,一个是带有Collection参数的有参构造函数,用于创建一个新的Collection,这个新的Collection与传入进来的Collection具备相同的元素。 -//要求实现基本的增删改查方法,并且需要能够转换为数组类型 - -```` -public class Collection接口 { - class collect implements Collection { - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean contains(Object o) { - return false; - } - - @Override - public Iterator iterator() { - return null; - } - - @Override - public Object[] toArray() { - return new Object[0]; - } - - @Override - public boolean add(Object o) { - return false; - } - - @Override - public boolean remove(Object o) { - return false; - } - - @Override - public boolean addAll(Collection c) { - return false; - } - - @Override - public void clear() { - - } -//省略部分代码 - - @Override - public Object[] toArray(Object[] a) { - return new Object[0]; - } - } -} -```` -## List接口 - -> List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。 - -2.1、ArrayList - -> ArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。 -> -> size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。 -> -> ArrayList擅长于随机访问。同时ArrayList是非同步的。 -> -> 2.2、LinkedList - -> 同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。 -> -> 由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。 -> -> 与ArrayList一样,LinkedList也是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List: -> List list = Collections.synchronizedList(new LinkedList(...)); - -> 2.3、Vector -> 与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。 -> -> 2.4、Stack -> Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。。 -```` -public class List接口 { - //下面是List的继承关系,由于List接口规定了包括诸如索引查询,迭代器的实现,所以实现List接口的类都会有这些方法。 - //所以不管是ArrayList和LinkedList底层都可以使用数组操作,但一般不提供这样外部调用方法。 - // public interface Iterable -// public interface Collection extends Iterable -// public interface List extends Collection - class MyList implements List { - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean contains(Object o) { - return false; - } - - @Override - public Iterator iterator() { - return null; - } - - @Override - public Object[] toArray() { - return new Object[0]; - } - - @Override - public boolean add(Object o) { - return false; - } - - @Override - public boolean remove(Object o) { - return false; - } - - @Override - public void clear() { - - } - - //省略部分代码 - - @Override - public Object get(int index) { - return null; - } - - @Override - public ListIterator listIterator() { - return null; - } - - @Override - public ListIterator listIterator(int index) { - return null; - } - - @Override - public List subList(int fromIndex, int toIndex) { - return null; - } - - @Override - public Object[] toArray(Object[] a) { - return new Object[0]; - } - } -} -```` -## Set接口 - -> Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样运行null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。实现了Set接口的集合有:EnumSet、HashSet、TreeSet。 -> -> 3.1、EnumSet -> 是枚举的专用Set。所有的元素都是枚举类型。 -> -> 3.2、HashSet -> HashSet堪称查询速度最快的集合,因为其内部是以HashCode来实现的。它内部元素的顺序是由哈希码来决定的,所以它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。 - -```` -public class Set接口 { - // Set接口规定将set看成一个集合,并且使用和数组类似的增删改查方式,同时提供iterator迭代器 - // public interface Set extends Collection - // public interface Collection extends Iterable - // public interface Iterable - class MySet implements Set { - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean contains(Object o) { - return false; - } - - @Override - public Iterator iterator() { - return null; - } - - @Override - public Object[] toArray() { - return new Object[0]; - } - - @Override - public boolean add(Object o) { - return false; - } - - @Override - public boolean remove(Object o) { - return false; - } - - @Override - public boolean addAll(Collection c) { - return false; - } - - @Override - public void clear() { - - } - - @Override - public boolean removeAll(Collection c) { - return false; - } - - @Override - public boolean retainAll(Collection c) { - return false; - } - - @Override - public boolean containsAll(Collection c) { - return false; - } - - @Override - public Object[] toArray(Object[] a) { - return new Object[0]; - } - } -} -```` -## Map接口 - -> Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。实现map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。 - -> 4.1、HashMap -> 以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个hash表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。 -> -> 4.2、TreeMap -> 键以某种排序规则排序,内部以red-black(红-黑)树数据结构实现,实现了SortedMap接口 -> -> 4.3、HashTable -> 也是以哈希表数据结构实现的,解决冲突时与HashMap也一样也是采用了散列链表的形式,不过性能比HashMap要低 -```` -public class Map接口 { - //Map接口是最上层接口,Map接口实现类必须实现put和get等哈希操作。 - //并且要提供keyset和values,以及entryset等查询结构。 - //public interface Map - class MyMap implements Map { - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean containsKey(Object key) { - return false; - } - - @Override - public boolean containsValue(Object value) { - return false; - } - - @Override - public Object get(Object key) { - return null; - } - - @Override - public Object put(Object key, Object value) { - return null; - } - - @Override - public Object remove(Object key) { - return null; - } - - @Override - public void putAll(Map m) { - - } - - @Override - public void clear() { - - } - - @Override - public Set keySet() { - return null; - } - - @Override - public Collection values() { - return null; - } - - @Override - public Set entrySet() { - return null; - } - } -} -```` -## Queue - -> 队列,它主要分为两大类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。 -```` -public class Queue接口 { - //queue接口是对队列的一个实现,需要提供队列的进队出队等方法。一般使用linkedlist作为实现类 - class MyQueue implements Queue { - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean contains(Object o) { - return false; - } - - @Override - public Iterator iterator() { - return null; - } - - @Override - public Object[] toArray() { - return new Object[0]; - } - - @Override - public Object[] toArray(Object[] a) { - return new Object[0]; - } - - @Override - public boolean add(Object o) { - return false; - } - - @Override - public boolean remove(Object o) { - return false; - } - - //省略部分代码 - @Override - public boolean offer(Object o) { - return false; - } - - @Override - public Object remove() { - return null; - } - - @Override - public Object poll() { - return null; - } - - @Override - public Object element() { - return null; - } - - @Override - public Object peek() { - return null; - } - } -} - -```` -## 关于Java集合的小抄 - -这部分内容转自我偶像 江南白衣 的博客:http://calvin1978.blogcn.com/articles/collection.html -在尽可能短的篇幅里,将所有集合与并发集合的特征、实现方式、性能捋一遍。适合所有"精通Java",其实还不那么自信的人阅读。 - -期望能不止用于面试时,平时选择数据结构,也能考虑一下其成本与效率,不要看着API合适就用了。 - -### List - -#### ArrayList -以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组。因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。 - -按数组下标访问元素-get(i)、set(i,e) 的性能很高,这是数组的基本优势。 - -如果按下标插入元素、删除元素-add(i,e)、 remove(i)、remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了。 - -越是前面的元素,修改时要移动的元素越多。直接在数组末尾加入元素-常用的add(e),删除最后一个元素则无影响。 - - - -#### LinkedList -以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,每插入一个元素都要构造一个额外的Node对象,也需要额外的链表指针操作。 - - - -按下标访问元素-get(i)、set(i,e) 要悲剧的部分遍历链表将指针移动到位 (如果i>数组大小的一半,会从末尾移起)。 - -插入、删除元素时修改前后节点的指针即可,不再需要复制移动。但还是要部分遍历链表的指针才能移动到下标所指的位置。 - -只有在链表两头的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指针的移动。 - -Apache Commons 有个TreeNodeList,里面是棵二叉树,可以快速移动指针到位。 - - - -#### CopyOnWriteArrayList -并发优化的ArrayList。基于不可变对象策略,在修改时先复制出一个数组快照来修改,改好了,再让内部指针指向新数组。 - -因为对快照的修改对读操作来说不可见,所以读读之间不互斥,读写之间也不互斥,只有写写之间要加锁互斥。但复制快照的成本昂贵,典型的适合读多写少的场景。 - -虽然增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。 - - - -#### 遗憾 -无论哪种实现,按值返回下标contains(e), indexOf(e), remove(e) 都需遍历所有元素进行比较,性能可想像的不会太好。 - -没有按元素值排序的SortedList。 - -除了CopyOnWriteArrayList,再没有其他线程安全又并发优化的实现如ConcurrentLinkedList。凑合着用Set与Queue中的等价类时,会缺少一些List特有的方法如get(i)。如果更新频率较高,或数组较大时,还是得用Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全。 - -### Map -#### HashMap - - -以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。 - -插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。 - -JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。 - -在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。 - -当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。 - -取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。 - -iterator()时顺着哈希桶数组来遍历,看起来是个乱序。 - - - -#### LinkedHashMap -扩展HashMap,每个Entry增加双向链表,号称是最占内存的数据结构。 - -支持iterator()时按Entry的插入顺序来排序(如果设置accessOrder属性为true,则所有读写访问都排序)。 - -插入时,Entry把自己加到Header Entry的前面去。如果所有读写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己,所以此时读操作也是线程不安全的了。 - - - -#### TreeMap -以红黑树实现,红黑树又叫自平衡二叉树: - -对于任一节点而言,其到叶节点的每一条路径都包含相同数目的黑结点。 -上面的规定,使得树的层数不会差的太远,使得所有操作的复杂度不超过 O(lgn),但也使得插入,修改时要复杂的左旋右旋来保持树的平衡。 - -支持iterator()时按Key值排序,可按实现了Comparable接口的Key的升序排序,或由传入的Comparator控制。可想象的,在树上插入/删除元素的代价一定比HashMap的大。 - -支持SortedMap接口,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。 - - - -#### EnumMap -EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组。性能与内存占用俱佳。 - -美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断,在JMC里录得不低的采样命中频率。 - - - -#### ConcurrentHashMap -并发优化的HashMap。 - -在JDK5里的经典设计,默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。 - -也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。 - -但在JDK8里,Segment[]的设计被抛弃了,改为精心设计的,只在需要锁的时候加锁。 - -支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。 - - - -#### ConcurrentSkipListMap -JDK6新增的并发优化的SortedMap,以SkipList结构实现。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。 - -原理上,可以想象为多个链表组成的N层楼,其中的元素从稀疏到密集,每个元素有往右与往下的指针。从第一层楼开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走。 - - - - - -典型的空间换时间。每次插入,都要决定在哪几层插入,同时,要决定要不要多盖一层楼。 - -它的size()同样不能随便调,会遍历来统计。 - - - -### Set - - -所有Set几乎都是内部用一个Map来实现, 因为Map里的KeySet就是一个Set,而value是假值,全部使用同一个Object即可。 - -Set的特征也继承了那些内部的Map实现的特征。 - -HashSet:内部是HashMap。 - -LinkedHashSet:内部是LinkedHashMap。 - -TreeSet:内部是TreeMap的SortedSet。 - -ConcurrentSkipListSet:内部是ConcurrentSkipListMap的并发优化的SortedSet。 - -CopyOnWriteArraySet:内部是CopyOnWriteArrayList的并发优化的Set,利用其addIfAbsent()方法实现元素去重,如前所述该方法的性能很一般。 - -好像少了个ConcurrentHashSet,本来也该有一个内部用ConcurrentHashMap的简单实现,但JDK偏偏没提供。Jetty就自己简单封了一个,Guava则直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。 - - - - -### Queue -Queue是在两端出入的List,所以也可以用数组或链表来实现。 - -#### 普通队列 - -LinkedList -是的,以双向链表实现的LinkedList既是List,也是Queue。 - -ArrayDeque -以循环数组实现的双向Queue。大小是2的倍数,默认是16。 - -为了支持FIFO,即从数组尾压入元素(快),从数组头取出元素(超慢),就不能再使用普通ArrayList的实现了,改为使用循环数组。 - -有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,队尾下标递增。如果加入元素时已到数组空间的末尾,则将元素赋值到数组[0],同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。 - -#### PriorityQueue -用平衡二叉最小堆实现的优先级队列,不再是FIFO,而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。 - -平衡最小二叉堆,用一个简单的数组即可表达,可以快速寻址,没有指针什么的。最小的在queue[0] ,比如queue[4]的两个孩子,会在queue[2*4+1] 和 queue[2*(4+1)],即queue[9]和queue[10]。 - -入队时,插入queue[size],然后二叉地往上比较调整堆。 - -出队时,弹出queue[0],然后把queque[size]拿出来二叉地往下比较调整堆。 - -初始大小为11,空间不够时自动50%扩容。 - - - -#### 线程安全的队列 -ConcurrentLinkedQueue/Deque -无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。 - -ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法。 - -#### 线程安全的阻塞队列 -BlockingQueue,一来如果队列已空不用重复的查看是否有新数据而会阻塞在那里,二来队列的长度受限,用以保证生产者与消费者的速度不会相差太远。当入队时队列已满,或出队时队列已空,不同函数的效果见下表 - - -ArrayBlockingQueue -定长的并发优化的BlockingQueue,也是基于循环数组实现。有一把公共的锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。 - -LinkedBlockingQueue/Deque -可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE成为无界无等待的。 - -利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。 - -PriorityBlockingQueue -无界的PriorityQueue,也是基于数组存储的二叉堆(见前)。一把公共的锁实现线程安全。因为无界,空间不够时会自动扩容,所以入列时不会锁,出列为空时才会锁。 - - -DelayQueue -内部包含一个PriorityQueue,同样是无界的,同样是出列时才会锁。一把公共的锁实现线程安全。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。 - -pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。 - - - -#### 同步队列 -SynchronousQueue同步队列本身无容量,放入元素时,比如等待元素被另一条线程的消费者取走再返回。JDK线程池里用它。 - -JDK7还有个LinkedTransferQueue,在普通线程安全的BlockingQueue的基础上,增加一个transfer(e) 函数,效果与SynchronousQueue一样。 - -## 参考文章 - -https://blog.csdn.net/zzw1531439090/article/details/87872424 -https://blog.csdn.net/weixin_40374341/article/details/86496343 -https://www.cnblogs.com/uodut/p/7067162.html -https://www.jb51.net/article/135672.htm -https://www.cnblogs.com/suiyue-/p/6052456.html - diff --git "a/docs/Java/basic/final\345\205\263\351\224\256\345\255\227\347\211\271\346\200\247.md" "b/docs/Java/basic/final\345\205\263\351\224\256\345\255\227\347\211\271\346\200\247.md" deleted file mode 100644 index 6b28474..0000000 --- "a/docs/Java/basic/final\345\205\263\351\224\256\345\255\227\347\211\271\346\200\247.md" +++ /dev/null @@ -1,564 +0,0 @@ -# 目录 - - * [final使用](#final使用) - * [final变量](#final变量) - * [final修饰基本数据类型变量和引用](#final修饰基本数据类型变量和引用) - * [final类](#final类) - * [final关键字的知识点](#final关键字的知识点) - * [final关键字的最佳实践](#final关键字的最佳实践) - * [final的用法](#final的用法) - * [关于空白final](#关于空白final) - * [final内存分配](#final内存分配) - * [使用final修饰方法会提高速度和效率吗](#使用final修饰方法会提高速度和效率吗) - * [使用final修饰变量会让变量的值不能被改变吗;](#使用final修饰变量会让变量的值不能被改变吗;) - * [如何保证数组内部不被修改](#如何保证数组内部不被修改) - * [final方法的三条规则](#final方法的三条规则) - * [final 和 jvm的关系](#final-和-jvm的关系) - * [写 final 域的重排序规则](#写-final-域的重排序规则) - * [读 final 域的重排序规则](#读-final-域的重排序规则) - * [如果 final 域是引用类型](#如果-final-域是引用类型) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -final关键字在java中使用非常广泛,可以申明成员变量、方法、类、本地变量。一旦将引用声明为final,将无法再改变这个引用。final关键字还能保证内存同步,本博客将会从final关键字的特性到从java内存层面保证同步讲解。这个内容在面试中也有可能会出现。 - -## final使用 - -### final变量 - -final变量有成员变量或者是本地变量(方法内的局部变量),在类成员中final经常和static一起使用,作为类常量使用。**其中类常量必须在声明时初始化,final成员常量可以在构造函数初始化。** - -``` -public class Main { - public static final int i; //报错,必须初始化 因为常量在常量池中就存在了,调用时不需要类的初始化,所以必须在声明时初始化 - public static final int j; - Main() { - i = 2; - j = 3; - } -} - -``` - -就如上所说的,对于类常量,JVM会缓存在常量池中,在读取该变量时不会加载这个类。 - -``` - -public class Main { - public static final int i = 2; - Main() { - System.out.println("调用构造函数"); // 该方法不会调用 - } - public static void main(String[] args) { - System.out.println(Main.i); - } -} - -``` -### final修饰基本数据类型变量和引用 - @Test - public void final修饰基本类型变量和引用() { - final int a = 1; - final int[] b = {1}; - final int[] c = {1}; - // b = c;报错 - b[0] = 1; - final String aa = "a"; - final Fi f = new Fi(); - //aa = "b";报错 - // f = null;//报错 - f.a = 1; - } - -final方法表示该方法不能被子类的方法重写,将方法声明为final,在编译的时候就已经静态绑定了,不需要在运行时动态绑定。final方法调用时使用的是invokespecial指令。 - -``` -class PersonalLoan{ - public final String getName(){ - return"personal loan”; - } -} - -class CheapPersonalLoan extends PersonalLoan{ - @Override - public final String getName(){ - return"cheap personal loan";//编译错误,无法被重载 - } - - public String test() { - return getName(); //可以调用,因为是public方法 - } -} - -``` - -### final类 - -final类不能被继承,final类中的方法默认也会是final类型的,java中的String类和Integer类都是final类型的。 - - class Si{ - //一般情况下final修饰的变量一定要被初始化。 - //只有下面这种情况例外,要求该变量必须在构造方法中被初始化。 - //并且不能有空参数的构造方法。 - //这样就可以让每个实例都有一个不同的变量,并且这个变量在每个实例中只会被初始化一次 - //于是这个变量在单个实例里就是常量了。 - final int s ; - Si(int s) { - this.s = s; - } - } - class Bi { - final int a = 1; - final void go() { - //final修饰方法无法被继承 - } - } - class Ci extends Bi { - final int a = 1; - // void go() { - // //final修饰方法无法被继承 - // } - } - final char[]a = {'a'}; - final int[]b = {1}; - -``` -final class PersonalLoan{} - -class CheapPersonalLoan extends PersonalLoan { //编译错误,无法被继承 -} - -``` - - @Test - public void final修饰类() { - //引用没有被final修饰,所以是可变的。 - //final只修饰了Fi类型,即Fi实例化的对象在堆中内存地址是不可变的。 - //虽然内存地址不可变,但是可以对内部的数据做改变。 - Fi f = new Fi(); - f.a = 1; - System.out.println(f); - f.a = 2; - System.out.println(f); - //改变实例中的值并不改变内存地址。 - - Fi ff = f; - //让引用指向新的Fi对象,原来的f对象由新的引用ff持有。 - //引用的指向改变也不会改变原来对象的地址 - f = new Fi(); - System.out.println(f); - System.out.println(ff); - } - -### final关键字的知识点 - -1. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。final变量一旦被初始化后不能再次赋值。 -2. 本地变量必须在声明时赋值。 因为没有初始化的过程 -3. 在匿名类中所有变量都必须是final变量。 -4. final方法不能被重写, final类不能被继承 -5. 接口中声明的所有变量本身是final的。类似于匿名类 -6. final和abstract这两个关键字是反相关的,final类就不可能是abstract的。 -7. final方法在编译阶段绑定,称为静态绑定(static binding)。 -8. 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。 - -final方法的好处: - -1. 提高了性能,JVM在常量池中会缓存final变量 -2. final变量在多线程中并发安全,无需额外的同步开销 -3. final方法是静态编译的,提高了调用速度 -4. **final类创建的对象是只可读的,在多线程可以安全共享** - -## final关键字的最佳实践 - -### final的用法 -1、final 对于常量来说,意味着值不能改变,例如 final int i=100。这个i的值永远都是100。 -但是对于变量来说又不一样,只是标识这个引用不可被改变,例如 final File f=new File("c:\\test.txt"); - -那么这个f一定是不能被改变的,如果f本身有方法修改其中的成员变量,例如是否可读,是允许修改的。有个形象的比喻:一个女子定义了一个final的老公,这个老公的职业和收入都是允许改变的,只是这个女人不会换老公而已。 - -### 关于空白final -final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。 - 另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。 -  -但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。 -```` - public class FinalTest { - final int p; - final int q=3; - FinalTest(){ - p=1; - } - FinalTest(int i){ - p=i;//可以赋值,相当于直接定义p - q=i;//不能为一个final变量赋值 - } - } -```` -### final内存分配 -刚提到了内嵌机制,现在详细展开。 -要知道调用一个函数除了函数本身的执行时间之外,还需要额外的时间去寻找这个函数(类内部有一个函数签名和函数地址的映射表)。所以减少函数调用次数就等于降低了性能消耗。 - -final修饰的函数会被编译器优化,优化的结果是减少了函数调用的次数。如何实现的,举个例子给你看: -```` - public class Test{ - final void func(){System.out.println("g");}; - public void main(String[] args){ - for(int j=0;j<1000;j++) - func(); - }} - - 经过编译器优化之后,这个类变成了相当于这样写: - public class Test{ - final void func(){System.out.println("g");}; - public void main(String[] args){ - for(int j=0;j<1000;j++) - {System.out.println("g");} - }} -```` -看出来区别了吧?编译器直接将func的函数体内嵌到了调用函数的地方,这样的结果是节省了1000次函数调用,当然编译器处理成字节码,只是我们可以想象成这样,看个明白。 - -不过,当函数体太长的话,用final可能适得其反,因为经过编译器内嵌之后代码长度大大增加,于是就增加了jvm解释字节码的时间。 - -在使用final修饰方法的时候,编译器会将被final修饰过的方法插入到调用者代码处,提高运行速度和效率,但被final修饰的方法体不能过大,编译器可能会放弃内联,但究竟多大的方法会放弃,我还没有做测试来计算过。 - -**下面这些内容是通过两个疑问来继续阐述的** - -### 使用final修饰方法会提高速度和效率吗 - -见下面的测试代码,我会执行五次: - -```` - public class Test - { - public static void getJava() - { - String str1 = "Java "; - String str2 = "final "; - for (int i = 0; i < 10000; i++) - { - str1 += str2; - } - } - public static final void getJava_Final() - { - String str1 = "Java "; - String str2 = "final "; - for (int i = 0; i < 10000; i++) - { - str1 += str2; - } - } - public static void main(String[] args) - { - long start = System.currentTimeMillis(); - getJava(); - System.out.println("调用不带final修饰的方法执行时间为:" + (System.currentTimeMillis() - start) + "毫秒时间"); - start = System.currentTimeMillis(); - String str1 = "Java "; - String str2 = "final "; - for (int i = 0; i < 10000; i++) - { - str1 += str2; - } - System.out.println("正常的执行时间为:" + (System.currentTimeMillis() - start) + "毫秒时间"); - start = System.currentTimeMillis(); - getJava_Final(); - System.out.println("调用final修饰的方法执行时间为:" + (System.currentTimeMillis() - start) + "毫秒时间"); - } - } - -```` - - 结果为: - 第一次: - 调用不带final修饰的方法执行时间为:1732毫秒时间 - 正常的执行时间为:1498毫秒时间 - 调用final修饰的方法执行时间为:1593毫秒时间 - 第二次: - 调用不带final修饰的方法执行时间为:1217毫秒时间 - 正常的执行时间为:1031毫秒时间 - 调用final修饰的方法执行时间为:1124毫秒时间 - 第三次: - 调用不带final修饰的方法执行时间为:1154毫秒时间 - 正常的执行时间为:1140毫秒时间 - 调用final修饰的方法执行时间为:1202毫秒时间 - 第四次: - 调用不带final修饰的方法执行时间为:1139毫秒时间 - 正常的执行时间为:999毫秒时间 - 调用final修饰的方法执行时间为:1092毫秒时间 - 第五次: - 调用不带final修饰的方法执行时间为:1186毫秒时间 - 正常的执行时间为:1030毫秒时间 - 调用final修饰的方法执行时间为:1109毫秒时间 - - 由以上运行结果不难看出,执行最快的是“正常的执行”即代码直接编写,而使用final修饰的方法,不像有些书上或者文章上所说的那样,速度与效率与“正常的执行”无异,而是位于第二位,最差的是调用不加final修饰的方法。 - - 观点:加了比不加好一点。 - - -### 使用final修饰变量会让变量的值不能被改变吗; -见代码: - -```` - public class Final - { - public static void main(String[] args) - { - Color.color[3] = "white"; - for (String color : Color.color) - System.out.print(color+" "); - } - } - - class Color - { - public static final String[] color = { "red", "blue", "yellow", "black" }; - } - - - 执行结果: - red blue yellow white - 看!,黑色变成了白色。 - -```` - - 在使用findbugs插件时,就会提示public static String[] color = { "red", "blue", "yellow", "black" };这行代码不安全,但加上final修饰,这行代码仍然是不安全的,因为final没有做到保证变量的值不会被修改! - - 原因是:final关键字只能保证变量本身不能被赋与新值,而不能保证变量的内部结构不被修改。例如在main方法有如下代码Color.color = new String[]{""};就会报错了。 - -### 如何保证数组内部不被修改 - - 那可能有的同学就会问了,加上final关键字不能保证数组不会被外部修改,那有什么方法能够保证呢?答案就是降低访问级别,把数组设为private。这样的话,就解决了数组在外部被修改的不安全性,但也产生了另一个问题,那就是这个数组要被外部使用的。 - -解决这个问题见代码: -```` - import java.util.AbstractList; - import java.util.List; - - public class Final - { - public static void main(String[] args) - { - for (String color : Color.color) - System.out.print(color + " "); - Color.color.set(3, "white"); - } - } - - class Color - { - private static String[] _color = { "red", "blue", "yellow", "black" }; - public static List color = new AbstractList() - { - @Override - public String get(int index) - { - return _color[index]; - } - @Override - public String set(int index, String value) - { - throw new RuntimeException("为了代码安全,不能修改数组"); - } - @Override - public int size() - { - return _color.length; - } - }; - - - } -```` -这样就OK了,既保证了代码安全,又能让数组中的元素被访问了。 - - -### final方法的三条规则 - -规则1:final修饰的方法不可以被重写。 - -规则2:final修饰的方法仅仅是不能重写,但它完全可以被重载。 - -规则3:父类中private final方法,子类可以重新定义,这种情况不是重写。 - -代码示例 -```` - 规则1代码 - - public class FinalMethodTest - { - public final void test(){} - } - class Sub extends FinalMethodTest - { - // 下面方法定义将出现编译错误,不能重写final方法 - public void test(){} - } - - 规则2代码 - - public class Finaloverload { - //final 修饰的方法只是不能重写,完全可以重载 - public final void test(){} - public final void test(String arg){} - } - - 规则3代码 - - public class PrivateFinalMethodTest - { - private final void test(){} - } - class Sub extends PrivateFinalMethodTest - { - // 下面方法定义将不会出现问题 - public void test(){} - } -```` - -## final 和 jvm的关系 - -与前面介绍的锁和 volatile 相比较,对 final 域的读和写更像是普通的变量访问。对于 final 域,编译器和处理器要遵守两个重排序规则: - -1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 -2. 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。 - -下面,我们通过一些示例性的代码来分别说明这两个规则: -```` -public class FinalExample { - int i; // 普通变量 - final int j; //final 变量 - static FinalExample obj; - - public void FinalExample () { // 构造函数 - i = 1; // 写普通域 - j = 2; // 写 final 域 - } - - public static void writer () { // 写线程 A 执行 - obj = new FinalExample (); - } - - public static void reader () { // 读线程 B 执行 - FinalExample object = obj; // 读对象引用 - int a = object.i; // 读普通域 - int b = object.j; // 读 final 域 - } -} -```` - -这里假设一个线程 A 执行 writer () 方法,随后另一个线程 B 执行 reader () 方法。下面我们通过这两个线程的交互来说明这两个规则。 - -### 写 final 域的重排序规则 - -写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面 2 个方面: - -* JMM 禁止编译器把 final 域的写重排序到构造函数之外。 -* 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。 - -现在让我们分析 writer () 方法。writer () 方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤: - -1. 构造一个 FinalExample 类型的对象; -2. 把这个对象的引用赋值给引用变量 obj。 - -假设线程 B 读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序: - -![img](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/6628576a54f0ba625c8c3af4586cef3a.jpg) - -在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程 B 错误的读取了普通变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。 - -写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B“看到”对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 1 还没有写入普通域 i)。 - -### 读 final 域的重排序规则 - -读 final 域的重排序规则如下: - -* 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。 - -初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。 - -reader() 方法包含三个操作: - -1. 初次读引用变量 obj; -2. 初次读引用变量 obj 指向对象的普通域 i。 -3. 初次读引用变量 obj 指向对象的 final 域 j。 - -现在我们假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序: - -![](https://static001.infoq.cn/resource/image/a0/36/a0a9b023bc56ab97bbda8812cdca7236.png) - -在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程 A 写入,这是一个错误的读取操作。而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。 - -读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。在这个示例程序中,如果该引用不为 null,那么引用对象的 final 域一定已经被 A 线程初始化过了。 - -### 如果 final 域是引用类型 - -上面我们看到的 final 域是基础数据类型,下面让我们看看如果 final 域是引用类型,将会有什么效果? - -请看下列示例代码: -```` -public class FinalReferenceExample { -final int[] intArray; //final 是引用类型 -static FinalReferenceExample obj; - -public FinalReferenceExample () { // 构造函数 - intArray = new int[1]; //1 - intArray[0] = 1; //2 -} - -public static void writerOne () { // 写线程 A 执行 - obj = new FinalReferenceExample (); //3 -} - -public static void writerTwo () { // 写线程 B 执行 - obj.intArray[0] = 2; //4 -} - -public static void reader () { // 读线程 C 执行 - if (obj != null) { //5 - int temp1 = obj.intArray[0]; //6 - } -} -} -```` - -这里 final 域为一个引用类型,它引用一个 int 型的数组对象。对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束: - -1. 在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 - -对上面的示例程序,我们假设首先线程 A 执行 writerOne() 方法,执行完后线程 B 执行 writerTwo() 方法,执行完后线程 C 执行 reader () 方法。下面是一种可能的线程执行时序: - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/29b097c36fd531028991826bb7c835db.png) -在上图中,1 是对 final 域的写入,2 是对这个 final 域引用的对象的成员域的写入,3 是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。 - -JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看的到,也可能看不到。JMM 不保证线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行结果不可预知。 - -如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间需要使用同步原语(lock 或 volatile)来确保内存可见性。 - - -## 参考文章 - -https://www.infoq.cn/article/java-memory-model-6 -https://www.jianshu.com/p/067b6c89875a -https://www.jianshu.com/p/f68d6ef2dcf0 -https://www.cnblogs.com/xiaoxi/p/6392154.html -https://www.iteye.com/blog/cakin24-2334965 -https://blog.csdn.net/chengqiuming/article/details/70139503 -https://blog.csdn.net/hupuxiang/article/details/7362267 - - diff --git "a/docs/Java/basic/javac\345\222\214javap.md" "b/docs/Java/basic/javac\345\222\214javap.md" deleted file mode 100644 index f797af7..0000000 --- "a/docs/Java/basic/javac\345\222\214javap.md" +++ /dev/null @@ -1,893 +0,0 @@ -# 目录 - * [聊聊IDE的实现原理](#聊聊ide的实现原理) - * [源代码保存](#源代码保存) - * [编译为class文件](#编译为class文件) - * [查找class](#查找class) - * [生成对象,并调用对象方法](#生成对象,并调用对象方法) - * [javac命令初窥](#javac命令初窥) - * [classpath是什么](#classpath是什么) - * [IDE中的classpath](#ide中的classpath) - * [Java项目和Java web项目的本质区别](#java项目和java-web项目的本质区别) - * [javac命令后缀](#javac命令后缀) - * [-g、-g:none、-g:{lines,vars,source}](#-g、-gnone、-g{linesvarssource}) - * [-bootclasspath、-extdirs](#-bootclasspath、-extdirs) - * [-sourcepath和-classpath(-cp)](#-sourcepath和-classpath(-cp)) - * [-d](#-d) - * [-implicit:{none,class}](#-implicit{noneclass}) - * [-source和-target](#-source和-target) - * [-encoding](#-encoding) - * [-verbose](#-verbose) - * [其他命令](#其他命令) - * [使用javac构建项目](#使用javac构建项目) - * [javap 的使用](#javap-的使用) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## 聊聊IDE的实现原理 - -> IDE是把双刃剑,它可以什么都帮你做了,你只要敲几行代码,点几下鼠标,程序就跑起来了,用起来相当方便。 -> -> 你不用去关心它后面做了些什么,执行了哪些命令,基于什么原理。然而也是这种过分的依赖往往让人散失了最基本的技能,当到了一个没有IDE的地方,你便觉得无从下手,给你个代码都不知道怎么去跑。好比给你瓶水,你不知道怎么打开去喝,然后活活给渴死。 -> -> 之前用惯了idea,Java文件编译运行的命令基本忘得一干二净。 - -那好,不如咱们先来了解一下IDE的实现原理,这样一来,即使离开IDE,我们还是知道如何运行Java程序了。 - -像Eclipse等java IDE是怎么编译和查找java源代码的呢? - -### 源代码保存 -这个无需多说,在编译器写入代码,并保存到文件。这个利用流来实现。 - -### 编译为class文件 -java提供了JavaCompiler,我们可以通过它来编译java源文件为class文件。 - -### 查找class -可以通过Class.forName(fullClassPath)或自定义类加载器来实现。 - -### 生成对象,并调用对象方法 -通过上面一个查找class,得到Class对象后,可以通过newInstance()或构造器的newInstance()得到对象。然后得到Method,最后调用方法,传入相关参数即可。 - -示例代码: -```` -public class MyIDE { - - public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - // 定义java代码,并保存到文件(Test.java) - StringBuilder sb = new StringBuilder(); - sb.append("package com.tommy.core.test.reflect;\n"); - sb.append("public class Test {\n"); - sb.append(" private String name;\n"); - sb.append(" public Test(String name){\n"); - sb.append(" this.name = name;\n"); - sb.append(" System.out.println(\"hello,my name is \" + name);\n"); - sb.append(" }\n"); - sb.append(" public String sayHello(String name) {\n"); - sb.append(" return \"hello,\" + name;\n"); - sb.append(" }\n"); - sb.append("}\n"); - - System.out.println(sb.toString()); - - String baseOutputDir = "F:\\output\\classes\\"; - String baseDir = baseOutputDir + "com\\tommy\\core\\test\\reflect\\"; - String targetJavaOutputPath = baseDir + "Test.java"; - // 保存为java文件 - FileWriter fileWriter = new FileWriter(targetJavaOutputPath); - fileWriter.write(sb.toString()); - fileWriter.flush(); - fileWriter.close(); - - // 编译为class文件 - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - StandardJavaFileManager manager = compiler.getStandardFileManager(null,null,null); - List files = new ArrayList<>(); - files.add(new File(targetJavaOutputPath)); - Iterable compilationUnits = manager.getJavaFileObjectsFromFiles(files); - - // 编译 - // 设置编译选项,配置class文件输出路径 - Iterable options = Arrays.asList("-d",baseOutputDir); - JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, options, null, compilationUnits); - // 执行编译任务 - task.call(); - - - - // 通过反射得到对象 -// Class clazz = Class.forName("com.tommy.core.test.reflect.Test"); - // 使用自定义的类加载器加载class - Class clazz = new MyClassLoader(baseOutputDir).loadClass("com.tommy.core.test.reflect.Test"); - // 得到构造器 - Constructor constructor = clazz.getConstructor(String.class); - // 通过构造器new一个对象 - Object test = constructor.newInstance("jack.tsing"); - // 得到sayHello方法 - Method method = clazz.getMethod("sayHello", String.class); - // 调用sayHello方法 - String result = (String) method.invoke(test, "jack.ma"); - System.out.println(result); - } -} -```` -自定义类加载器代码: - -```` - -public class MyClassLoader extends ClassLoader { - private String baseDir; - public MyClassLoader(String baseDir) { - this.baseDir = baseDir; - } - @Override - protected Class findClass(String name) throws ClassNotFoundException { - String fullClassFilePath = this.baseDir + name.replace("\\.","/") + ".class"; - File classFilePath = new File(fullClassFilePath); - if (classFilePath.exists()) { - FileInputStream fileInputStream = null; - ByteArrayOutputStream byteArrayOutputStream = null; - try { - fileInputStream = new FileInputStream(classFilePath); - byte[] data = new byte[1024]; - int len = -1; - byteArrayOutputStream = new ByteArrayOutputStream(); - while ((len = fileInputStream.read(data)) != -1) { - byteArrayOutputStream.write(data,0,len); - } - - return defineClass(name,byteArrayOutputStream.toByteArray(),0,byteArrayOutputStream.size()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (null != fileInputStream) { - try { - fileInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - if (null != byteArrayOutputStream) { - try { - byteArrayOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - return super.findClass(name); - } -} -```` -## javac命令初窥 - -注:以下红色标记的参数在下文中有所讲解。 - -本部分参考https://www.cnblogs.com/xiazdong/p/3216220.html - -用法: javac - -其中, 可能的选项包括: - -> -g 生成所有调试信息 -> -> -g:none 不生成任何调试信息 -> -> -g:{lines,vars,source} 只生成某些调试信息 -> -> -nowarn 不生成任何警告 -> -> -verbose 输出有关编译器正在执行的操作的消息 -> -> -deprecation 输出使用已过时的 API 的源位置 -> -> -classpath <路径> 指定查找用户类文件和注释处理程序的位置 -> -> -cp <路径> 指定查找用户类文件和注释处理程序的位置 -> -> -sourcepath <路径> 指定查找输入源文件的位置 -> -> -bootclasspath <路径> 覆盖引导类文件的位置 -> -> -extdirs <目录> 覆盖所安装扩展的位置 -> -> -endorseddirs <目录> 覆盖签名的标准路径的位置 -> -> -proc:{none,only} 控制是否执行注释处理和/或编译。 -> -> -processor [,,...] 要运行的注释处理程序的名称; 绕过默认的搜索进程 -> -> -processorpath <路径> 指定查找注释处理程序的位置 -> -> -d <目录> 指定放置生成的类文件的位置 -> -> -s <目录> 指定放置生成的源文件的位置 -> -> -implicit:{none,class} 指定是否为隐式引用文件生成类文件 -> -> -encoding <编码> 指定源文件使用的字符编码 -> -> -source <发行版> 提供与指定发行版的源兼容性 -> -> -target <发行版> 生成特定 VM 版本的类文件 -> -> -version 版本信息 -> -> -help 输出标准选项的提要 -> -> -A关键字[=值] 传递给注释处理程序的选项 -> -> -X 输出非标准选项的提要 -> -> -J<标记> 直接将 <标记> 传递给运行时系统 -> -> -Werror 出现警告时终止编译 -> -> @<文件名> 从文件读取选项和文件名 - - -在详细介绍javac命令之前,先看看这个classpath是什么 - - -### classpath是什么 - -在dos下编译java程序,就要用到classpath这个概念,尤其是在没有设置环境变量的时候。classpath就是存放.class等编译后文件的路径。 - -javac:如果当前你要编译的java文件中引用了其它的类(比如说:继承),但该引用类的.class文件不在当前目录下,这种情况下就需要在javac命令后面加上-classpath参数,通过使用以下三种类型的方法 来指导编译器在编译的时候去指定的路径下查找引用类。 - -> (1).绝对路径:javac -classpath c:/junit3.8.1/junit.jar Xxx.java -> -> (2).相对路径:javac -classpath ../junit3.8.1/Junit.javr Xxx.java -> -> (3).系统变量:javac -classpath %CLASSPATH% Xxx.java (注意:%CLASSPATH%表示使用系统变量CLASSPATH的值进行查找,这里假设Junit.jar的路径就包含在CLASSPATH系统变量中) - - -#### IDE中的classpath - -对于一个普通的Javaweb项目,一般有这样的配置: - -> 1 WEB-INF/classes,lib才是classpath,WEB-INF/ 是资源目录, 客户端不能直接访问。 -> -> 2、WEB-INF/classes目录存放src目录java文件编译之后的class文件,xml、properties等资源配置文件,这是一个定位资源的入口。 -> -> 3、引用classpath路径下的文件,只需在文件名前加classpath: -> -> classpath:applicationContext-*.xml -> -> classpath:context/conf/controller.xml -> -> 4、lib和classes同属classpath,两者的访问优先级为: lib>classes。 -> -> 5、classpath 和 classpath* 区别: -> -> classpath:只会到你的class路径中查找找文件; -> classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找。 - -总结: - -(1).何时需要使用-classpath:当你要编译或执行的类引用了其它的类,但被引用类的.class文件不在当前目录下时,就需要通过-classpath来引入类 - -(2).何时需要指定路径:当你要编译的类所在的目录和你执行javac命令的目录不是同一个目录时,就需要指定源文件的路径(CLASSPATH是用来指定.class路径的,不是用来指定.java文件的路径的) -#### Java项目和Java web项目的本质区别 - -(看清IDE及classpath本质) - -> 现在只是说说Java Project和Web Project,那么二者有区别么?回答:没有!都是Java语言的应用,只是应用场合不同罢了,那么他们的本质到底是什么? - -> 回答:编译后路径!虚拟机执行的是class文件而不是java文件,那么我们不管是何种项目都是写的java文件,怎么就不一样了呢?分成java和web两种了呢? - -> 从.classpath文件入手来看,这个文件在每个项目目录下都是存在的,很少有人打开看吧,那么我们就来一起看吧。这是一个XML文件,使用文本编辑器打开即可。 -> -> 这里展示一个web项目的.classpath - -Xml代码 -```` - - - - - - - - - …… - - -```` -> XML文档包含一个根元素,就是classpath,类路径,那么这里面包含了什么信息呢?子元素是classpathentry,kind属性区别了种 类信息,src源码,con你看看后面的path就知道是JRE容器的信息。lib是项目依赖的第三方类库,output是src编译后的位置。 - -> 既然是web项目,那么就是WEB-INF/classes目录,可能用MyEclipse的同学会说他们那里是WebRoot或者是WebContext而不是webapp,有区别么?回答:完全没有! - -> 既然看到了编译路径的本来面目后,还区分什么java项目和web项目么?回答:不区分!普通的java 项目你这样写就行了:,看看Eclipse是不是这样生成的?这个问题解决了吧。 - -> 再说说webapp目录命名的问题,这个无所谓啊,web项目是要发布到服务器上的对吧,那么服务器读取的是类文件和页面文件吧,它不管源文件,它也无法去理解源文件。那么webapp目录的命名有何关系呢?只要让服务器找到不就行了。 - -### javac命令后缀 - -#### -g、-g:none、-g:{lines,vars,source} - -> •-g:在生成的class文件中包含所有调试信息(行号、变量、源文件) -> •-g:none :在生成的class文件中不包含任何调试信息。 -> -> 这个参数在javac编译中是看不到什么作用的,因为调试信息都在class文件中,而我们看不懂这个class文件。 -> -> 为了看出这个参数的作用,我们在eclipse中进行实验。在eclipse中,我们经常做的事就是“debug”,而在debug的时候,我们会 -> •加入“断点”,这个是靠-g:lines起作用,如果不记录行号,则不能加断点。 -> •在“variables”窗口中查看当前的变量,如下图所示,这是靠-g:vars起作用,否则不能查看变量信息。 -> •在多个文件之间来回调用,比如 A.java的main()方法中调用了B.java的fun()函数,而我想看看程序进入fun()后的状态,这是靠-g:source,如果没有这个参数,则不能查看B.java的源代码。 - -#### -bootclasspath、-extdirs - -> -bootclasspath和-extdirs 几乎不需要用的,因为他是用来改变 “引导类”和“扩展类”。 -> •引导类(组成Java平台的类):Java\jdk1.7.0_25\jre\lib\rt.jar等,用-bootclasspath设置。 -> •扩展类:Java\jdk1.7.0_25\jre\lib\ext目录中的文件,用-extdirs设置。 -> •用户自定义类:用-classpath设置。 -> -> 我们用-verbose编译后出现的“类文件的搜索路径”,就是由上面三个路径组成,如下: - - - [类文件的搜索路径: C:\Java\jdk1.7.0_25\jre\lib\resources.jar,C:\Java\jdk1.7.0_25 - - \jre\lib\rt.jar,C:\Java\jdk1.7.0_25\jre\lib\sunrsasign.jar,C:\Java\jdk1.7.0_25\j - - re\lib\jsse.jar,C:\Java\jdk1.7.0_25\jre\lib\jce.jar,C:\Java\jdk1.7.0_25\jre\lib\ - - charsets.jar,C:\Java\jdk1.7.0_25\jre\lib\jfr.jar,C:\Java\jdk1.7.0_25\jre\classes - - ,C:\Java\jdk1.7.0_25\jre\lib\ext\access-bridge-32.jar,C:\Java\jdk1.7.0_25\jre\li - - b\ext\dnsns.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\jaccess.jar,C:\Java\jdk1.7.0_25\ - - jre\lib\ext\localedata.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunec.jar,C:\Java\jdk - - 1.7.0_25\jre\lib\ext\sunjce_provider.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunmsca - - pi.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunpkcs11.jar,C:\Java\jdk1.7.0_25\jre\lib - \ext\zipfs.jar,..\bin] - -如果利用 -bootclasspath 重新定义: javac -bootclasspath src Xxx.java,则会出现下面错误: - - -致命错误: 在类路径或引导类路径中找不到程序包 java.lang - -#### -sourcepath和-classpath(-cp) - -•-classpath(-cp)指定你依赖的类的class文件的查找位置。在Linux中,用“:”分隔classpath,而在windows中,用“;”分隔。 -•-sourcepath指定你依赖的类的java文件的查找位置。 - -举个例子, - - -```` -public class A -{ - public static void main(String[] args) { - B b = new B(); - b.print(); - } -} - - - - -public class B -{ - public void print() - { - System.out.println("old"); - } -} -```` - -目录结构如下: - - -sourcepath //此处为当前目录 - -``` -|-src -    |-com -      |- B.java -    |- A.java -  |-bin -    |- B.class //是 B.java -``` - 编译后的类文件 - -如果要编译 A.java,则必须要让编译器找到类B的位置,你可以指定B.class的位置,也可以是B.java的位置,也可以同时都存在。 - - - javac -classpath bin src/A.java //查找到B.class - - javac -sourcepath src/com src/A.java //查找到B.java - - javac -sourcepath src/com -classpath bin src/A.java //同时查找到B.class和B.java - -如果同时找到了B.class和B.java,则: -•如果B.class和B.java内容一致,则遵循B.class。 -•如果B.class和B.java内容不一致,则遵循B.java,并编译B.java。 - -以上规则可以通过 -verbose选项看出。 - -#### -d - -•d就是 destination,用于指定.class文件的生成目录,在eclipse中,源文件都在src中,编译的class文件都是在bin目录中。 - -这里我用来实现一下这个功能,假设项目名称为project,此目录为当前目录,且在src/com目录中有一个Main.java文件。‘ - - -```` - package com; - public class Main - { - public static void main(String[] args) { - System.out.println("Hello"); - } - } - - -```` - -javac -d bin src/com/Main.java - -上面的语句将Main.class生成在bin/com目录下。 - -#### -implicit:{none,class} - -•如果有文件为A.java(其中有类A),且在类A中使用了类B,类B在B.java中,则编译A.java时,默认会自动编译B.java,且生成B.class。 -•implicit:none:不自动生成隐式引用的类文件。 -•implicit:class(默认):自动生成隐式引用的类文件。 -```` -public class A -{ - public static void main(String[] args) { - B b = new B(); - } -} - -public class B -{ -} -```` -如果使用: - - - -javac -implicit:none A.java - -则不会生成 B.class。 - -#### -source和-target - -•-source:使用指定版本的JDK编译,比如:-source 1.4表示用JDK1.4的标准编译,如果在源文件中使用了泛型,则用JDK1.4是不能编译通过的。 -•-target:指定生成的class文件要运行在哪个JVM版本,以后实际运行的JVM版本必须要高于这个指定的版本。 - - -javac -source 1.4 Xxx.java - -javac -target 1.4 Xxx.java - -#### -encoding - -默认会使用系统环境的编码,比如我们一般用的中文windows就是GBK编码,所以直接javac时会用GBK编码,而Java文件一般要使用utf-8,如果用GBK就会出现乱码。 - -•指定源文件的编码格式,如果源文件是UTF-8编码的,而-encoding GBK,则源文件就变成了乱码(特别是有中文时)。 - - -javac -encoding UTF-8 Xxx.java - -#### -verbose - -输出详细的编译信息,包括:classpath、加载的类文件信息。 - -比如,我写了一个最简单的HelloWorld程序,在命令行中输入: - - -D:\Java>javac -verbose -encoding UTF-8 HelloWorld01.java - -输出: - - - [语法分析开始时间 RegularFileObject[HelloWorld01.java]] - [语法分析已完成, 用时 21 毫秒] - [源文件的搜索路径: .,D:\大三下\编译原理\cup\java-cup-11a.jar,E:\java\jflex\lib\J //-sourcepath - Flex.jar] - [类文件的搜索路径: C:\Java\jdk1.7.0_25\jre\lib\resources.jar,C:\Java\jdk1.7.0_25 //-classpath、-bootclasspath、-extdirs - 省略............................................ - [正在加载ZipFileIndexFileObject[C:\Java\jdk1.7.0_25\lib\ct.sym(META-INF/sym/rt.j - ar/java/lang/Object.class)]] - [正在加载ZipFileIndexFileObject[C:\Java\jdk1.7.0_25\lib\ct.sym(META-INF/sym/rt.j - ar/java/lang/String.class)]] - [正在检查Demo] - 省略............................................ - [已写入RegularFileObject[Demo.class]] - [共 447 毫秒] - -编写一个程序时,比如写了一句:System.out.println("hello"),实际上还需要加载:Object、PrintStream、String等类文件,而上面就显示了加载的全部类文件。 - -#### 其他命令 - --J <标记> -•传递一些信息给 Java Launcher. - - - javac -J-Xms48m Xxx.java //set the startup memory to 48M. - --@<文件名> - -> 如果同时需要编译数量较多的源文件(比如1000个),一个一个编译是不现实的(当然你可以直接 javac *.java ),比较好的方法是:将你想要编译的源文件名都写在一个文件中(比如sourcefiles.txt),其中每行写一个文件名,如下所示: -> -> -> HelloWorld01.java -> HelloWorld02.java -> HelloWorld03.java - -则使用下面的命令: - - -javac @sourcefiles.txt - -编译这三个源文件。 - - - -## 使用javac构建项目 - -这部分参考: -https://blog.csdn.net/mingover/article/details/57083176 - -一个简单的javac编译 - -新建两个文件夹,src和 build -src/com/yp/test/HelloWorld.java -build/ - - -```` -├─build -└─src - └─com - └─yp - └─test - HelloWorld.java -```` - - -java文件非常简单 -```` - package com.yp.test; - public class HelloWorld { - - public static void main(String[] args) { - System.out.println("helloWorld"); - } - } -```` - -编译: -javac src/com/yp/test/HelloWorld.java -d build - --d 表示编译到 build文件夹下 - -```` -查看build文件夹 -├─build -│ └─com -│ └─yp -│ └─test -│ HelloWorld.class -│ -└─src - └─com - └─yp - └─test - HelloWorld.java -```` - - -运行文件 - -> E:\codeplace\n_learn\java\javacmd> java com/yp/test/HelloWorld.class -> 错误: 找不到或无法加载主类 build.com.yp.test.HelloWorld.class -> -> 运行时要指定main -> E:\codeplace\n_learn\java\javacmd\build> java com.yp.test.HelloWorld -> helloWorld - -如果引用到多个其他的类,应该怎么做呢 ? - -> 编译 -> -> E:\codeplace\n_learn\java\javacmd>javac src/com/yp/test/HelloWorld.java -sourcepath src -d build -g -> 1 -> -sourcepath 表示 从指定的源文件目录中找到需要的.java文件并进行编译。 -> 也可以用-cp指定编译好的class的路径 -> 运行,注意:运行在build目录下 -> -> E:\codeplace\n_learn\java\javacmd\build>java com.yp.test.HelloWorld - -怎么打成jar包? - -> 生成: -> E:\codeplace\n_learn\java\javacmd\build>jar cvf h.jar * -> 运行: -> E:\codeplace\n_learn\java\javacmd\build>java h.jar -> 错误: 找不到或无法加载主类 h.jar - -> 这个错误是没有指定main类,所以类似这样来指定: -> E:\codeplace\n_learn\java\javacmd\build>java -cp h.jar com.yp.test.HelloWorld - - -生成可以运行的jar包 - -需要指定jar包的应用程序入口点,用-e选项: - - E:\codeplace\n_learn\java\javacmd\build> jar cvfe h.jar com.yp.test.HelloWorld * - 已添加清单 - 正在添加: com/(输入 = 0) (输出 = 0)(存储了 0%) - 正在添加: com/yp/(输入 = 0) (输出 = 0)(存储了 0%) - 正在添加: com/yp/test/(输入 = 0) (输出 = 0)(存储了 0%) - 正在添加: com/yp/test/entity/(输入 = 0) (输出 = 0)(存储了 0%) - 正在添加: com/yp/test/entity/Cat.class(输入 = 545) (输出 = 319)(压缩了 41%) - 正在添加: com/yp/test/HelloWorld.class(输入 = 844) (输出 = 487)(压缩了 42%) - -直接运行 - - java -jar h.jar - - 额外发现 - 指定了Main类后,jar包里面的 META-INF/MANIFEST.MF 是这样的, 比原来多了一行Main-Class…. - Manifest-Version: 1.0 - Created-By: 1.8.0 (Oracle Corporation) - Main-Class: com.yp.test.HelloWorld - -如果类里有引用jar包呢? - -先下一个jar包 这里直接下 log4j - -* main函数改成 - -```` -import com.yp.test.entity.Cat; -import org.apache.log4j.Logger; - -public class HelloWorld { - - static Logger log = Logger.getLogger(HelloWorld.class); - - public static void main(String[] args) { - Cat c = new Cat("keyboard"); - log.info("这是log4j"); - System.out.println("hello," + c.getName()); - } - -} -```` -现的文件是这样的 - - -```` -├─build -├─lib -│ log4j-1.2.17.jar -│ -└─src - └─com - └─yp - └─test - │ HelloWorld.java - │ - └─entity - Cat.java -```` - - 这个时候 javac命令要接上 -cp ./lib/*.jar - E:\codeplace\n_learn\java\javacmd>javac -encoding "utf8" src/com/yp/test/HelloWorld.java -sourcepath src -d build -g -cp ./lib/*.jar - - - 运行要加上-cp, -cp 选项貌似会把工作目录给换了, 所以要加上 ;../build - E:\codeplace\n_learn\java\javacmd\build>java -cp ../lib/log4j-1.2.17.jar;../build com.yp.test.HelloWorld - -结果: - - log4j:WARN No appenders could be found for logger(com.yp.test.HelloWorld). - log4j:WARN Please initialize the log4j system properly. - log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info. - hello,keyboard - -由于没有 log4j的配置文件,所以提示上面的问题,往 build 里面加上 log4j.xml - -```` - - - - - - - - - - - - - - -```` -再运行 - - E:\codeplace\n_learn\java\javacmd>java -cp lib/log4j-1.2.17.jar;build com.yp.tes t.HelloWorld - 15:19:57,359 INFO [HelloWorld] 这是log4j - hello,keyboard - -说明: -这个log4j配置文件,习惯的做法是放在src目录下, 在编译过程中 copy到build中的,但根据ant的做法,不是用javac的,而是用来处理,我猜测javac是不能copy的,如果想在命令行直接 使用,应该是用cp命令主动去执行 copy操作 - -ok 一个简单的java 工程就运行完了 -但是 貌似有些繁琐, 需要手动键入 java文件 以及相应的jar包 很是麻烦, -so 可以用 shell 来脚本来简化相关操作 -shell 文件整理如下: -```` - #!/bin/bash - echo "build start" - - JAR_PATH=libs - BIN_PATH=bin - SRC_PATH=src - - # java文件列表目录 - SRC_FILE_LIST_PATH=src/sources.list - - #生所有的java文件列表 放入列表文件中 - rm -f $SRC_PATH/sources - find $SRC_PATH/ -name *.java > $SRC_FILE_LIST_PATH - - #删除旧的编译文件 生成bin目录 - rm -rf $BIN_PATH/ - mkdir $BIN_PATH/ - - #生成依赖jar包 列表 - for file in ${JAR_PATH}/*.jar; - do - jarfile=${jarfile}:${file} - done - echo "jarfile = "$jarfile - - #编译 通过-cp指定所有的引用jar包,将src下的所有java文件进行编译 - javac -d $BIN_PATH/ -cp $jarfile @$SRC_FILE_LIST_PATH - - #运行 通过-cp指定所有的引用jar包,指定入口函数运行 - java -cp $BIN_PATH$jarfile com.zuiapps.danmaku.server.Main -```` - -> 有一点需要注意的是, javac -d $BIN_PATH/ -cp $jarfile @$SRC_FILE_LIST_PATH -> 在要编译的文件很多时候,一个个敲命令会显得很长,也不方便修改, - -> 可以把要编译的源文件列在文件中,在文件名前加@,这样就可以对多个文件进行编译, - -> 以上就是吧java文件放到 $SRC_FILE_LIST_PATH 中去了 - - 编译 : - 1. 需要编译所有的java文件 - 2. 依赖的java 包都需要加入到 classpath 中去 - 3. 最后设置 编译后的 class 文件存放目录 即 -d bin/ - 4. java文件过多是可以使用 @$SRC_FILE_LIST_PATH 把他们放到一个文件中去 - 运行: - 1.需要吧 编译时设置的bin目录和 所有jar包加入到 classpath 中去 - - - -## javap 的使用 - -> javap是jdk自带的一个工具,可以对代码反编译,也可以查看java编译器生成的字节码。 -> -> 情况下,很少有人使用javap对class文件进行反编译,因为有很多成熟的反编译工具可以使用,比如jad。但是,javap还可以查看java编译器为我们生成的字节码。通过它,可以对照源代码和字节码,从而了解很多编译器内部的工作。 -> -> -> -> javap命令分解一个class文件,它根据options来决定到底输出什么。如果没有使用options,那么javap将会输出包,类里的protected和public域以及类里的所有方法。javap将会把它们输出在标准输出上。来看这个例子,先编译(javac)下面这个类。 -```` -import java.awt.*; -import java.applet.*; - -public class DocFooter extends Applet { - String date; - String email; - - public void init() { - resize(500,100); - date = getParameter("LAST_UPDATED"); - email = getParameter("EMAIL"); - } -} -```` -在命令行上键入javap DocFooter后,输出结果如下 - - -Compiled from "DocFooter.java" -```` -public class DocFooter extends java.applet.Applet { - java.lang.String date; - java.lang.String email; - public DocFooter(); - public void init(); -} -```` -如果加入了-c,即javap -c DocFooter,那么输出结果如下 - -Compiled from "DocFooter.java" -```` - public class DocFooter extends java.applet.Applet { - java.lang.String date; - - java.lang.String email; - - public DocFooter(); - Code: - 0: aload_0 - 1: invokespecial #1 // Method java/applet/Applet."":()V - 4: return - - public void init(); - Code: - 0: aload_0 - 1: sipush 500 - 4: bipush 100 - 6: invokevirtual #2 // Method resize:(II)V - 9: aload_0 - 10: aload_0 - 11: ldc #3 // String LAST_UPDATED - 13: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String; - 16: putfield #5 // Field date:Ljava/lang/String; - 19: aload_0 - 20: aload_0 - 21: ldc #6 // String EMAIL - 23: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String; - 26: putfield #7 // Field email:Ljava/lang/String; - 29: return - - } -```` -上面输出的内容就是字节码。 - -用法摘要 - --help 帮助 --l 输出行和变量的表 --public 只输出public方法和域 --protected 只输出public和protected类和成员 --package 只输出包,public和protected类和成员,这是默认的 --p -private 输出所有类和成员 --s 输出内部类型签名 --c 输出分解后的代码,例如,类中每一个方法内,包含java字节码的指令, --verbose 输出栈大小,方法参数的个数 --constants 输出静态final常量 -总结 - -javap可以用于反编译和查看编译器编译后的字节码。平时一般用javap -c比较多,该命令用于列出每个方法所执行的JVM指令,并显示每个方法的字节码的实际作用。可以通过字节码和源代码的对比,深入分析java的编译原理,了解和解决各种Java原理级别的问题。 - -## 参考文章 - -https://blog.csdn.net/Anbernet/article/details/81449390 -https://www.cnblogs.com/luobiao320/p/7975442.html -https://www.jianshu.com/p/f7330dbdc051 -https://www.jianshu.com/p/6a8997560b05 -https://blog.csdn.net/w372426096/article/details/81664431 -https://blog.csdn.net/qincidong/article/details/82492140 - diff --git "a/docs/Java/basic/string\345\222\214\345\214\205\350\243\205\347\261\273.md" "b/docs/Java/basic/string\345\222\214\345\214\205\350\243\205\347\261\273.md" deleted file mode 100644 index 9adf53e..0000000 --- "a/docs/Java/basic/string\345\222\214\345\214\205\350\243\205\347\261\273.md" +++ /dev/null @@ -1,799 +0,0 @@ -# 目录 - - * [string基础](#string基础) - * [Java String 类](#java-string-类) - * [创建字符串](#创建字符串) - * [StringDemo.java 文件代码:](#stringdemojava-文件代码:) - * [String基本用法](#string基本用法) - * [创建String对象的常用方法](#创建string对象的常用方法) - * [String中常用的方法,用法如图所示,具体问度娘](#string中常用的方法,用法如图所示,具体问度娘) - * [三个方法的使用: lenth() substring() charAt()](#三个方法的使用:-lenth---substring---charat) - * [字符串与byte数组间的相互转换](#字符串与byte数组间的相互转换) - * [==运算符和equals之间的区别:](#运算符和equals之间的区别:) - * [字符串的不可变性](#字符串的不可变性) - * [String的连接](#string的连接) - * [String、String builder和String buffer的区别](#string、string-builder和string-buffer的区别) - * [String类的源码分析](#string类的源码分析) - * [String类型的intern](#string类型的intern) - * [String类型的equals](#string类型的equals) - * [StringBuffer和Stringbuilder](#stringbuffer和stringbuilder) - * [append方法](#append方法) - * [扩容](#扩容) - * [](#) - * [删除](#删除) - * [system.arraycopy方法](#systemarraycopy方法) - * [String和JVM的关系](#string和jvm的关系) - * [String为什么不可变?](#string为什么不可变?) - * [不可变有什么好处?](#不可变有什么好处?) - * [String常用工具类](#string常用工具类) - * [参考文章](#参考文章) - - - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -## string基础 - -### Java String 类 - -字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。 - -### 创建字符串 - -创建字符串最简单的方式如下: - -String greeting = "菜鸟教程"; - -在代码中遇到字符串常量时,这里的值是 "**菜鸟教程**"",编译器会使用该值创建一个 String 对象。 - -和其它对象一样,可以使用关键字和构造方法来创建 String 对象。 - -String 类有 11 种构造方法,这些方法提供不同的参数来初始化字符串,比如提供一个字符数组参数: - -### StringDemo.java 文件代码: -```` -public class StringDemo{ -public static void main(String args[]){ -char[] helloArray = { 'r', 'u', 'n', 'o', 'o', 'b'}; -String helloString = new String(helloArray); -System.out.println( helloString ); -} } -```` -以上实例编译运行结果如下: - -``` -runoob -``` - -**注意:**String 类是不可改变的,所以你一旦创建了 String 对象,那它的值就无法改变了(详看笔记部分解析)。 - -如果需要对字符串做很多修改,那么应该选择使用 [StringBuffer & StringBuilder 类](https://www.runoob.com/java/java-stringbuffer.html)。 - -## String基本用法 - -### 创建String对象的常用方法 - -(1) String s1 = "mpptest" - -(2) String s2 = new String(); - -(3) String s3 = new String("mpptest") - -### String中常用的方法,用法如图所示,具体问度娘 - -![](https://img2018.cnblogs.com/blog/710412/201902/710412-20190213220237169-1966705420.png) - -### 三个方法的使用: lenth() substring() charAt() -```` -package com.mpp.string; -public class StringDemo1 { - public static void main(String[] args) { //定义一个字符串"晚来天欲雪 能饮一杯无" - String str = "晚来天欲雪 能饮一杯无"; - System.out.println("字符串的长度是:"+str.length()); //字符串的雪字打印输出 charAt(int index) - System.out.println(str.charAt(4)); //取出子串 天欲 - System.out.println(str.substring(2)); //取出从index2开始直到最后的子串,包含2 - System.out.println(str.substring(2,4)); //取出index从2到4的子串,包含2不包含4 顾头不顾尾 - } -} -```` - - -两个方法的使用,求字符或子串第一次/最后一次在字符串中出现的位置: -indexOf() lastIndexOf() - -```` -package com.mpp.string; public class StringDemo2 { - public static void main(String[] args) { - String str = new String("赵客缦胡缨 吴钩胡缨霜雪明"); //查找胡在字符串中第一次出现的位置 - System.out.println("\"胡\"在字符串中第一次出现的位置:"+str.indexOf("胡")); //查找子串"胡缨"在字符串中第一次出现的位置 - System.out.println("\"胡缨\"在字符串中第一次出现的位置"+str.indexOf("胡缨")); //查找胡在字符串中最后一次次出现的位置 - System.out.println(str.lastIndexOf("胡")); //查找子串"胡缨"在字符串中最后一次出现的位置 - System.out.println(str.lastIndexOf("胡缨")); //从indexof为5的位置,找第一次出现的"吴" - System.out.println(str.indexOf("吴",5)); - } -} -```` - - - -### 字符串与byte数组间的相互转换 - -```` -package com.mpp.string; import java.io.UnsupportedEncodingException; -public class StringDemo3 { - public static void main(String[] args) throws UnsupportedEncodingException { - - //字符串和byte数组之间的相互转换 - String str = new String("hhhabc银鞍照白马 飒沓如流星"); //将字符串转换为byte数组,并打印输出 - byte[] arrs = str.getBytes("GBK"); - for(int i=0;i){ - System.out.print(arrs[i]); - } - - //将byte数组转换成字符串 - System.out.println(); - String str1 = new String(arrs,"GBK"); //保持字符集的一致,否则会出现乱码 - System.out.println(str1); - } -} -```` - -### ==运算符和equals之间的区别: - -引用指向的内容和引用指向的地址 - -```` -package com.mpp.string; public class StringDemo5 { - public static void main(String[] args) { - String str1 = "mpp"; - String str2 = "mpp"; - String str3 = new String("mpp"); - - System.out.println(str1.equals(str2)); //true 内容相同 - System.out.println(str1.equals(str3)); //true 内容相同 - System.out.println(str1==str2); //true 地址相同 - System.out.println(str1==str3); //false 地址不同 - } -} -```` - -### 字符串的不可变性 - -String的对象一旦被创建,则不能修改,是不可变的 - -所谓的修改其实是创建了新的对象,所指向的内存空间不变 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/blog/Java%E6%8A%80%E6%9C%AF%E6%B1%9F%E6%B9%96/%E4%BA%8C%E7%BB%B4%E7%A0%81/710412-20190214224055939-746946317.png) - -上图中,s1不再指向imooc所在的内存空间,而是指向了hello,imooc -### String的连接 - - @Test - public void contact () { - //1连接方式 - String s1 = "a"; - String s2 = "a"; - String s3 = "a" + s2; - String s4 = "a" + "a"; - String s5 = s1 + s2; - //表达式只有常量时,编译期完成计算 - //表达式有变量时,运行期才计算,所以地址不一样 - System.out.println(s3 == s4); //f - System.out.println(s3 == s5); //f - System.out.println(s4 == "aa"); //t - - } -### String、String builder和String buffer的区别 -String是Java中基础且重要的类,并且String也是Immutable类的典型实现,被声明为final class,除了hash这个属性其它属性都声明为final,因为它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。 - -StringBuffer就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,提供append和add方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了synchronized。但是保证了线程安全是需要性能的代价的。 - -在很多情况下我们的字符串拼接操作不需要线程安全,这时候StringBuilder登场了,StringBuilder是JDK1.5发布的,它和StringBuffer本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。 - -StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder ,底层都是利用可修改的char数组(JDK 9 以后是 byte数组)。 - -所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者StringBuilder 的时候设置好capacity,避免多次扩容的开销。扩容要抛弃原有数组,还要进行数组拷贝创建新的数组。 - -我们平日开发通常情况下少量的字符串拼接其实没太必要担心,例如 - -String str = "aa"+"bb"+"cc"; - -像这种没有变量的字符串,编译阶段就直接合成"aabbcc"了,然后看字符串常量池(下面会说到常量池)里有没有,有也直接引用,没有就在常量池中生成,返回引用。 - -如果是带变量的,其实影响也不大,JVM会帮我们优化了。 - -> 1、在字符串不经常发生变化的业务场景优先使用String(代码更清晰简洁)。如常量的声明,少量的字符串操作(拼接,删除等)。 -> -> 2、在单线程情况下,如有大量的字符串操作情况,应该使用StringBuilder来操作字符串。不能使用String"+"来拼接而是使用,避免产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)。如JSON的封装等。 -> -> 3、在多线程情况下,如有大量的字符串操作情况,应该使用StringBuffer。如HTTP参数解析和封装等。 - -## String类的源码分析 - -### String类型的intern -```` -public void intern () { - //2:string的intern使用 - //s1是基本类型,比较值。s2是string实例,比较实例地址 - //字符串类型用equals方法比较时只会比较值 - String s1 = "a"; - String s2 = new String("a"); - //调用intern时,如果s2中的字符不在常量池,则加入常量池并返回常量的引用 - String s3 = s2.intern(); - System.out.println(s1 == s2); - System.out.println(s1 == s3); -} -```` -### String类型的equals -```` -//字符串的equals方法 -// public boolean equals(Object anObject) { -// if (this == anObject) { -// return true; -// } -// if (anObject instanceof String) { -// String anotherString = (String)anObject; -// int n = value.length; -// if (n == anotherString.value.length) { -// char v1[] = value; -// char v2[] = anotherString.value; -// int i = 0; -// while (n-- != 0) { -// if (v1[i] != v2[i]) -// return false; -// i++; -// } -// return true; -// } -// } -// return false; -// } -```` -### StringBuffer和Stringbuilder -底层是继承父类的可变字符数组value -```` -/** - -- The value is used for character storage. - */ - char[] value; - 初始化容量为16 - -/** - -- Constructs a string builder with no characters in it and an -- initial capacity of 16 characters. - */ - public StringBuilder() { - super(16); - } - 这两个类的append方法都是来自父类AbstractStringBuilder的方法 - -public AbstractStringBuilder append(String str) { - if (str == null) - return appendNull(); - int len = str.length(); - ensureCapacityInternal(count + len); - str.getChars(0, len, value, count); - count += len; - return this; -} -@Override -public StringBuilder append(String str) { - super.append(str); - return this; -} - -@Override -public synchronized StringBuffer append(String str) { - toStringCache = null; - super.append(str); - return this; -} -```` - -### append方法 -Stringbuffer在大部分涉及字符串修改的操作上加了synchronized关键字来保证线程安全,效率较低。 - -String类型在使用 + 运算符例如 - -String a = "a" - -a = a + a;时,实际上先把a封装成stringbuilder,调用append方法后再用tostring返回,所以当大量使用字符串加法时,会大量地生成stringbuilder实例,这是十分浪费的,这种时候应该用stringbuilder来代替string。 - -### 扩容 -#注意在append方法中调用到了一个函数 - -ensureCapacityInternal(count + len); -该方法是计算append之后的空间是否足够,不足的话需要进行扩容 -```` -public void ensureCapacity(int minimumCapacity) { - if (minimumCapacity > 0) - ensureCapacityInternal(minimumCapacity); -} -private void ensureCapacityInternal(int minimumCapacity) { - // overflow-conscious code - if (minimumCapacity - value.length > 0) { - value = Arrays.copyOf(value, - newCapacity(minimumCapacity)); - } -} -```` -如果新字符串长度大于value数组长度则进行扩容 - -扩容后的长度一般为原来的两倍 + 2; - -假如扩容后的长度超过了jvm支持的最大数组长度MAX_ARRAY_SIZE。 - -考虑两种情况 - -如果新的字符串长度超过int最大值,则抛出异常,否则直接使用数组最大长度作为新数组的长度。 -```` -private int hugeCapacity(int minCapacity) { - if (Integer.MAX_VALUE - minCapacity < 0) { // overflow - throw new OutOfMemoryError(); - } - return (minCapacity > MAX_ARRAY_SIZE) - ? minCapacity : MAX_ARRAY_SIZE; -} -```` -### 删除 -这两个类型的删除操作: - -都是调用父类的delete方法进行删除 -```` -public AbstractStringBuilder delete(int start, int end) { - if (start < 0) - throw new StringIndexOutOfBoundsException(start); - if (end > count) - end = count; - if (start > end) - throw new StringIndexOutOfBoundsException(); - int len = end - start; - if (len > 0) { - System.arraycopy(value, start+len, value, start, count-end); - count -= len; - } - return this; -} -```` -事实上是将剩余的字符重新拷贝到字符数组value。 - -这里用到了system.arraycopy来拷贝数组,速度是比较快的 - -### system.arraycopy方法 -转自知乎: - -> 在主流高性能的JVM上(HotSpot VM系、IBM J9 VM系、JRockit系等等),可以认为System.arraycopy()在拷贝数组时是可靠高效的——如果发现不够高效的情况,请报告performance bug,肯定很快就会得到改进。 -> -> java.lang.System.arraycopy()方法在Java代码里声明为一个native方法。所以最naïve的实现方式就是通过JNI调用JVM里的native代码来实现。 -> -> String的不可变性 -> 关于String的不可变性,这里转一个不错的回答 -> -> 什么是不可变? -> String不可变很简单,如下图,给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。 -> - -## String和JVM的关系 - -下面我们了解下Java栈、Java堆、方法区和常量池: - -Java栈(线程私有数据区): - -``` - 每个Java虚拟机线程都有自己的Java虚拟机栈,Java虚拟机栈用来存放栈帧,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 -``` - -Java堆(线程共享数据区): - -``` - 在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。 -``` - -方法区(线程共享数据区): - -``` - 方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。在JDK8之前永久代是方法区的一种实现,而JDK8元空间替代了永久代,永久代被移除,也可以理解为元空间是方法区的一种实现。 -``` - -常量池(线程共享数据区): - -``` - 常量池常被分为两大类:静态常量池和运行时常量池。 - - 静态常量池也就是Class文件中的常量池,存在于Class文件中。 - - 运行时常量池(Runtime Constant Pool)是方法区的一部分,存放一些运行时常量数据。 -``` - -下面重点了解的是字符串常量池: - -``` - 字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。 - - 字符串常量池的存在使JVM提高了性能和减少了内存开销。 - - 使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。 -``` - -``` - 使用字符串常量池,每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。 -``` - -## String为什么不可变? -翻开JDK源码,java.lang.String类起手前三行,是这样写的: -```` -public final class String implements java.io.Serializable, Comparable, CharSequence { - /** String本质是个char数组. 而且用final关键字修饰.*/ -private final char value[]; ... ... - } -```` -首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[]数组,而且是用final修饰的。 - -final修饰的字段创建以后就不可改变。 有的人以为故事就这样完了,其实没有。因为虽然value是不可变,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。 - -Array的数据结构看下图。 - -也就是说Array变量只是stack上的一个引用,数组的本体结构在heap堆。 - -String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。看下面这个例子, -```` -final int[] value={1,2,3} ; -int[] another={4,5,6}; - value=another; //编译器报错,final不可变 value用final修饰,编译器不允许我把value指向堆区另一个地址。 -但如果我直接对数组元素动手,分分钟搞定。 - - final int[] value={1,2,3}; - value[2]=100; //这时候数组里已经是{1,2,100} 所以String是不可变,关键是因为SUN公司的工程师。 - 在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。 -```` -### 不可变有什么好处? -这个最简单地原因,就是为了安全。看下面这个场景(有评论反应例子不够清楚,现在完整地写出来),一个函数appendStr( )在不可变的String参数后面加上一段“bbb”后返回。appendSb( )负责在可变的StringBuilder后面加“bbb”。 - -总结以下String的不可变性。 - -> 1 首先final修饰的类只保证不能被继承,并且该类的对象在堆内存中的地址不会被改变。 -> -> 2 但是持有String对象的引用本身是可以改变的,比如他可以指向其他的对象。 -> -> 3 final修饰的char数组保证了char数组的引用不可变。但是可以通过char[0] = 'a’来修改值。不过String内部并不提供方法来完成这一操作,所以String的不可变也是基于代码封装和访问控制的。 - -举个例子 -```` -final class Fi { - int a; - final int b = 0; - Integer s; - -} -final char[]a = {'a'}; -final int[]b = {1}; -@Test -public void final修饰类() { - //引用没有被final修饰,所以是可变的。 - //final只修饰了Fi类型,即Fi实例化的对象在堆中内存地址是不可变的。 - //虽然内存地址不可变,但是可以对内部的数据做改变。 - Fi f = new Fi(); - f.a = 1; - System.out.println(f); - f.a = 2; - System.out.println(f); - //改变实例中的值并不改变内存地址。 -```` -```` -Fi ff = f; -//让引用指向新的Fi对象,原来的f对象由新的引用ff持有。 -//引用的指向改变也不会改变原来对象的地址 -f = new Fi(); -System.out.println(f); -System.out.println(ff); - -} -```` - -这里的对f.a的修改可以理解为char[0] = 'a'这样的操作。只改变数据值,不改变内存值。 - -## String常用工具类 -问题描述 -很多时候我们需要对字符串进行很多固定的操作,而这些操作在JDK/JRE中又没有预置,于是我们想到了apache-commons组件,但是它也不能完全覆盖我们的业务需求,所以很多时候还是要自己写点代码的,下面就是基于apache-commons组件写的部分常用方法: - - -```` - - org.apache.commons - commons-lang3 - ${commons-lang3.version} - -```` - -代码成果 - -```` -public class StringUtils extends org.apache.commons.lang3.StringUtils { - -/** 值为"NULL"的字符串 */ -private static final String NULL_STRING = "NULL"; - -private static final char SEPARATOR = '_'; - - -/** - * 满足一下情况返回true
- * ①.入参为空 - * ②.入参为空字符串 - * ③.入参为"null"字符串 - * - * @param string 需要判断的字符型 - * @return boolean - */ -public static boolean isNullOrEmptyOrNULLString(String string) { - return isBlank(string) || NULL_STRING.equalsIgnoreCase(string); -} - -/** - * 把字符串转为二进制码
- * 本方法不会返回null - * - * @param str 需要转换的字符串 - * @return 二进制字节码数组 - */ -public static byte[] toBytes(String str) { - return isBlank(str) ? new byte[]{} : str.getBytes(); -} - -/** - * 把字符串转为二进制码
- * 本方法不会返回null - * - * @param str 需要转换的字符串 - * @param charset 编码类型 - * @return 二进制字节码数组 - * @throws UnsupportedEncodingException 字符串转换的时候编码不支持时出现 - */ -public static byte[] toBytes(String str, Charset charset) throws UnsupportedEncodingException { - return isBlank(str) ? new byte[]{} : str.getBytes(charset.displayName()); -} - -/** - * 把字符串转为二进制码
- * 本方法不会返回null - * - * @param str 需要转换的字符串 - * @param charset 编码类型 - * @param locale 编码类型对应的地区 - * @return 二进制字节码数组 - * @throws UnsupportedEncodingException 字符串转换的时候编码不支持时出现 - */ -public static byte[] toBytes(String str, Charset charset, Locale locale) throws UnsupportedEncodingException { - return isBlank(str) ? new byte[]{} : str.getBytes(charset.displayName(locale)); -} - -/** - * 二进制码转字符串
- * 本方法不会返回null - * - * @param bytes 二进制码 - * @return 字符串 - */ -public static String bytesToString(byte[] bytes) { - return bytes == null || bytes.length == 0 ? EMPTY : new String(bytes); -} - -/** - * 二进制码转字符串
- * 本方法不会返回null - * - * @param bytes 二进制码 - * @param charset 编码集 - * @return 字符串 - * @throws UnsupportedEncodingException 当前二进制码可能不支持传入的编码 - */ -public static String byteToString(byte[] bytes, Charset charset) throws UnsupportedEncodingException { - return bytes == null || bytes.length == 0 ? EMPTY : new String(bytes, charset.displayName()); -} - -/** - * 二进制码转字符串
- * 本方法不会返回null - * - * @param bytes 二进制码 - * @param charset 编码集 - * @param locale 本地化 - * @return 字符串 - * @throws UnsupportedEncodingException 当前二进制码可能不支持传入的编码 - */ -public static String byteToString(byte[] bytes, Charset charset, Locale locale) throws UnsupportedEncodingException { - return bytes == null || bytes.length == 0 ? EMPTY : new String(bytes, charset.displayName(locale)); -} - -/** - * 把对象转为字符串 - * - * @param object 需要转化的字符串 - * @return 字符串, 可能为空 - */ -public static String parseString(Object object) { - if (object == null) { - return null; - } - if (object instanceof byte[]) { - return bytesToString((byte[]) object); - } - return object.toString(); -} - -/** - * 把字符串转为int类型 - * - * @param str 需要转化的字符串 - * @return int - * @throws NumberFormatException 字符串格式不正确时抛出 - */ -public static int parseInt(String str) throws NumberFormatException { - return isBlank(str) ? 0 : Integer.parseInt(str); -} - -/** - * 把字符串转为double类型 - * - * @param str 需要转化的字符串 - * @return double - * @throws NumberFormatException 字符串格式不正确时抛出 - */ -public static double parseDouble(String str) throws NumberFormatException { - return isBlank(str) ? 0D : Double.parseDouble(str); -} - -/** - * 把字符串转为long类型 - * - * @param str 需要转化的字符串 - * @return long - * @throws NumberFormatException 字符串格式不正确时抛出 - */ -public static long parseLong(String str) throws NumberFormatException { - return isBlank(str) ? 0L : Long.parseLong(str); -} - -/** - * 把字符串转为float类型 - * - * @param str 需要转化的字符串 - * @return float - * @throws NumberFormatException 字符串格式不正确时抛出 - */ -public static float parseFloat(String str) throws NumberFormatException { - return isBlank(str) ? 0L : Float.parseFloat(str); -} - -/** - * 获取i18n字符串 - * - * @param code - * @param args - * @return - */ -public static String getI18NMessage(String code, Object[] args) { - //LocaleResolver localLocaleResolver = (LocaleResolver) SpringContextHolder.getBean(LocaleResolver.class); - //HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); - //Locale locale = localLocaleResolver.resolveLocale(request); - //return SpringContextHolder.getApplicationContext().getMessage(code, args, locale); - return ""; -} - -/** - * 获得用户远程地址 - * - * @param request 请求头 - * @return 用户ip - */ -public static String getRemoteAddr(HttpServletRequest request) { - String remoteAddr = request.getHeader("X-Real-IP"); - if (isNotBlank(remoteAddr)) { - remoteAddr = request.getHeader("X-Forwarded-For"); - } else if (isNotBlank(remoteAddr)) { - remoteAddr = request.getHeader("Proxy-Client-IP"); - } else if (isNotBlank(remoteAddr)) { - remoteAddr = request.getHeader("WL-Proxy-Client-IP"); - } - return remoteAddr != null ? remoteAddr : request.getRemoteAddr(); -} - -/** - * 驼峰命名法工具 - * - * @return toCamelCase(" hello_world ") == "helloWorld" - * toCapitalizeCamelCase("hello_world") == "HelloWorld" - * toUnderScoreCase("helloWorld") = "hello_world" - */ -public static String toCamelCase(String s, Locale locale, char split) { - if (isBlank(s)) { - return ""; - } - - s = s.toLowerCase(locale); - - StringBuilder sb = new StringBuilder(); - for (char c : s.toCharArray()) { - sb.append(c == split ? Character.toUpperCase(c) : c); - } - - return sb.toString(); -} - -public static String toCamelCase(String s) { - return toCamelCase(s, Locale.getDefault(), SEPARATOR); -} - -public static String toCamelCase(String s, Locale locale) { - return toCamelCase(s, locale, SEPARATOR); -} - -public static String toCamelCase(String s, char split) { - return toCamelCase(s, Locale.getDefault(), split); -} - -public static String toUnderScoreCase(String s, char split) { - if (isBlank(s)) { - return ""; - } - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - boolean nextUpperCase = (i < (s.length() - 1)) && Character.isUpperCase(s.charAt(i + 1)); - boolean upperCase = (i > 0) && Character.isUpperCase(c); - sb.append((!upperCase || !nextUpperCase) ? split : "").append(Character.toLowerCase(c)); - } - - return sb.toString(); -} - -public static String toUnderScoreCase(String s) { - return toUnderScoreCase(s, SEPARATOR); -} - -/** - * 把字符串转换为JS获取对象值的三目运算表达式 - * - * @param objectString 对象串 - * 例如:入参:row.user.id/返回:!row?'':!row.user?'':!row.user.id?'':row.user.id - */ -public static String toJsGetValueExpression(String objectString) { - StringBuilder result = new StringBuilder(); - StringBuilder val = new StringBuilder(); - String[] fileds = split(objectString, "."); - for (int i = 0; i < fileds.length; i++) { - val.append("." + fileds[i]); - result.append("!" + (val.substring(1)) + "?'':"); - } - result.append(val.substring(1)); - return result.toString(); -} - - -} -```` - -## 参考文章 -https://blog.csdn.net/qq_34490018/article/details/82110578 -https://www.runoob.com/java/java-string.html -https://www.cnblogs.com/zhangyinhua/p/7689974.html -https://blog.csdn.net/sinat_21925975/article/details/86493248 -https://www.cnblogs.com/niew/p/9597379.html - diff --git "a/docs/Java/basic/\344\273\243\347\240\201\345\235\227\345\222\214\344\273\243\347\240\201\346\211\247\350\241\214\351\241\272\345\272\217.md" "b/docs/Java/basic/\344\273\243\347\240\201\345\235\227\345\222\214\344\273\243\347\240\201\346\211\247\350\241\214\351\241\272\345\272\217.md" deleted file mode 100644 index 7ebb784..0000000 --- "a/docs/Java/basic/\344\273\243\347\240\201\345\235\227\345\222\214\344\273\243\347\240\201\346\211\247\350\241\214\351\241\272\345\272\217.md" +++ /dev/null @@ -1,542 +0,0 @@ -# 目录 - - * [Java中的构造方法](#java中的构造方法) - * [构造方法简介](#构造方法简介) - * [构造方法实例](#构造方法实例) - * [例 1](#例-1) - * [例 2](#例-2) - * [Java中的几种构造方法详解](#java中的几种构造方法详解) - * [普通构造方法](#普通构造方法) - * [默认构造方法](#默认构造方法) - * [重载构造方法](#重载构造方法) - * [java子类构造方法调用父类构造方法](#java子类构造方法调用父类构造方法) - * [Java中的代码块简介](#java中的代码块简介) - * [Java代码块使用](#java代码块使用) - * [局部代码块](#局部代码块) - * [构造代码块](#构造代码块) - * [静态代码块](#静态代码块) - * [Java代码块、构造方法(包含继承关系)的执行顺序](#java代码块、构造方法(包含继承关系)的执行顺序) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - -## Java中的构造方法 -### 构造方法简介 - -构造方法是类的一种特殊方法,用来初始化类的一个新的对象。[Java](http://c.biancheng.net/java/)中的每个类都有一个默认的构造方法,它必须具有和类名相同的名称,而且没有返回类型。构造方法的默认返回类型就是对象类型本身,并且构造方法不能被 static、final、synchronized、abstract 和 native 修饰。 - -提示:构造方法用于初始化一个新对象,所以用 static 修饰没有意义;构造方法不能被子类继承,所以用 final 和 abstract 修饰没有意义;多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 修饰没有必要。 - -构造方法的语法格式如下: -```` -class class_name -{ - public class_name(){} //默认无参构造方法 - public ciass_name([paramList]){} //定义构造方法 - … - //类主体 -} -```` -在一个类中,与类名相同的方法就是构造方法。每个类可以具有多个构造方法,但要求它们各自包含不同的方法参数。 - -### 构造方法实例 - -#### 例 1 - -构造方法主要有无参构造方法和有参构造方法两种,示例如下: -```` -public class MyClass -{ - private int m; //定义私有变量 - MyClass() - { - //定义无参的构造方法 - m=0; - } - MyCiass(int m) - { - //定义有参的构造方法 - this.m=m; - } -} -```` -该示例定义了两个构造方法,分别是无参构造方法和有参构造方法。在一个类中定义多个具有不同参数的同名方法,这就是方法的重载。这两个构造方法的名称都与类名相同,均为 MyClass。在实例化该类时可以调用不同的构造方法进行初始化。 - -注意:类的构造方法不是要求必须定义的。如果在类中没有定义任何一个构造方法,则 Java 会自动为该类生成一个默认的构造方法。默认的构造方法不包含任何参数,并且方法体为空。如果类中显式地定义了一个或多个构造方法,则 Java 不再提供默认构造方法。 - -#### 例 2 - -要在不同的条件下使用不同的初始化行为创建类的对象,这时候就需要在一个类中创建多个构造方法。下面通过一个示例来演示构造方法的使用。 - -(1) 首先在员工类 Worker 中定义两个构造方法,代码如下: -```` -public class Worker -{ - public String name; //姓名 - private int age; //年龄 - //定义带有一个参数的构造方法 - public Worker(String name) - { - this.name=name; - } - //定义带有两个参数的构造方法 - public Worker(String name,int age) - { - this.name=name; - this.age=age; - } - public String toString() - { - return"大家好!我是新来的员工,我叫"+name+",今年"+age+"岁。"; - } -} -```` -在 Worker 类中定义了两个属性,其中 name 属性不可改变。分别定义了带有一个参数和带有两个参数的构造方法,并对其属性进行初始化。最后定义了该类的 toString() 方法,返回一条新进员工的介绍语句。 - -提示:Object 类具有一个 toString() 方法,该方法是个特殊的方法,创建的每个类都会继承该方法,它返回一个 String 类型的字符串。如果一个类中定义了该方法,则在调用该类对象时,将会自动调用该类对象的 toString() 方法返回一个字符串,然后使用“System.out.println(对象名)”就可以将返回的字符串内容打印出来。 - -(2) 在 TestWorker 类中创建 main() 方法作为程序的入口处,在 main() 方法中调用不同的构造方法实例化 Worker 对象,并对该对象中的属性进行初始化,代码如下: -```` -public class TestWorker -{ - public static void main(String[] args) - { - System.out.println("-----------带有一个参数的构造方法-----------"); - //调用带有一个参数的构造方法,Staff类中的sex和age属性值不变 - Worker worker1=new Worker("张强"); - System.out.println(worker1); - System.out.println("-----------带有两个参数的构造方法------------"); - //调用带有两个参数的构造方法,Staff类中的sex属性值不变 - Worker worker2=new Worker("李丽",25); - System.out.println(worker2); - } -} -```` -在上述代码中,创建了两个不同的 Worker 对象:一个是姓名为张强的员工对象,一个是姓名为李丽、年龄为 25 的员工对象。对于第一个 Worker 对象 Worker1,并未指定 age 属性值,因此程序会将其值采用默认值 0。对于第二个 Worker 对象 Worker2,分别对其指定了 name 属性值和 age 属性值,因此程序会将传递的参数值重新赋值给 Worker 类中的属性值。 - -运行 TestWorker 类,输出的结果如下: - - -----------带有一个参数的构造方法----------- - 大家好!我是新来的员工,我叫张强,今年0岁。 - -----------带有两个参数的构造方法------------ - 大家好!我是新来的员工,我叫李丽,今年25岁。 - -通过调用带参数的构造方法,在创建对象时,一并完成了对象成员的初始化工作,简化了对象初始化的代码。 - -## Java中的几种构造方法详解 -### 普通构造方法 - -方法名与类名相同 - -无返回类型 - -子类不能继承父类的构造方法 - -不能被static、final、abstract修饰(有final和static修饰的是不能被子类继承的,abstract修饰的是抽象类,抽象类是不能实例化的,也就是不能new) - -可以被private修饰,可以在本类里面实例化,但是外部不能实例化对象(注意!!!) -```` - public class A{ - int i=0; - public A(){ - i=2; - } - public A(int i){ - this.i=i; - } - } -```` -### 默认构造方法 -如果没有任何的构造方法,编译时系统会自动添加一个默认无参构造方法 - -隐含的默认构造方法 -```` - public A(){} -```` -显示的默认构造方法 -```` - public A(){ - System.out.print("显示的默认构造方法") - } -```` - -### 重载构造方法 -比如原本的类里的构造方法是一个参数的,现在新建的对象是有三个参数,此时就要重载构造方法 - -当一个类中有多个构造方法,有可能会出现重复性操作,这时可以用this语句调用其他的构造方法。 -```` - public class A{ - private int age; - private String name; - public A(int age,String name){ - this.age=age; - this.name=name; - } - public A(int age){ - this(age,"无名氏");//调用 A(int age,String name)构造方法 - } - public A(){ - this(1);//调用 A(int age)构造方法 - } - public void setName(String name) {this.name=name;} - public String getName() {return name;} - public void setAge(int age) {this.age=age;} - public int getAge() {return age;} - } - - A a=new A(20,"周一"); - A b=new A(20); - A c=new A(); - String name = a.getName(); - String name1 = b.getName(); - int age = c.getAge(); - System.out.println(name); - System.out.println(name1); - System.out.println(age); -```` - -### java子类构造方法调用父类构造方法 - -首先父类构造方法是绝对不能被子类继承的。 - -子类构造方法调用父类的构造方法重点是:子类构造方法无论如何都要调用父类的构造方法。 - -子类构造方法要么调用父类无参构造方法(包括当父类没有构造方法时。系统默认给的无参构造方法),要么调用父类有参构造方法。当子类构造方法调用父类无参构造方法,一般都是默认不写的,要写的话就是super(),且要放在构造方法的第一句。当子类构造方法要调用父类有参数的构造方法,那么子类的构造方法中必须要用super(参数)调用父类构造方法,且要放在构造方法的第一句。 - -当子类的构造方法是无参构造方法时,必须调用父类无参构造方法。因为系统会自动找父类有没有无参构造方法,如果没有的话系统会报错:说父类没有定义无参构造方法。 - -当子类构造方法是有参构造方法时,这时就会有两种情况。 -第一种:子类构造方法没有写super,也就是说你默认调用父类无参构造方法,这样的话就和子类是无参构造方法一样。 - -第二种:子类构造方法有super(参数)时,就是调用父类有参构造方法,系统会找父类有没有参数一致(参数数量,且类型顺序要相同)的有参构造方法,如果没有的话,同样也会报错。 - -但是这里会遇到和重载构造方法this一样问题,一个参数的构造方法可以调用多个参数构造方法,没有的参数给一个自己定义值也是可以的。 - -## Java中的代码块简介 - -在java中用{}括起来的称为代码块,代码块可分为以下四种: - -**一.简介** - -**1.普通代码块:** - -类中方法的方法体 - -**2.构造代码块**: - -构造块会在创建对象时被调用,每次创建时都会被调用,优先于类构造函数执行。 - -**3.静态代码块:** - -用static{}包裹起来的代码片段,只会执行一次。静态代码块优先于构造块执行。 - -**4.同步代码块:** - -使用synchronized(){}包裹起来的代码块,在多线程环境下,对共享数据的读写操作是需要互斥进行的,否则会导致数据的不一致性。同步代码块需要写在方法中。 - -**二.静态代码块和构造代码块的异同点** - -相同点:都是JVM加载类后且在构造函数执行之前执行,在类中可定义多个,一般在代码块中对一些static变量进行赋值。 - -不同点:静态代码块在非静态代码块之前执行。静态代码块只在第一次new时执行一次,之后不在执行。而非静态代码块每new一次就执行一次。 - -## Java代码块使用 - -### 局部代码块 - -> 位置:局部位置(方法内部) - -> 作用:限定变量的生命周期,尽早释放,节约内存 - -> 调用:调用其所在的方法时执行 - - -```` -public class 局部代码块 { - @Test - public void test (){ - B b = new B(); - b.go(); - } - } - class B { - B(){} - public void go() { - //方法中的局部代码块,一般进行一次性地调用,调用完立刻释放空间,避免在接下来的调用过程中占用栈空间 - //因为栈空间内存是有限的,方法调用可能会会生成很多局部变量导致栈内存不足。 - //使用局部代码块可以避免这样的情况发生。 - { - int i = 1; - ArrayList list = new ArrayList<>(); - while (i < 1000) { - list.add(i ++); - } - for (Integer j : list) { - System.out.println(j); - } - System.out.println("gogogo"); - } - System.out.println("hello"); - } - } -```` -### 构造代码块 - - >位置:类成员的位置,就是类中方法之外的位置 - - >作用:把多个构造方法共同的部分提取出来,共用构造代码块 - - >调用:每次调用构造方法时,都会优先于构造方法执行,也就是每次new一个对象时自动调用,对 对象的初始化 -```` - class A{ - int i = 1; - int initValue;//成员变量的初始化交给代码块来完成 - { - //代码块的作用体现于此:在调用构造方法之前,用某段代码对成员变量进行初始化。 - //而不是在构造方法调用时再进行。一般用于将构造方法的相同部分提取出来。 - // - for (int i = 0;i < 100;i ++) { - initValue += i; - } - } - { - System.out.println(initValue); - System.out.println(i);//此时会打印1 - int i = 2;//代码块里的变量和成员变量不冲突,但会优先使用代码块的变量 - System.out.println(i);//此时打印2 - //System.out.println(j);//提示非法向后引用,因为此时j的的初始化还没开始。 - // - } - { - System.out.println("代码块运行"); - } - int j = 2; - { - System.out.println(j); - System.out.println(i);//代码块中的变量运行后自动释放,不会影响代码块之外的代码 - } - A(){ - System.out.println("构造方法运行"); - } - } - public class 构造代码块 { - @Test - public void test() { - A a = new A(); - } - } -```` -### 静态代码块 - - 位置:类成员位置,用static修饰的代码块 - - 作用:对类进行一些初始化 只加载一次,当new多个对象时,只有第一次会调用静态代码块,因为,静态代码块 是属于类的,所有对象共享一份 - - 调用: new 一个对象时自动调用 - - ```` - public class 静态代码块 { - - @Test - public void test() { - C c1 = new C(); - C c2 = new C(); - //结果,静态代码块只会调用一次,类的所有对象共享该代码块 - //一般用于类的全局信息初始化 - //静态代码块调用 - //代码块调用 - //构造方法调用 - //代码块调用 - //构造方法调用 - } - - } - class C{ - C(){ - System.out.println("构造方法调用"); - } - { - System.out.println("代码块调用"); - } - static { - System.out.println("静态代码块调用"); - } - } -```` -## Java代码块、构造方法(包含继承关系)的执行顺序 - -这是一道常见的面试题,要回答这个问题,先看看这个实例吧。 - -一共3个类:A、B、C -其中A是B的父类,C无继承仅作为输出 - -A类: -```` - public class A { - - static { - Log.i("HIDETAG", "A静态代码块"); - } - - private static C c = new C("A静态成员"); - private C c1 = new C("A成员"); - - { - Log.i("HIDETAG", "A代码块"); - } - - static { - Log.i("HIDETAG", "A静态代码块2"); - } - - public A() { - Log.i("HIDETAG", "A构造方法"); - } - - } -```` -B类: -```` - public class B extends A { - - private static C c1 = new C("B静态成员"); - - { - Log.i("HIDETAG", "B代码块"); - } - - private C c = new C("B成员"); - - static { - Log.i("HIDETAG", "B静态代码块2"); - } - - static { - Log.i("HIDETAG", "B静态代码块"); - } - - public B() { - Log.i("HIDETAG", "B构造方法"); - - } - - } -```` -C类: -```` - public class C { - - public C(String str) { - Log.i("HIDETAG", str + "构造方法"); - } - } -```` -执行语句:new B(); - -输出结果如下: - - I/HIDETAG: A静态代码块 - I/HIDETAG: A静态成员构造方法 - I/HIDETAG: A静态代码块2 - I/HIDETAG: B静态成员构造方法 - I/HIDETAG: B静态代码块2 - I/HIDETAG: B静态代码块 - I/HIDETAG: A成员构造方法 - I/HIDETAG: A代码块 - I/HIDETAG: A构造方法 - I/HIDETAG: B代码块 - I/HIDETAG: B成员构造方法 - I/HIDETAG: B构造方法 -得出结论: - - 执行顺序依次为: - 父类的静态成员和代码块 - 子类静态成员和代码块 - 父类成员初始化和代码快 - 父类构造方法 - 子类成员初始化和代码块 - 子类构造方法 - -注意:可以发现,同一级别的代码块和成员初始化是按照代码顺序从上到下依次执行 - -**看完上面这个demo,再来看看下面这道题,看看你搞得定吗?** - -看下面一段代码,求执行顺序: -```` - class A { - public A() { - System.out.println("1A类的构造方法"); - } - { - System.out.println("2A类的构造快"); - } - static { - System.out.println("3A类的静态块"); - } - } - - public class B extends A { - public B() { - System.out.println("4B类的构造方法"); - } - { - System.out.println("5B类的构造快"); - } - static { - System.out.println("6B类的静态块"); - } - public static void main(String[] args) { - System.out.println("7"); - new B(); - new B(); - System.out.println("8"); - } - } -```` - -执行顺序结果为:367215421548 - -为什么呢? - -首先我们要知道下面这5点: - -每次new都会执行构造方法以及构造块。 -构造块的内容会在构造方法之前执行。 -非主类的静态块会在类加载时,构造方法和构造块之前执行,切只执行一次。 -主类(public class)里的静态块会先于main执行。 -继承中,子类实例化,会先执行父类的构造方法,产生父类对象,再调用子类构造方法。 -所以题目里,由于主类B继承A,所以会先加载A,所以第一个执行的是第3句。 - -从第4点我们知道6会在7之前执行,所以前三句是367。 - -之后实例化了B两次,每次都会先实例化他的父类A,然后再实例化B,而根据第1、2、5点,知道顺序为2154。 - -最后执行8 - -所以顺序是367215421548 - -## 参考文章 - -https://blog.csdn.net/likunkun__/article/details/83066062 -https://www.jianshu.com/p/6877aae403f7 -https://www.jianshu.com/p/49e45af288ea -https://blog.csdn.net/du_du1/article/details/91383128 -http://c.biancheng.net/view/976.html -https://blog.csdn.net/evilcry2012/article/details/79499786 -https://www.jb51.net/article/129990.htm - diff --git "a/docs/Java/basic/\345\217\215\345\260\204.md" "b/docs/Java/basic/\345\217\215\345\260\204.md" deleted file mode 100644 index 14629eb..0000000 --- "a/docs/Java/basic/\345\217\215\345\260\204.md" +++ /dev/null @@ -1,614 +0,0 @@ -# 目录 - - * [回顾:什么是反射?](#回顾:什么是反射?) - * [反射的主要用途](#反射的主要用途) - * [反射的基础:关于Class类](#反射的基础:关于class类) - * [Java为什么需要反射?反射要解决什么问题?](#java为什么需要反射?反射要解决什么问题?) - * [反射的基本运用](#反射的基本运用) - * [判断是否为某个类的实例](#判断是否为某个类的实例) - * [创建实例](#创建实例) - * [获取方法](#获取方法) - * [获取构造器信息](#获取构造器信息) - * [获取类的成员变量(字段)信息](#获取类的成员变量(字段)信息) - * [调用方法](#调用方法) - * [利用反射创建数组](#利用反射创建数组) - * [Java反射常见面试题](#java反射常见面试题) - * [什么是反射?](#什么是反射?) - * [哪里用到反射机制?](#哪里用到反射机制?) - * [什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作?](#什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作?) - * [反射机制的优缺点?](#反射机制的优缺点?) - * [动态代理是什么?有哪些应用?](#动态代理是什么?有哪些应用?) - * [怎么实现动态代理?](#怎么实现动态代理?) - * [Java反射机制的作用](#java反射机制的作用) - * [如何使用Java的反射?](#如何使用java的反射) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - -## 回顾:什么是反射? - -反射(Reflection)是Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。 -Oracle官方对反射的解释是 - -> Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. - -> The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control. -> -> 简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。 -> -> 程序中一般的对象的类型都是在编译期就确定下来的,而Java反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。 -> -> 反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。 - -Java反射框架主要提供以下功能: - -> 1.在运行时判断任意一个对象所属的类; - -> 2.在运行时构造任意一个类的对象; - -> 3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法); - -> 4.在运行时调用任意一个对象的方法 - -> 重点:是运行时而不是编译时 - - -## 反射的主要用途 - -> 很多人都认为反射在实际的Java开发应用中并不广泛,其实不然。 - -> 当我们在使用IDE(如Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。 - -> 反射最重要的用途就是开发各种通用框架。 - -> 很多框架(比如Spring)都是配置化的(比如通过XML文件配置JavaBean,Action之类的),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。 - -> 对于框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。 - -## 反射的基础:关于Class类 - -更多关于Class类和Object类的原理和介绍请见上一节 - -> 1、Class是一个类,一个描述类的类(也就是描述类本身),封装了描述方法的Method,描述字段的Filed,描述构造器的Constructor等属性 -> -> 2、对象照镜子后(反射)可以得到的信息:某个类的数据成员名、方法和构造器、某个类到底实现了哪些接口。 -> -> 3、对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个Class对象包含了特定某个类的有关信息。 -> -> 4、Class 对象只能由系统建立对象 -> -> 5、一个类在 JVM 中只会有一个Class实例 -> -```` -//总结一下就是,JDK有一个类叫做Class,这个类用来封装所有Java类型,包括这些类的所有信息,JVM中类信息是放在方法区的。 - -//所有类在加载后,JVM会为其在堆中创建一个Class<类名称>的对象,并且每个类只会有一个Class对象,这个类的所有对象都要通过Class<类名称>来进行实例化。 - -//上面说的是JVM进行实例化的原理,当然实际上在Java写代码时只需要用 类名称就可以进行实例化了。 - -public final class Class implements java.io.Serializable, - GenericDeclaration, - Type, - AnnotatedElement { - //通过类名.class获得唯一的Class对象。 - Class cls = UserBean.class; - //通过integer.TYPEl来获取Class对象 - Class inti = Integer.TYPE; - //接口本质也是一个类,一样可以通过.class获取 - Class userClass = User.class; -} -```` -JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 - -要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象. - -以上的总结就是什么是反射 -反射就是把java类中的各种成分映射成一个个的Java对象 -例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述) -如图是类的正常加载过程:反射的原理在与class对象。 -熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象。 - -![](https://java-tutorial.oss-cn-shanghai.aliyuncs.com/20230403211245.png) -## Java为什么需要反射?反射要解决什么问题? -Java中编译类型有两种: - -静态编译:在编译时确定类型,绑定对象即通过。 -动态编译:运行时确定类型,绑定对象。动态编译最大限度地发挥了Java的灵活性,体现了多态的应用,可以减低类之间的耦合性。 -Java反射是Java被视为动态(或准动态)语言的一个关键性质。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public、static等)、superclass(例如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。 - -Reflection可以在运行时加载、探知、使用编译期间完全未知的classes。即Java程序可以加载一个运行时才得知名称的class,获取其完整构造,并生成其对象实体、或对其fields设值、或唤起其methods。 - -反射(reflection)允许静态语言在运行时(runtime)检查、修改程序的结构与行为。 -在静态语言中,使用一个变量时,必须知道它的类型。在Java中,变量的类型信息在编译时都保存到了class文件中,这样在运行时才能保证准确无误;换句话说,程序在运行时的行为都是固定的。如果想在运行时改变,就需要反射这东西了。 - -实现Java反射机制的类都位于java.lang.reflect包中: - -Class类:代表一个类 -Field类:代表类的成员变量(类的属性) -Method类:代表类的方法 -Constructor类:代表类的构造方法 -Array类:提供了动态创建数组,以及访问数组的元素的静态方法 -一句话概括就是使用反射可以赋予jvm动态编译的能力,否则类的元数据信息只能用静态编译的方式实现,例如热加载,Tomcat的classloader等等都没法支持。 - -## 反射的基本运用 - -上面我们提到了反射可以用于判断任意对象所属的类,获得Class对象,构造任意一个对象以及调用一个对象。这里我们介绍一下基本反射功能的实现(反射相关的类一般都在java.lang.relfect包里)。 - -1、获得Class对象方法有三种 - -(1)使用Class类的forName静态方法: -```` -public static Class forName(String className) -```` -在JDBC开发中常用此方法加载数据库驱动: -要使用全类名来加载这个类,一般数据库驱动的配置信息会写在配置文件中。加载这个驱动前要先导入jar包 -```` -Class.forName(driver); -```` -(2)直接获取某一个对象的class,比如: -```` -//Class是一个泛型表示,用于获取一个类的类型。 -Class klass = int.class; -Class classInt = Integer.TYPE; -```` -(3)调用某个对象的getClass()方法,比如: -```` -StringBuilder str = new StringBuilder("123"); -Class klass = str.getClass(); -```` -## 判断是否为某个类的实例 - -一般地,我们用instanceof关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的isInstance()方法来判断是否为某个类的实例,它是一个Native方法: -```` -public native boolean isInstance(Object obj); -```` -## 创建实例 - -通过反射来生成对象主要有两种方式。 - -(1)使用Class对象的newInstance()方法来创建Class对象对应类的实例。 - -注意:利用newInstance创建对象:调用的类必须有无参的构造器 -```` -//Class代表任何类的一个类对象。 -//使用这个类对象可以为其他类进行实例化 -//因为jvm加载类以后自动在堆区生成一个对应的*.Class对象 -//该对象用于让JVM对进行所有*对象实例化。 -Class c = String.class; - -//Class 中的 ? 是通配符,其实就是表示任意符合泛类定义条件的类,和直接使用 Class -//效果基本一致,但是这样写更加规范,在某些类型转换时可以避免不必要的 unchecked 错误。 - -Object str = c.newInstance(); -```` -(2)先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。 -```` - //获取String所对应的Class对象 - Class c = String.class; - //获取String类带一个String参数的构造器 - Constructor constructor = c.getConstructor(String.class); - //根据构造器创建实例 - Object obj = constructor.newInstance("23333"); - System.out.println(obj); -```` -## 获取方法 -获取某个Class对象的方法集合,主要有以下几个方法: - -getDeclaredMethods()方法返回类或接口声明的所有方法,==包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法==。 -```` -public Method[] getDeclaredMethods() throws SecurityException -```` -getMethods()方法返回某个类的所有公用(public)方法,==包括其继承类的公用方法。== -```` -public Method[] getMethods() throws SecurityException -```` -getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象 -```` -public Method getMethod(String name, Class... parameterTypes) -```` -只是这样描述的话可能难以理解,我们用例子来理解这三个方法: -本文中的例子用到了以下这些类,用于反射的测试。 - -```` - //注解类,可可用于表示方法,可以通过反射获取注解的内容。 - //Java注解的实现是很多注框架实现注解配置的基础 - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) - public @interface Invoke { - } -```` -userbean的父类personbean -```` - public class PersonBean { - private String name; - - int id; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -```` -接口user -```` - public interface User { - public void login (); - - } -```` -userBean实现user接口,继承personbean -```` - public class UserBean extends PersonBean implements User{ - @Override - public void login() { - - } - - class B { - - } - - public String userName; - protected int i; - static int j; - private int l; - private long userId; - public UserBean(String userName, long userId) { - this.userName = userName; - this.userId = userId; - } - public String getName() { - return userName; - } - public long getId() { - return userId; - } - @Invoke - public static void staticMethod(String devName,int a) { - System.out.printf("Hi %s, I'm a static method", devName); - } - @Invoke - public void publicMethod() { - System.out.println("I'm a public method"); - } - @Invoke - private void privateMethod() { - System.out.println("I'm a private method"); - } - } -```` -1 getMethods和getDeclaredMethods的区别 -```` -public class 动态加载类的反射 { - public static void main(String[] args) { - try { - Class clazz = Class.forName("com.javase.反射.UserBean"); - for (Field field : clazz.getDeclaredFields()) { -// field.setAccessible(true); - System.out.println(field); - } - //getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。 - System.out.println("------共有方法------"); -// getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。 -// getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。 - for (Method method : clazz.getMethods()) { - String name = method.getName(); - System.out.println(name); - //打印出了UserBean.java的所有方法以及父类的方法 - } - System.out.println("------独占方法------"); - - for (Method method : clazz.getDeclaredMethods()) { - String name = method.getName(); - System.out.println(name); - } - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} - -```` - -2 打印一个类的所有方法及详细信息: -```` -public class 打印所有方法 { - - public static void main(String[] args) { - Class userBeanClass = UserBean.class; - Field[] fields = userBeanClass.getDeclaredFields(); - //注意,打印方法时无法得到局部变量的名称,因为jvm只知道它的类型 - Method[] methods = userBeanClass.getDeclaredMethods(); - for (Method method : methods) { - //依次获得方法的修饰符,返回类型和名称,外加方法中的参数 - String methodString = Modifier.toString(method.getModifiers()) + " " ; // private static - methodString += method.getReturnType().getSimpleName() + " "; // void - methodString += method.getName() + "("; // staticMethod - Class[] parameters = method.getParameterTypes(); - Parameter[] p = method.getParameters(); - - for (Class parameter : parameters) { - methodString += parameter.getSimpleName() + " " ; // String - } - methodString += ")"; - System.out.println(methodString); - } - //注意方法只能获取到其类型,拿不到变量名 -/* public String getName() - public long getId() - public static void staticMethod(String int ) - public void publicMethod() - private void privateMethod()*/ - } -} -```` -## 获取构造器信息 - -获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例: -```` -public class 打印构造方法 { - public static void main(String[] args) { - // constructors - Class clazz = UserBean.class; - - Class userBeanClass = UserBean.class; - //获得所有的构造方法 - Constructor[] constructors = userBeanClass.getDeclaredConstructors(); - for (Constructor constructor : constructors) { - String s = Modifier.toString(constructor.getModifiers()) + " "; - s += constructor.getName() + "("; - //构造方法的参数类型 - Class[] parameters = constructor.getParameterTypes(); - for (Class parameter : parameters) { - s += parameter.getSimpleName() + ", "; - } - s += ")"; - System.out.println(s); - //打印结果//public com.javase.反射.UserBean(String, long, ) - - } - } -} -```` -## 获取类的成员变量(字段)信息 -主要是这几个方法,在此不再赘述: - -getFiled: 访问公有的成员变量 -getDeclaredField:所有已声明的成员变量。但不能得到其父类的成员变量 -getFileds和getDeclaredFields用法同上(参照Method) - -```` -public class 打印成员变量 { - public static void main(String[] args) { - Class userBeanClass = UserBean.class; - //获得该类的所有成员变量,包括static private - Field[] fields = userBeanClass.getDeclaredFields(); - - for(Field field : fields) { - //private属性即使不用下面这个语句也可以访问 -// field.setAccessible(true); - - //因为类的私有域在反射中默认可访问,所以flag默认为true。 - String fieldString = ""; - fieldString += Modifier.toString(field.getModifiers()) + " "; // `private` - fieldString += field.getType().getSimpleName() + " "; // `String` - fieldString += field.getName(); // `userName` - fieldString += ";"; - System.out.println(fieldString); - - //打印结果 -// public String userName; -// protected int i; -// static int j; -// private int l; -// private long userId; - } - - } -} -```` -## 调用方法 -当我们从类中获取了一个方法后,我们就可以用invoke()方法来调用这个方法。invoke方法的原型为: -```` -public Object invoke(Object obj, Object... args) - throws IllegalAccessException, IllegalArgumentException, - InvocationTargetException - -public class 使用反射调用方法 { - public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException { - Class userBeanClass = UserBean.class; - //获取该类所有的方法,包括静态方法,实例方法。 - //此处也包括了私有方法,只不过私有方法在用invoke访问之前要设置访问权限 - //也就是使用setAccessible使方法可访问,否则会抛出异常 -// // IllegalAccessException的解释是 -// * An IllegalAccessException is thrown when an application tries -// * to reflectively create an instance (other than an array), -// * set or get a field, or invoke a method, but the currently -// * executing method does not have access to the definition of -// * the specified class, field, method or constructor. - -// getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。 -// getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。 - - //就是说,当这个类,域或者方法被设为私有访问,使用反射调用但是却没有权限时会抛出异常。 - Method[] methods = userBeanClass.getDeclaredMethods(); // 获取所有成员方法 - for (Method method : methods) { - //反射可以获取方法上的注解,通过注解来进行判断 - if (method.isAnnotationPresent(Invoke.class)) { // 判断是否被 @Invoke 修饰 - //判断方法的修饰符是是static - if (Modifier.isStatic(method.getModifiers())) { // 如果是 static 方法 - //反射调用该方法 - //类方法可以直接调用,不必先实例化 - method.invoke(null, "wingjay",2); // 直接调用,并传入需要的参数 devName - } else { - //如果不是类方法,需要先获得一个实例再调用方法 - //传入构造方法需要的变量类型 - Class[] params = {String.class, long.class}; - //获取该类指定类型的构造方法 - //如果没有这种类型的方法会报错 - Constructor constructor = userBeanClass.getDeclaredConstructor(params); // 获取参数格式为 String,long 的构造函数 - //通过构造方法的实例来进行实例化 - Object userBean = constructor.newInstance("wingjay", 11); // 利用构造函数进行实例化,得到 Object - if (Modifier.isPrivate(method.getModifiers())) { - method.setAccessible(true); // 如果是 private 的方法,需要获取其调用权限 -// Set the {@code accessible} flag for this object to -// * the indicated boolean value. A value of {@code true} indicates that -// * the reflected object should suppress Java language access -// * checking when it is used. A value of {@code false} indicates -// * that the reflected object should enforce Java language access checks. - //通过该方法可以设置其可见或者不可见,不仅可以用于方法 - //后面例子会介绍将其用于成员变量 - //打印结果 -// I'm a public method -// Hi wingjay, I'm a static methodI'm a private method - } - method.invoke(userBean); // 调用 method,无须参数 - } - } - } - } -} -```` -## 利用反射创建数组 - -数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子: -```` -public class 用反射创建数组 { - public static void main(String[] args) { - Class cls = null; - try { - cls = Class.forName("java.lang.String"); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - Object array = Array.newInstance(cls,25); - //往数组里添加内容 - Array.set(array,0,"hello"); - Array.set(array,1,"Java"); - Array.set(array,2,"fuck"); - Array.set(array,3,"Scala"); - Array.set(array,4,"Clojure"); - //获取某一项的内容 - System.out.println(Array.get(array,3)); - //Scala - } - -} -```` -其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是: -```` -public static Object newInstance(Class componentType, int length) - throws NegativeArraySizeException { - return newArray(componentType, length); - } -```` -而newArray()方法是一个Native方法,它在Hotspot JVM里的具体实现我们后边再研究,这里先把源码贴出来 -```` -private static native Object newArray(Class componentType, int length) - throws NegativeArraySizeException; - -```` - - -## Java反射常见面试题 - -### 什么是反射? - -反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。 - -### 哪里用到反射机制? - -JDBC中,利用反射动态加载了数据库驱动程序。 -Web服务器中利用反射调用了Sevlet的服务方法。 -Eclispe等开发工具利用反射动态刨析对象的类型与结构,动态提示对象的属性和方法。 -很多框架都用到反射机制,注入属性,调用方法,如Spring。 - -### 什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作? - -对象序列化,将对象中的数据编码为字节序列的过程。 -反序列化;将对象的编码字节重新反向解码为对象的过程。 -JAVA提供了API实现了对象的序列化和反序列化的功能,使用这些API时需要遵守如下约定: -被序列化的对象类型需要实现序列化接口,此接口是标志接口,没有声明任何的抽象方法,JAVA编译器识别这个接口,自动的为这个类添加序列化和反序列化方法。 -为了保持序列化过程的稳定,建议在类中添加序列化版本号。 -不想让字段放在硬盘上就加transient -以下情况需要使用 Java 序列化: -想把的内存中的对象状态保存到一个文件中或者数据库中时候; -想用套接字在网络上传送对象的时候; -想通过RMI(远程方法调用)传输对象的时候。 - -### 反射机制的优缺点? - -优点:可以动态执行,在运行期间根据业务功能动态执行方法、访问属性,最大限度发挥了java的灵活性。 -缺点:对性能有影响,这类操作总是慢于直接执行java代码。 - -### 动态代理是什么?有哪些应用? - -动态代理是运行时动态生成代理类。 -动态代理的应用有 Spring AOP数据查询、测试框架的后端 mock、rpc,Java注解对象获取等。 - -### 怎么实现动态代理? - -JDK 原生动态代理和 cglib 动态代理。 -JDK 原生动态代理是基于接口实现的,而 cglib 是基于继承当前类的子类实现的。 - -### Java反射机制的作用 - -在运行时判断任意一个对象所属的类 -在运行时构造任意一个类的对象 -在运行时判断任意一个类所具有的成员变量和方法 -在运行时调用任意一个对象的方法 - -### 如何使用Java的反射? - -通过一个全限类名创建一个对象 - -Class.forName(“全限类名”); 例如:com.mysql.jdbc.Driver Driver类已经被加载到 jvm中,并且完成了类的初始化工作就行了 -类名.class; 获取Class<?> clz 对象 -对象.getClass(); - -获取构造器对象,通过构造器new出一个对象 - -Clazz.getConstructor([String.class]); -Con.newInstance([参数]); -通过class对象创建一个实例对象(就相当与new类名()无参构造器) -Cls.newInstance(); - -通过class对象获得一个属性对象 - -Field c=cls.getFields():获得某个类的所有的公共(public)的字段,包括父类中的字段。 -Field c=cls.getDeclaredFields():获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的声明字段 - -通过class对象获得一个方法对象 - -Cls.getMethod(“方法名”,class……parameaType);(只能获取公共的) -Cls.getDeclareMethod(“方法名”);(获取任意修饰的方法,不能执行私有) -M.setAccessible(true);(让私有的方法可以执行) -让方法执行 -1). Method.invoke(obj实例对象,obj可变参数);-----(是有返回值的) - -## 参考文章 -http://www.cnblogs.com/peida/archive/2013/04/26/3038503.html -http://www.cnblogs.com/whoislcj/p/5671622.html -https://blog.csdn.net/grandgrandpa/article/details/84832343 -http://blog.csdn.net/lylwo317/article/details/52163304 -https://blog.csdn.net/qq_37875585/article/details/89340495 - diff --git "a/docs/Java/basic/\345\244\232\347\272\277\347\250\213.md" "b/docs/Java/basic/\345\244\232\347\272\277\347\250\213.md" deleted file mode 100644 index 35e3ada..0000000 --- "a/docs/Java/basic/\345\244\232\347\272\277\347\250\213.md" +++ /dev/null @@ -1,634 +0,0 @@ -# 目录 - * [Java中的线程](#java中的线程) - * [Java线程状态机](#java线程状态机) - * [一个线程的生命周期](#一个线程的生命周期) - * [Java多线程实战](#java多线程实战) - * [多线程的实现](#多线程的实现) - * [线程状态转换](#线程状态转换) - * [Java Thread常用方法](#java-thread常用方法) - * [Thread#yield():](#threadyield:) - * [Thread.interrupt():](#threadinterrupt:) - * [Thread#interrupted(),返回true或者false:](#threadinterrupted,返回true或者false:) - * [Thread.isInterrupted(),返回true或者false:](#threadisinterrupted,返回true或者false:) - * [Thread#join(),Thread#join(time):](#threadjoin,threadjointime:) - * [构造方法和守护线程](#构造方法和守护线程) - * [启动线程的方式和isAlive方法](#启动线程的方式和isalive方法) - * [Java多线程优先级](#java多线程优先级) - * [Java多线程面试题](#java多线程面试题) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - - - - -## Java中的线程 - -Java之父对线程的定义是: -> 线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)。java.lang.Thread对象负责统计和控制这种行为。 - -> 每个程序都至少拥有一个线程-即作为Java虚拟机(JVM)启动参数运行在主类main方法的线程。在Java虚拟机初始化过程中也可能启动其他的后台线程。这种线程的数目和种类因JVM的实现而异。然而所有用户级线程都是显式被构造并在主线程或者是其他用户线程中被启动。 - - 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。在这之前,首先让我们来了解下在操作系统中进程和线程的区别: - -   进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位) - -   线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位) - -   线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 - -   多进程是指操作系统能同时运行多个任务(程序)。 - -   多线程是指在同一程序中有多个顺序流在执行。 - - 在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口.(其实准确来讲,应该有三种,还有一种是实现Callable接口,并与Future、线程池结合使用 - -## Java线程状态机 - - -Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 - -多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。 - -这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。 - -多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。 - -* * * - -### 一个线程的生命周期 - -线程是一个动态执行的过程,它也有一个从产生到死亡的过程。 - -下图显示了一个线程完整的生命周期。 - -![](https://www.runoob.com/wp-content/uploads/2014/01/java-thread.jpg) - -* **新建状态:** - - 使用**new**关键字和**Thread**类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序**start()**这个线程。 - -* **就绪状态:** - - 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。 - -* **运行状态:** - - 如果就绪状态的线程获取 CPU 资源,就可以执行**run()**,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 - -* **阻塞状态:** - - 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种: - - * 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。 - - * 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。 - - * 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。 - -* **死亡状态:** - - 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。 -## Java多线程实战 -## 多线程的实现 - - public class 多线程实例 { - - //继承thread - @Test - public void test1() { - class A extends Thread { - @Override - public void run() { - System.out.println("A run"); - } - } - A a = new A(); - a.start(); - } - - //实现Runnable - @Test - public void test2() { - class B implements Runnable { - - @Override - public void run() { - System.out.println("B run"); - } - } - B b = new B(); - //Runable实现类需要由Thread类包装后才能执行 - new Thread(b).start(); - } - - //有返回值的线程 - @Test - public void test3() { - Callable callable = new Callable() { - int sum = 0; - @Override - public Object call() throws Exception { - for (int i = 0;i < 5;i ++) { - sum += i; - } - return sum; - } - }; - //这里要用FutureTask,否则不能加入Thread构造方法 - FutureTask futureTask = new FutureTask(callable); - new Thread(futureTask).start(); - try { - System.out.println(futureTask.get()); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - //线程池实现 - @Test - public void test4() { - ExecutorService executorService = Executors.newFixedThreadPool(5); - //execute直接执行线程 - executorService.execute(new Thread()); - executorService.execute(new Runnable() { - @Override - public void run() { - System.out.println("runnable"); - } - }); - //submit提交有返回结果的任务,运行完后返回结果。 - Future future = executorService.submit(new Callable() { - @Override - public String call() throws Exception { - return "a"; - } - }); - try { - System.out.println(future.get()); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - - ArrayList list = new ArrayList<>(); - //有返回值的线程组将返回值存进集合 - for (int i = 0;i < 5;i ++ ) { - int finalI = i; - Future future1 = executorService.submit(new Callable() { - @Override - public String call() throws Exception { - return "res" + finalI; - } - }); - try { - list.add((String) future1.get()); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - for (String s : list) { - System.out.println(s); - } - } - } - -## 线程状态转换 - - public class 线程的状态转换 { - //一开始线程是init状态,结束时是terminated状态 - class t implements Runnable { - private String name; - public t(String name) { - this.name = name; - } - @Override - public void run() { - System.out.println(name + "run"); - } - } - - //测试join,父线程在子线程运行时进入waiting状态 - @Test - public void test1() throws InterruptedException { - Thread dad = new Thread(new Runnable() { - Thread son = new Thread(new t("son")); - @Override - public void run() { - System.out.println("dad init"); - son.start(); - try { - //保证子线程运行完再运行父线程 - son.join(); - System.out.println("dad run"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }); - //调用start,线程进入runnable状态,等待系统调度 - dad.start(); - //在父线程中对子线程实例使用join,保证子线程在父线程之前执行完 - - } - - //测试sleep - @Test - public void test2(){ - Thread t1 = new Thread(new Runnable() { - @Override - public void run() { - System.out.println("t1 run"); - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }); - - //主线程休眠。进入time waiting状态 - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - t1.start(); - - } - - //线程2进入blocked状态。 - public static void main(String[] args) { - test4(); - Thread.yield();//进入runnable状态 - } - - //测试blocked状态 - public static void test4() { - class A { - //线程1获得实例锁以后线程2无法获得实例锁,所以进入blocked状态 - synchronized void run() { - while (true) { - System.out.println("run"); - } - } - } - A a = new A(); - new Thread(new Runnable() { - @Override - public void run() { - System.out.println("t1 get lock"); - a.run(); - } - }).start(); - new Thread(new Runnable() { - @Override - public void run() { - System.out.println("t2 get lock"); - a.run(); - } - }).start(); - - } - - //volatile保证线程可见性 - volatile static int flag = 1; - //object作为锁对象,用于线程使用wait和notify方法 - volatile static Object o = new Object(); - //测试wait和notify - //wait后进入waiting状态,被notify进入blocked(阻塞等待锁释放)或者runnable状态(获取到锁) - public void test5() { - new Thread(new Runnable() { - @Override - public void run() { - //wait和notify只能在同步代码块内使用 - synchronized (o) { - while (true) { - if (flag == 0) { - try { - Thread.sleep(2000); - System.out.println("thread1 wait"); - //释放锁,线程挂起进入object的等待队列,后续代码运行 - o.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - System.out.println("thread1 run"); - System.out.println("notify t2"); - flag = 0; - //通知等待队列的一个线程获取锁 - o.notify(); - } - } - } - }).start(); - //解释同上 - new Thread(new Runnable() { - @Override - public void run() { - while (true) { - synchronized (o) { - if (flag == 1) { - try { - Thread.sleep(2000); - System.out.println("thread2 wait"); - o.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - System.out.println("thread2 run"); - System.out.println("notify t1"); - flag = 1; - o.notify(); - } - } - } - }).start(); - } - - //输出结果是 - // thread1 run - // notify t2 - // thread1 wait - // thread2 run - // notify t1 - // thread2 wait - // thread1 run - // notify t2 - //不断循环 - - } -## Java Thread常用方法 - -### Thread#yield(): - -执行此方法会向系统线程调度器(Schelduler)发出一个暗示,告诉其当前JAVA线程打算放弃对CPU的使用,但该暗示,有可能被调度器忽略。使用该方法,可以防止线程对CPU的过度使用,提高系统性能。 - -Thread#sleep(time)或Thread.sleep(time, nanos): - -使当前线程进入休眠阶段,状态变为:TIME_WAITING - -### Thread.interrupt(): - -中断当前线程的执行,允许当前线程对自身进行中断,否则将会校验调用方线程是否有对该线程的权限。 - -如果当前线程因被调用Object#wait(),Object#wait(long, int), 或者线程本身的join(), join(long),sleep()处于阻塞状态中,此时调用interrupt方法会使抛出InterruptedException,而且线程的阻塞状态将会被清除。 - -### Thread#interrupted(),返回true或者false: - -查看当前线程是否处于中断状态,这个方法比较特殊之处在于,如果调用成功,会将当前线程的interrupt status清除。所以如果连续2次调用该方法,第二次将返回false。 - -### Thread.isInterrupted(),返回true或者false: - -与上面方法相同的地方在于,该方法返回当前线程的中断状态。不同的地方在于,它不会清除当前线程的interrupt status状态。 - -### Thread#join(),Thread#join(time): - -A线程调用B线程的join()方法,将会使A等待B执行,直到B线程终止。如果传入time参数,将会使A等待B执行time的时间,如果time时间到达,将会切换进A线程,继续执行A线程。 - - -## 构造方法和守护线程 - - 构造方法 - Thread类中不同的构造方法接受如下参数的不同组合: - - 一个Runnable对象,这种情况下,Thread.start方法将会调用对应Runnable对象的run方法。如果没有提供Runnable对象,那么就会立即得到一个Thread.run的默认实现。 - - 一个作为线程标识名的String字符串,该标识在跟踪和调试过程中会非常有用,除此别无它用。 - - 线程组(ThreadGroup),用来放置新创建的线程,如果提供的ThreadGroup不允许被访问,那么就会抛出一个SecurityException 。 - - - - Thread对象拥有一个守护(daemon)标识属性,这个属性无法在构造方法中被赋值,但是可以在线程启动之前设置该属性(通过setDaemon方法)。 - - 当程序中所有的非守护线程都已经终止,调用setDaemon方法可能会导致虚拟机粗暴的终止线程并退出。 - - isDaemon方法能够返回该属性的值。守护状态的作用非常有限,即使是后台线程在程序退出的时候也经常需要做一些清理工作。 - - (daemon的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程,比如打印机队列管理,它总是在系统中运行。) - -## 启动线程的方式和isAlive方法 - -启动线程 -调用start方法会触发Thread实例以一个新的线程启动其run方法。新线程不会持有调用线程的任何同步锁。 - -当一个线程正常地运行结束或者抛出某种未检测的异常(比如,运行时异常(RuntimeException),错误(ERROR) 或者其子类)线程就会终止。 - -**当线程终止之后,是不能被重新启动的。在同一个Thread上调用多次start方法会抛出InvalidThreadStateException异常。** - -如果线程已经启动但是还没有终止,那么调用isAlive方法就会返回true.即使线程由于某些原因处于阻塞(Blocked)状态该方法依然返回true。 - -如果线程已经被取消(cancelled),那么调用其isAlive在什么时候返回false就因各Java虚拟机的实现而异了。没有方法可以得知一个处于非活动状态的线程是否已经被启动过了。 - -## Java多线程优先级 - -**Java的线程实现基本上都是内核级线程的实现,所以Java线程的具体执行还取决于操作系统的特性。** - -Java虚拟机为了实现跨平台(不同的硬件平台和各种操作系统)的特性,Java语言在线程调度与调度公平性上未作出任何的承诺,甚至都不会严格保证线程会被执行。但是Java线程却支持优先级的方法,这些方法会影响线程的调度: - -每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10) -默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5). - -线程的当前优先级可以通过getPriority方法获得。 -线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。 - -## Java多线程面试题 - -这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 - -这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都看过,但是本文写作的重心就是所有的问题都会按照自己的理解回答一遍,不会去看网上的答案,因此可能有些问题讲的不对,能指正的希望大家不吝指教。 - -> **1、多线程有什么用?** - -一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓"知其然知其所以然","会用"只是"知其然","为什么用"才是"知其所以然",只有达到"知其然知其所以然"的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法: - -**1)发挥多核CPU的优势** - -随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。**单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了**。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。 - -**2)防止阻塞** - -从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。 - -**3)便于建模** - -这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。 - -> **2、创建线程的方式** - -比较常见的一个问题了,一般就是两种: - -1)继承Thread类 - -2)实现Runnable接口 - -至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,**面向接口编程**也是设计模式6大原则的核心。 - - -> **3、start()方法和run()方法的区别** - -只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。 - -> **4、Runnable接口和Callable接口的区别** - -有点深的问题了,也看出一个Java程序员学习知识的广度。 - -Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 - -这其实是很有用的一个特性,因为**多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性**,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。 - -> **5、CyclicBarrier和CountDownLatch的区别** - -两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于: - -1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。 - -2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。 - -3) CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。 - -> **6、volatile关键字的作用** - -一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了,可以参见第31点,volatile关键字的作用主要有两个: - -1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。 - -2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是**Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互**,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。 - -从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger,更多详情请点击[这里](http://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247483916&idx=1&sn=89daf388da0d6fe40dc54e9a4018baeb&chksm=eb53873adc240e2cf55400f3261228d08fc943c4f196566e995681549c47630b70ac01b75031&scene=21#wechat_redirect)进行学习。 - -> **7、什么是线程安全** - -又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:**如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的**。 - -这个问题有值得一提的地方,就是线程安全也是有几个级别的: - -**1)不可变** - -像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用 - -**2)绝对线程安全** - -不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet - -**3)相对线程安全** - -相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是**fail-fast机制**。 - -**4)线程非安全** - -这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类,点击[这里](http://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247486446&idx=2&sn=cb4f3aff0427c5ac3ffe5b61e150f506&chksm=eb538ed8dc2407ceb91fffe3c3bd559d9b15537446f84eb3bfb1a80e67f5efee176ca468a07b&scene=21#wechat_redirect)了解为什么不安全。 - -> **8、Java中如何获取到线程dump文件** - -死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步: - -1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java - -2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid - -另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。 - -> **9、一个线程如果出现了运行时异常会怎么样** - -如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:**如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放** - -> **10、如何在两个线程之间共享数据** - -通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的 - -> **11、sleep方法和wait方法有什么区别** - -这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器 - -> **12、生产者消费者模型的作用是什么** - -这个问题很理论,但是很重要: - -1)**通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率**,这是生产者消费者模型最重要的作用 - -2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约 - -> **13、ThreadLocal有什么用** - -简单说ThreadLocal就是一种以**空间换时间**的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了 - -> **14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用** - -这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁 - -> **15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别** - -wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:**wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器**。 - -> **16、为什么要使用线程池** - -避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。点击[这里](https://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247483824&idx=1&sn=7e34a3944a93d649d78d618cf04e0619&scene=21#wechat_redirect)学习线程池详解。 - -> **17、怎么唤醒一个阻塞的线程** - -如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。 - -> **18、不可变对象对多线程有什么帮助** - -前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。 - -> **19、什么是多线程的上下文切换** - -多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。 - -> **20、线程类的构造方法、静态块是被哪个线程调用的** - -这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。 - -如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么: - -1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的 - -2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的 - - -> **21、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?** - -这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是: - -1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换 - -2)并发不高、任务执行时间长的业务要区分开看: - -a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务 - -b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换 - -c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。 - -## 参考文章 -https://blog.csdn.net/zl1zl2zl3/article/details/81868173 -https://www.runoob.com/java/java-multithreading.html -https://blog.csdn.net/qq_38038480/article/details/80584715 -https://blog.csdn.net/tongxuexie/article/details/80145663 -https://www.cnblogs.com/snow-flower/p/6114765.html - - diff --git "a/docs/Java/basic/\345\272\217\345\210\227\345\214\226\345\222\214\345\217\215\345\272\217\345\210\227\345\214\226.md" "b/docs/Java/basic/\345\272\217\345\210\227\345\214\226\345\222\214\345\217\215\345\272\217\345\210\227\345\214\226.md" deleted file mode 100644 index c6d33a7..0000000 --- "a/docs/Java/basic/\345\272\217\345\210\227\345\214\226\345\222\214\345\217\215\345\272\217\345\210\227\345\214\226.md" +++ /dev/null @@ -1,550 +0,0 @@ -# 目录 - * [序列化与反序列化概念](#序列化与反序列化概念) - * [Java对象的序列化与反序列化](#java对象的序列化与反序列化) - * [相关接口及类](#相关接口及类) - * [序列化ID](#序列化id) - * [静态变量不参与序列化](#静态变量不参与序列化) - * [探究ArrayList的序列化](#探究arraylist的序列化) - * [如何自定义的序列化和反序列化策略](#如何自定义的序列化和反序列化策略) - * [为什么要实现Serializable](#为什么要实现serializable) - * [序列化知识点总结](#序列化知识点总结) - * [参考文章](#参考文章) - - -本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 -> https://github.com/h2pl/Java-Tutorial - -喜欢的话麻烦点下Star哈 - -文章首发于我的个人博客: -> www.how2playlife.com - -本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。 -该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。 - -如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。 - - - -本文参考 http://www.importnew.com/17964.html和 -https://www.ibm.com/developerworks/cn/java/j-lo-serial/ - -## 序列化与反序列化概念 - -序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。 - -### Java对象的序列化与反序列化 - -在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。 - -只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。 - -但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。 - -> 对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。 - -对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。 - -在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。 - -### 相关接口及类 - -Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类: - - java.io.Serializable - - java.io.Externalizable - - ObjectOutput - - ObjectInput - - ObjectOutputStream - - ObjectInputStream - - Serializable 接口 - -**类通过实现 java.io.Serializable 接口以启用其序列化功能。** - -未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。 (该接口并没有方法和字段,为什么只有实现了该接口的类的对象才能被序列化呢?) - -当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出NotSerializableException。 - -如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成java.io.Serializable接口。 - -下面是一个实现了java.io.Serializable接口的类 -```` -public class 序列化和反序列化 { - public static void main(String[] args) { - - } - //注意,内部类不能进行序列化,因为它依赖于外部类 - @Test - public void test() throws IOException { - A a = new A(); - a.i = 1; - a.s = "a"; - FileOutputStream fileOutputStream = null; - FileInputStream fileInputStream = null; - try { - //将obj写入文件 - fileOutputStream = new FileOutputStream("temp"); - ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); - objectOutputStream.writeObject(a); - fileOutputStream.close(); - //通过文件读取obj - fileInputStream = new FileInputStream("temp"); - ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); - A a2 = (A) objectInputStream.readObject(); - fileInputStream.close(); - System.out.println(a2.i); - System.out.println(a2.s); - //打印结果和序列化之前相同 - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} - -class A implements Serializable { - - int i; - String s; -} -```` -**Externalizable接口** - -除了Serializable 之外,java中还提供了另一个序列化接口Externalizable - -为了了解Externalizable接口和Serializable接口的区别,先来看代码,我们把上面的代码改成使用Externalizable的形式。 -```` -class B implements Externalizable { - //必须要有公开无参构造函数。否则报错 - public B() { - - } - int i; - String s; - @Override - public void writeExternal(ObjectOutput out) throws IOException { - - } - - @Override - public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { - - } -} - -@Test - public void test2() throws IOException, ClassNotFoundException { - B b = new B(); - b.i = 1; - b.s = "a"; - //将obj写入文件 - FileOutputStream fileOutputStream = new FileOutputStream("temp"); - ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); - objectOutputStream.writeObject(b); - fileOutputStream.close(); - //通过文件读取obj - FileInputStream fileInputStream = new FileInputStream("temp"); - ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); - B b2 = (B) objectInputStream.readObject(); - fileInputStream.close(); - System.out.println(b2.i); - System.out.println(b2.s); - //打印结果为0和null,即初始值,没有被赋值 - //0 - //null - } -```` -通过上面的实例可以发现,对B类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和Serializable接口的区别: - -Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。 - -当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。 - -> 还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。 - -```` -class C implements Externalizable { - int i; - int j; - String s; - public C() { - - } - //实现下面两个方法可以选择序列化中需要被复制的成员。 - //并且,写入顺序和读取顺序要一致,否则报错。 - //可以写入多个同类型变量,顺序保持一致即可。 - @Override - public void writeExternal(ObjectOutput out) throws IOException { - out.writeInt(i); - out.writeInt(j); - out.writeObject(s); - } - - @Override - public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { - i = in.readInt(); - j = in.readInt(); - s = (String) in.readObject(); - } -} - -@Test -public void test3() throws IOException, ClassNotFoundException { - C c = new C(); - c.i = 1; - c.j = 2; - c.s = "a"; - //将obj写入文件 - FileOutputStream fileOutputStream = new FileOutputStream("temp"); - ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); - objectOutputStream.writeObject(c); - fileOutputStream.close(); - //通过文件读取obj - FileInputStream fileInputStream = new FileInputStream("temp"); - ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); - C c2 = (C) objectInputStream.readObject(); - fileInputStream.close(); - System.out.println(c2.i); - System.out.println(c2.j); - System.out.println(c2.s); - //打印结果为0和null,即初始值,没有被赋值 - //0 - //null -} -```` - -## 序列化ID - -序列化 ID 问题 -情境:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。 - -问题:C 对象的全类路径假设为 com.inout.Test,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。 - -解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。 -```` -package com.inout; - -import java.io.Serializable; - -public class A implements Serializable { - - private static final long serialVersionUID = 1L; - - private String name; - - public String getName() - { - return name; - } - - public void setName(String name) - { - this.name = name; - } -} - -package com.inout; - -import java.io.Serializable; - -public class A implements Serializable { - - private static final long serialVersionUID = 2L; - - private String name; - - public String getName() - { - return name; - } - - public void setName(String name) - { - this.name = name; - } -} -```` - -### 静态变量不参与序列化 - -清单 2 中的 main 方法,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。依照清单 2,这个 System.out.println(t.staticVar) 语句输出的是 10 还是 5 呢? -```` -public class Test implements Serializable { - - private static final long serialVersionUID = 1L; - - public static int staticVar = 5; - - public static void main(String[] args) { - try { - //初始时staticVar为5 - ObjectOutputStream out = new ObjectOutputStream( - new FileOutputStream("result.obj")); - out.writeObject(new Test()); - out.close(); - - //序列化后修改为10 - Test.staticVar = 10; - - ObjectInputStream oin = new ObjectInputStream(new FileInputStream( - "result.obj")); - Test t = (Test) oin.readObject(); - oin.close(); - - //再读取,通过t.staticVar打印新的值 - System.out.println(t.staticVar); - - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} -```` - -最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。 - -## 探究ArrayList的序列化 - -ArrayList的序列化 -在介绍ArrayList序列化之前,先来考虑一个问题: - -如何自定义的序列化和反序列化策略 - -带着这个问题,我们来看java.util.ArrayList的源码 - -```` - public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable - { - private static final long serialVersionUID = 8683452581122892189L; - transient Object[] elementData; // non-private to simplify nested class access - private int size; - } -```` - -笔者省略了其他成员变量,从上面的代码中可以知道ArrayList实现了java.io.Serializable接口,那么我们就可以对它进行序列化及反序列化。 - -因为elementData是transient的(1.8好像改掉了这一点),所以我们认为这个成员变量不会被序列化而保留下来。我们写一个Demo,验证一下我们的想法: -```` -public class ArrayList的序列化 { - public static void main(String[] args) throws IOException, ClassNotFoundException { - ArrayList list = new ArrayList(); - list.add("a"); - list.add("b"); - ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("arr")); - objectOutputStream.writeObject(list); - objectOutputStream.close(); - ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("arr")); - ArrayList list1 = (ArrayList) objectInputStream.readObject(); - objectInputStream.close(); - System.out.println(Arrays.toString(list.toArray())); - //序列化成功,里面的元素保持不变。 - } -```` -了解ArrayList的人都知道,ArrayList底层是通过数组实现的。那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。那么为什么code 4的结果却通过序列化和反序列化把List中的元素保留下来了呢? - -**writeObject和readObject方法** - -在ArrayList中定义了来个方法: writeObject和readObject。 - -这里先给出结论: - -> 在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。 -> -> 如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。 -> -> 用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。 - -来看一下这两个方法的具体实现: -```` -private void readObject(java.io.ObjectInputStream s) - throws java.io.IOException, ClassNotFoundException { - elementData = EMPTY_ELEMENTDATA; - - // Read in size, and any hidden stuff - s.defaultReadObject(); - - // Read in capacity - s.readInt(); // ignored - - if (size > 0) { - // be like clone(), allocate array based upon size not capacity - ensureCapacityInternal(size); - - Object[] a = elementData; - // Read in all elements in the proper order. - for (int i=0; i